dce-reactkit 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cjs/index.js +662 -771
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/Pagination.d.ts +13 -0
  4. package/dist/cjs/types/constants/LOG_REVIEW_GET_LOGS_ROUTE.d.ts +6 -0
  5. package/dist/cjs/types/helpers/cloneDeep.d.ts +8 -0
  6. package/dist/cjs/types/index.d.ts +4 -1
  7. package/dist/cjs/types/types/LogReviewerFilterState/ActionErrorFilterState.d.ts +17 -0
  8. package/dist/cjs/types/types/LogReviewerFilterState/AdvancedFilterState.d.ts +21 -0
  9. package/dist/cjs/types/types/LogReviewerFilterState/ContextFilterState.d.ts +10 -0
  10. package/dist/cjs/types/types/LogReviewerFilterState/DateFilterState.d.ts +17 -0
  11. package/dist/cjs/types/types/LogReviewerFilterState/TagFilterState.d.ts +8 -0
  12. package/dist/cjs/types/types/LogReviewerFilterState/index.d.ts +17 -0
  13. package/dist/esm/index.js +661 -773
  14. package/dist/esm/index.js.map +1 -1
  15. package/dist/esm/types/components/Pagination.d.ts +13 -0
  16. package/dist/esm/types/constants/LOG_REVIEW_GET_LOGS_ROUTE.d.ts +6 -0
  17. package/dist/esm/types/helpers/cloneDeep.d.ts +8 -0
  18. package/dist/esm/types/index.d.ts +4 -1
  19. package/dist/esm/types/types/LogReviewerFilterState/ActionErrorFilterState.d.ts +17 -0
  20. package/dist/esm/types/types/LogReviewerFilterState/AdvancedFilterState.d.ts +21 -0
  21. package/dist/esm/types/types/LogReviewerFilterState/ContextFilterState.d.ts +10 -0
  22. package/dist/esm/types/types/LogReviewerFilterState/DateFilterState.d.ts +17 -0
  23. package/dist/esm/types/types/LogReviewerFilterState/TagFilterState.d.ts +8 -0
  24. package/dist/esm/types/types/LogReviewerFilterState/index.d.ts +17 -0
  25. package/dist/index.d.ts +98 -1
  26. package/package.json +2 -1
  27. package/src/components/LogReviewer.tsx +1133 -1307
  28. package/src/components/Pagination.tsx +172 -0
  29. package/src/constants/LOG_REVIEW_GET_LOGS_ROUTE.ts +9 -0
  30. package/src/helpers/cloneDeep.ts +11 -0
  31. package/src/index.ts +6 -0
  32. package/src/types/LogReviewerFilterState/ActionErrorFilterState.ts +24 -0
  33. package/src/types/LogReviewerFilterState/AdvancedFilterState.ts +36 -0
  34. package/src/types/LogReviewerFilterState/ContextFilterState.ts +16 -0
  35. package/src/types/LogReviewerFilterState/DateFilterState.ts +26 -0
  36. package/src/types/LogReviewerFilterState/TagFilterState.ts +9 -0
  37. package/src/types/LogReviewerFilterState/index.ts +24 -0
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  // Import React
8
- import React, { useReducer, useEffect } from 'react';
8
+ import React, { useEffect, useReducer, useRef } from 'react';
9
9
 
10
10
  // Import FontAwesome
11
11
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -16,6 +16,7 @@ import {
16
16
  faList,
17
17
  faTag,
18
18
  faTimes,
19
+ faSearch,
19
20
  } from '@fortawesome/free-solid-svg-icons';
20
21
 
21
22
  // Import shared helpers
@@ -25,7 +26,7 @@ import { alert, showFatalError } from './AppWrapper';
25
26
  import getHumanReadableDate from '../helpers/getHumanReadableDate';
26
27
 
27
28
  // Import shared constants
28
- import LOG_REVIEW_ROUTE_PATH_PREFIX from '../constants/LOG_REVIEW_ROUTE_PATH_PREFIX';
29
+ import LOG_REVIEW_GET_LOGS_ROUTE from '../constants/LOG_REVIEW_GET_LOGS_ROUTE';
29
30
 
30
31
  // Import shared types
31
32
  import Log from '../types/Log';
@@ -38,6 +39,13 @@ import IntelliTableColumn from '../types/IntelliTableColumn';
38
39
  import LogBuiltInMetadata from '../types/LogBuiltInMetadata';
39
40
  import LogMetadataContextMap from '../types/LogMetadataContextMap';
40
41
  import LogMetadataTargetMap from '../types/LogMetadataTargetMap';
42
+ import LogReviewerFilterState from '../types/LogReviewerFilterState';
43
+ import DateFilterState from '../types/LogReviewerFilterState/DateFilterState';
44
+ import ContextFilterState from '../types/LogReviewerFilterState/ContextFilterState';
45
+ import TagFilterState from '../types/LogReviewerFilterState/TagFilterState';
46
+ import ActionErrorFilterState from '../types/LogReviewerFilterState/ActionErrorFilterState';
47
+ import AdvancedFilterState from '../types/LogReviewerFilterState/AdvancedFilterState';
48
+ import Variant from '../types/Variant';
41
49
 
42
50
  // Import shared components
43
51
  import SimpleDateChooser from './SimpleDateChooser';
@@ -50,6 +58,8 @@ import TabBox from './TabBox';
50
58
  import RadioButton from './RadioButton';
51
59
  import ButtonInputGroup from './ButtonInputGroup';
52
60
  import IntelliTable from './IntelliTable';
61
+ import Pagination from './Pagination';
62
+ import cloneDeep from '../helpers/cloneDeep';
53
63
 
54
64
  /*------------------------------------------------------------------------*/
55
65
  /* -------------------------------- Types ------------------------------- */
@@ -63,13 +73,6 @@ type Props = {
63
73
  onClose: () => void,
64
74
  };
65
75
 
66
- // Map of loaded logs (year => month => Log[])
67
- type LogMap = {
68
- [k: string]: {
69
- [k: string]: Log[]
70
- }
71
- };
72
-
73
76
  // Triple of year/month/day
74
77
  type DateTriple = {
75
78
  // Full year
@@ -89,93 +92,6 @@ enum FilterDrawer {
89
92
  Advanced = 'advanced',
90
93
  }
91
94
 
92
- // Date filter state
93
- type DateFilterState = {
94
- // Current start date
95
- startDate: {
96
- // Full year
97
- year: number,
98
- // 1-indexed month
99
- month: number,
100
- // 1-indexed day
101
- day: number,
102
- },
103
- // Current end date
104
- endDate: {
105
- // Full year
106
- year: number,
107
- // 1-indexed month
108
- month: number,
109
- // 1-indexed day
110
- day: number,
111
- },
112
- };
113
-
114
- // Context filter state
115
- type ContextFilterState = {
116
- [k: string]: (
117
- // No subcontexts
118
- | boolean // True if selected
119
- // Includes subcontexts
120
- | {
121
- [k: string]: boolean // True if selected
122
- }
123
- )
124
- };
125
-
126
- // Tag filter state
127
- type TagFilterState = {
128
- [k: string]: boolean // tag => true if in the list of tags to show
129
- };
130
-
131
- // Action filter state (only relevant for action logs)
132
- type ActionErrorFilterState = {
133
- // Required type of log
134
- type: LogType | undefined, // If undefined, no filter applied
135
- // Query for error message (only relevant if type is error)
136
- errorMessage: string, // If empty, no filter applied
137
- // Query for error code (only relevant if type is error)
138
- errorCode: string, // If empty, no filter applied
139
- // Action targets to include (only relevant if type is action)
140
- target: {
141
- [k: string]: boolean
142
- },
143
- // Action types to include (only relevant if type is action)
144
- action: {
145
- [k: string]: boolean
146
- },
147
- };
148
-
149
- // Advanced filter state
150
- type AdvancedFilterState = {
151
- // Query for user first name (case insensitive)
152
- userFirstName: string, // If empty, no filter applied
153
- // Query for user last name (case insensitive)
154
- userLastName: string, // If empty, no filter applied
155
- // Query for user email (case insensitive)
156
- userEmail: string, // If empty, no filter applied
157
- // Match for userId (numerical)
158
- userId: string, // If empty, no filter applied
159
- // If true, include students
160
- includeLearners: boolean,
161
- // If true, include ttms
162
- includeTTMs: boolean,
163
- // If true, include admins
164
- includeAdmins: boolean,
165
- // Match for courseId (numerical)
166
- courseId: string, // If empty, no filter applied
167
- // Query for course name (case insensitive)
168
- courseName: string, // If empty, no filter applied
169
- // Required isMobile value
170
- isMobile: (true | false | undefined), // If undefined, no filter applied
171
- // Required log source value
172
- source: LogSource | undefined, // If undefined, no filter applied
173
- // Query for route path (only relevant if source is server)
174
- routePath: string, // If empty, no filter applied
175
- // Query for route template (only relevant if source is server)
176
- routeTemplate: string, // If empty, no filter applied
177
- };
178
-
179
95
  /*------------------------------------------------------------------------*/
180
96
  /* -------------------------------- Style ------------------------------- */
181
97
  /*------------------------------------------------------------------------*/
@@ -530,23 +446,31 @@ const genHumanReadableName = (machineReadableName: string) => {
530
446
 
531
447
  type State = {
532
448
  /* -------------- Logs -------------- */
533
- // True if currently loading
449
+ // True if currently loading new logs
534
450
  loading: boolean,
535
- // Loaded logs (year => month => Log[])
536
- logMap: LogMap,
451
+ // Loaded logs
452
+ logs: Log[],
537
453
  /* ------------- Filters ------------ */
538
454
  // Current expanded filter drawer
539
455
  expandedFilterDrawer: FilterDrawer | undefined,
540
456
  // State of date filters
541
- dateFilterState: DateFilterState,
457
+ pendingDateFilterState: DateFilterState,
542
458
  // State of context filters
543
- contextFilterState: ContextFilterState,
459
+ pendingContextFilterState: ContextFilterState,
544
460
  // State of tag filters
545
- tagFilterState: TagFilterState,
461
+ pendingTagFilterState: TagFilterState,
546
462
  // State of the action and error filter
547
- actionErrorFilterState: ActionErrorFilterState,
463
+ pendingActionErrorFilterState: ActionErrorFilterState,
548
464
  // State of the advanced filter
549
- advancedFilterState: AdvancedFilterState,
465
+ pendingAdvancedFilterState: AdvancedFilterState,
466
+ // Current page number
467
+ pageNumber: number,
468
+ // If true, there is another page to load
469
+ hasAnotherPage: boolean,
470
+ // Total number of pages
471
+ numPages: number,
472
+ // If true, filters have changed
473
+ userMadeFilterChange: boolean,
550
474
  };
551
475
 
552
476
  /* ------------- Actions ------------ */
@@ -555,7 +479,7 @@ type State = {
555
479
  enum ActionType {
556
480
  // Show the loading bar
557
481
  StartLoading = 'start-loading',
558
- // Finish loading one or more months of logs
482
+ // Finish loading logs
559
483
  FinishLoading = 'finish-loading',
560
484
  // Reset filters to initial values
561
485
  ResetFilters = 'reset-filters',
@@ -573,6 +497,14 @@ enum ActionType {
573
497
  UpdateActionErrorFilterState = 'update-action-error-filter-state',
574
498
  // Update the advanced filter state
575
499
  UpdateAdvancedFilterState = 'update-advanced-filter-state',
500
+ // Set has another page
501
+ SetHasAnotherPage = 'set-has-another-page',
502
+ // Set the number of pages
503
+ SetNumPages = 'set-num-pages',
504
+ // Set page number
505
+ SetPageNumber = 'set-page-number',
506
+ // Reset user made filter change indicator
507
+ ResetUserMadeFilterChange = 'reset-user-made-filter-change',
576
508
  }
577
509
 
578
510
  // Action definitions
@@ -580,8 +512,8 @@ type Action = (
580
512
  | {
581
513
  // Action type
582
514
  type: ActionType.FinishLoading,
583
- // Updated logMap
584
- logMap: LogMap,
515
+ // Updated logs
516
+ logs: Log[],
585
517
  }
586
518
  | {
587
519
  // Action type
@@ -629,11 +561,26 @@ type Action = (
629
561
  // New advanced filter state
630
562
  advancedFilterState: AdvancedFilterState,
631
563
  }
564
+ | {
565
+ type: ActionType.SetHasAnotherPage,
566
+ hasAnotherPage: boolean,
567
+ }
568
+ | {
569
+ type: ActionType.SetNumPages,
570
+ numPages: number,
571
+ }
572
+ | {
573
+ type: ActionType.SetPageNumber,
574
+ pageNumber: number,
575
+ }
576
+ | {
577
+ type: ActionType.StartLoading,
578
+ }
632
579
  | {
633
580
  // Action type
634
581
  type: (
635
- | ActionType.StartLoading
636
582
  | ActionType.HideFilterDrawer
583
+ | ActionType.ResetUserMadeFilterChange
637
584
  ),
638
585
  }
639
586
  );
@@ -656,7 +603,7 @@ const reducer = (state: State, action: Action): State => {
656
603
  return {
657
604
  ...state,
658
605
  loading: false,
659
- logMap: action.logMap,
606
+ logs: action.logs,
660
607
  };
661
608
  }
662
609
  case ActionType.ToggleFilterDrawer: {
@@ -678,41 +625,71 @@ const reducer = (state: State, action: Action): State => {
678
625
  case ActionType.ResetFilters: {
679
626
  return {
680
627
  ...state,
681
- dateFilterState: action.initDateFilterState,
682
- contextFilterState: action.initContextFilterState,
683
- tagFilterState: action.initTagFilterState,
684
- actionErrorFilterState: action.initActionErrorFilterState,
685
- advancedFilterState: action.initAdvancedFilterState,
628
+ pendingDateFilterState: action.initDateFilterState,
629
+ pendingContextFilterState: action.initContextFilterState,
630
+ pendingTagFilterState: action.initTagFilterState,
631
+ pendingActionErrorFilterState: action.initActionErrorFilterState,
632
+ pendingAdvancedFilterState: action.initAdvancedFilterState,
633
+ pageNumber: 1,
686
634
  };
687
635
  }
688
636
  case ActionType.UpdateDateFilterState: {
689
637
  return {
690
638
  ...state,
691
- dateFilterState: action.dateFilterState,
639
+ pendingDateFilterState: action.dateFilterState,
640
+ userMadeFilterChange: true,
692
641
  };
693
642
  }
694
643
  case ActionType.UpdateContextFilterState: {
695
644
  return {
696
645
  ...state,
697
- contextFilterState: action.contextFilterState,
646
+ pendingContextFilterState: action.contextFilterState,
647
+ userMadeFilterChange: true,
698
648
  };
699
649
  }
700
650
  case ActionType.UpdateTagFilterState: {
701
651
  return {
702
652
  ...state,
703
- tagFilterState: action.tagFilterState,
653
+ pendingTagFilterState: action.tagFilterState,
654
+ userMadeFilterChange: true,
704
655
  };
705
656
  }
706
657
  case ActionType.UpdateActionErrorFilterState: {
707
658
  return {
708
659
  ...state,
709
- actionErrorFilterState: action.actionErrorFilterState,
660
+ pendingActionErrorFilterState: action.actionErrorFilterState,
661
+ userMadeFilterChange: true,
710
662
  };
711
663
  }
712
664
  case ActionType.UpdateAdvancedFilterState: {
713
665
  return {
714
666
  ...state,
715
- advancedFilterState: action.advancedFilterState,
667
+ pendingAdvancedFilterState: action.advancedFilterState,
668
+ userMadeFilterChange: true,
669
+ };
670
+ }
671
+ case ActionType.SetHasAnotherPage: {
672
+ return {
673
+ ...state,
674
+ hasAnotherPage: action.hasAnotherPage,
675
+ };
676
+ }
677
+ case ActionType.SetNumPages: {
678
+ return {
679
+ ...state,
680
+ numPages: action.numPages,
681
+ };
682
+ }
683
+ case ActionType.SetPageNumber: {
684
+ return {
685
+ ...state,
686
+ pageNumber: action.pageNumber,
687
+ };
688
+ }
689
+ case ActionType.ResetUserMadeFilterChange: {
690
+ return {
691
+ ...state,
692
+ userMadeFilterChange: false,
716
693
  };
717
694
  }
718
695
  default: {
@@ -750,7 +727,9 @@ const LogReviewer: React.FC<Props> = (props) => {
750
727
 
751
728
  // If context has children, add an "uncategorized" subcontext
752
729
  if (typeof contextMap[context] !== 'string') {
753
- (contextMap[context] as any)[LogBuiltInMetadata.Context.Uncategorized] = (
730
+ (contextMap[context] as any)[
731
+ LogBuiltInMetadata.Context.Uncategorized
732
+ ] = (
754
733
  LogBuiltInMetadata.Context.Uncategorized
755
734
  );
756
735
  }
@@ -797,7 +776,7 @@ const LogReviewer: React.FC<Props> = (props) => {
797
776
  } else {
798
777
  // Case: subcontexts exist
799
778
  initContextFilterState[context] = {};
800
- Object.values(contextMap[context]).forEach((subcontext) => {
779
+ Object.keys(contextMap[context]).forEach((subcontext) => {
801
780
  // Skip self ("_")
802
781
  if (subcontext === '_') {
803
782
  return;
@@ -850,13 +829,17 @@ const LogReviewer: React.FC<Props> = (props) => {
850
829
  // Initial state
851
830
  const initialState: State = {
852
831
  loading: true,
853
- logMap: {},
832
+ logs: [],
854
833
  expandedFilterDrawer: undefined,
855
- dateFilterState: initDateFilterState,
856
- contextFilterState: initContextFilterState,
857
- tagFilterState: initTagFilterState,
858
- actionErrorFilterState: initActionErrorFilterState,
859
- advancedFilterState: initAdvancedFilterState,
834
+ pendingDateFilterState: initDateFilterState,
835
+ pendingContextFilterState: initContextFilterState,
836
+ pendingTagFilterState: initTagFilterState,
837
+ pendingActionErrorFilterState: initActionErrorFilterState,
838
+ pendingAdvancedFilterState: initAdvancedFilterState,
839
+ pageNumber: 1,
840
+ hasAnotherPage: false,
841
+ numPages: 1,
842
+ userMadeFilterChange: false,
860
843
  };
861
844
 
862
845
  // Initialize state
@@ -865,133 +848,100 @@ const LogReviewer: React.FC<Props> = (props) => {
865
848
  // Destructure common state
866
849
  const {
867
850
  loading,
868
- logMap,
851
+ logs,
869
852
  expandedFilterDrawer,
870
- dateFilterState,
871
- contextFilterState,
872
- tagFilterState,
873
- actionErrorFilterState,
874
- advancedFilterState,
853
+ pendingDateFilterState,
854
+ pendingContextFilterState,
855
+ pendingTagFilterState,
856
+ pendingActionErrorFilterState,
857
+ pendingAdvancedFilterState,
858
+ pageNumber,
859
+ numPages,
860
+ userMadeFilterChange,
875
861
  } = state;
876
862
 
863
+ /* -------------- Refs -------------- */
864
+
865
+ // Initialize refs
866
+ const activeFiltersRef = useRef({
867
+ dateFilterState: JSON.parse(JSON.stringify(pendingDateFilterState)),
868
+ contextFilterState: JSON.parse(JSON.stringify(pendingContextFilterState)),
869
+ tagFilterState: JSON.parse(JSON.stringify(pendingTagFilterState)),
870
+ actionErrorFilterState: JSON.parse(JSON.stringify(pendingActionErrorFilterState)),
871
+ advancedFilterState: JSON.parse(JSON.stringify(pendingAdvancedFilterState)),
872
+ });
873
+
877
874
  /*------------------------------------------------------------------------*/
878
875
  /* ------------------------- Component Functions ------------------------ */
879
876
  /*------------------------------------------------------------------------*/
880
877
 
881
878
  /**
882
- * Get the list of year/month combos that need to be loaded given a new
883
- * start or end date and the existing logMap
884
- * @author Gabe Abrams
885
- * @param newDateFilterState the new date filter state
886
- * @returns list of year/month combos that need to be loaded
887
- */
888
- const listMonthsToLoad = (
889
- newDateFilterState: DateFilterState,
890
- ): { year: number, month: number }[] => {
891
- // List of year/month combos that need to be loaded
892
- const toLoad: { year: number, month: number }[] = [];
893
-
894
- // Loop through dates
895
- let { year, month } = newDateFilterState.startDate;
896
- while (
897
- // Earlier year
898
- (year < newDateFilterState.endDate.year)
899
- // Current year but included month
900
- || (
901
- year === newDateFilterState.endDate.year
902
- && month <= newDateFilterState.endDate.month
903
- )
904
- ) {
905
- // Add to list if not already loaded
906
- if (
907
- !logMap[year]
908
- || !logMap[year][month]
909
- ) {
910
- toLoad.push({
911
- year,
912
- month,
913
- });
914
- }
915
-
916
- // Increment
917
- month += 1;
918
- if (month > 12) {
919
- month -= 12;
920
- year += 1;
921
- }
922
- }
923
-
924
- // Return
925
- return toLoad;
926
- };
927
-
928
- /**
929
- * Handle updated start/end dates (updates state, loads if necessary)
930
- * @author Gabe Abrams
931
- * @param newDateFilterState the new date filter state
879
+ * Fetch logs from the server based on current filters
880
+ * @author Yuen Ler Chow
881
+ * @param opts object containing all arguments
882
+ * @param opts.filters the filters to apply
883
+ * @param opts.pageNum the page number to fetch
884
+ * @param opts.filtersChanged if true, the filters have changed
932
885
  */
933
- const handleDateRangeUpdated = async (
934
- newDateFilterState: DateFilterState,
886
+ const fetchLogs = async (
887
+ opts: {
888
+ filters: LogReviewerFilterState,
889
+ pageNum: number,
890
+ filtersChanged: boolean,
891
+ },
935
892
  ) => {
936
- // Update state
937
- dispatch({
938
- type: ActionType.UpdateDateFilterState,
939
- dateFilterState: newDateFilterState,
940
- });
893
+ const {
894
+ filters,
895
+ pageNum,
896
+ filtersChanged,
897
+ } = opts;
941
898
 
942
- // Check which year/month combos we need to load
943
- const toLoad = listMonthsToLoad(newDateFilterState);
944
-
945
- // If nothing to load, finished
946
- if (toLoad.length === 0) {
947
- return;
948
- }
949
-
950
- // Start loading
951
899
  dispatch({
952
900
  type: ActionType.StartLoading,
953
901
  });
954
902
 
955
- // Load required months
956
903
  try {
957
- for (let i = 0; i < toLoad.length; i++) {
958
- // Destructure
959
- const { year, month } = toLoad[i];
960
-
961
- // Load
962
- let logs: Log[] = [];
963
- let pageNumber = 1;
964
- let hasAnotherPage = true;
965
-
966
- while (hasAnotherPage) {
967
- const response = await visitServerEndpoint({
968
- path: `${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/${year}/months/${month}`,
969
- method: 'GET',
970
- params: {
971
- pageNumber,
972
- },
973
- });
974
-
975
- logs = logs.concat(response.items);
976
- hasAnotherPage = response.hasAnotherPage;
977
- pageNumber += 1;
978
- }
904
+ // Send filters to the server
905
+ let fetchedLogs: Log[] = [];
906
+
907
+ // Get logs from server
908
+ const response = await visitServerEndpoint({
909
+ path: LOG_REVIEW_GET_LOGS_ROUTE,
910
+ method: 'GET',
911
+ params: {
912
+ pageNumber: pageNum,
913
+ filters,
914
+ countDocuments: filtersChanged,
915
+ },
916
+ });
979
917
 
980
- // Add to map
981
- if (!logMap[year]) {
982
- logMap[year] = {};
983
- }
984
- logMap[year][month] = logs;
918
+ fetchedLogs = fetchedLogs.concat(response.items);
919
+ dispatch({
920
+ type: ActionType.SetHasAnotherPage,
921
+ hasAnotherPage: response.hasAnotherPage,
922
+ });
923
+
924
+ if (filtersChanged && response.numPages !== undefined) {
925
+ dispatch({
926
+ type: ActionType.SetNumPages,
927
+ numPages: response.numPages,
928
+ });
985
929
  }
930
+
931
+ // Update logs in state
932
+ dispatch({
933
+ type: ActionType.FinishLoading,
934
+ logs: fetchedLogs,
935
+ });
936
+
937
+ // Update page number
938
+ dispatch({
939
+ type: ActionType.SetPageNumber,
940
+ pageNumber: pageNum,
941
+ });
986
942
  } catch (err) {
987
943
  return showFatalError(err);
988
944
  }
989
-
990
- // Finish loading
991
- dispatch({
992
- type: ActionType.FinishLoading,
993
- logMap,
994
- });
995
945
  };
996
946
 
997
947
  /*------------------------------------------------------------------------*/
@@ -999,350 +949,454 @@ const LogReviewer: React.FC<Props> = (props) => {
999
949
  /*------------------------------------------------------------------------*/
1000
950
 
1001
951
  /**
1002
- * Mount
1003
- * @author Gabe Abrams
952
+ * Fetch logs on mount
953
+ * @author Yuen Ler Chow
1004
954
  */
1005
- useEffect(
1006
- () => {
1007
- // Perform initial load
1008
- handleDateRangeUpdated(dateFilterState);
1009
- },
1010
- [],
1011
- );
955
+ useEffect(() => {
956
+ fetchLogs({
957
+ filters: {
958
+ dateFilterState: pendingDateFilterState,
959
+ contextFilterState: pendingContextFilterState,
960
+ tagFilterState: pendingTagFilterState,
961
+ actionErrorFilterState: pendingActionErrorFilterState,
962
+ advancedFilterState: pendingAdvancedFilterState,
963
+ },
964
+ pageNum: 1,
965
+ filtersChanged: true,
966
+ });
967
+ }, []);
1012
968
 
1013
969
  /*------------------------------------------------------------------------*/
1014
970
  /* ------------------------------- Render ------------------------------- */
1015
971
  /*------------------------------------------------------------------------*/
1016
-
1017
972
  /*----------------------------------------*/
1018
973
  /* --------------- Main UI -------------- */
1019
974
  /*----------------------------------------*/
1020
975
 
1021
- // Body that will be filled with the contents of the panel
1022
- let body: React.ReactNode;
976
+ /*----------------------------------------*/
977
+ /* ------------ Pagination -------------- */
978
+ /*----------------------------------------*/
979
+
980
+ const paginationControls = logs.length > 0 && (
981
+ <Pagination
982
+ currentPage={pageNumber}
983
+ numPages={numPages}
984
+ loading={loading}
985
+ onPageChanged={(targetPage) => {
986
+ const { current: activeFilters } = activeFiltersRef;
987
+ fetchLogs({
988
+ filters: {
989
+ dateFilterState: activeFilters.dateFilterState,
990
+ contextFilterState: activeFilters.contextFilterState,
991
+ tagFilterState: activeFilters.tagFilterState,
992
+ actionErrorFilterState: activeFilters.actionErrorFilterState,
993
+ advancedFilterState: activeFilters.advancedFilterState,
994
+ },
995
+ pageNum: targetPage,
996
+ filtersChanged: false,
997
+ });
998
+ }}
999
+ />
1000
+ );
1023
1001
 
1024
- /* ------------- Loading ------------ */
1002
+ /*----------------------------------------*/
1003
+ /* --------------- Filters -------------- */
1004
+ /*----------------------------------------*/
1025
1005
 
1026
- if (loading) {
1027
- body = (
1028
- <div className="text-center p-5">
1029
- <LoadingSpinner />
1006
+ // Filter toggle
1007
+ const filterToggles = (
1008
+ <div className="LogReviewer-filter-toggles">
1009
+ <div className="LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0">
1010
+ {/* Date */}
1011
+ <button
1012
+ type="button"
1013
+ id="LogReviewer-toggle-date-filter-drawer"
1014
+ className={`btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1015
+ aria-label="toggle date filter drawer"
1016
+ onClick={() => {
1017
+ dispatch({
1018
+ type: ActionType.ToggleFilterDrawer,
1019
+ filterDrawer: FilterDrawer.Date,
1020
+ });
1021
+ }}
1022
+ >
1023
+ <FontAwesomeIcon
1024
+ icon={faCalendar}
1025
+ className="me-2"
1026
+ />
1027
+ Date
1028
+ </button>
1029
+ {/* Context */}
1030
+ <button
1031
+ type="button"
1032
+ id="LogReviewer-toggle-context-filter-drawer"
1033
+ className={`btn btn-${FilterDrawer.Context === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1034
+ aria-label="toggle context filter drawer"
1035
+ onClick={() => {
1036
+ dispatch({
1037
+ type: ActionType.ToggleFilterDrawer,
1038
+ filterDrawer: FilterDrawer.Context,
1039
+ });
1040
+ }}
1041
+ >
1042
+ <FontAwesomeIcon
1043
+ icon={faCircle}
1044
+ className="me-2"
1045
+ />
1046
+ Context
1047
+ </button>
1048
+ {/* Tag */}
1049
+ {(LogMetadata.Tag && Object.keys(LogMetadata.Tag).length > 0) && (
1050
+ <button
1051
+ type="button"
1052
+ id="LogReviewer-toggle-tag-filter-drawer"
1053
+ className={`btn btn-${FilterDrawer.Tag === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1054
+ aria-label="toggle tag filter drawer"
1055
+ onClick={() => {
1056
+ dispatch({
1057
+ type: ActionType.ToggleFilterDrawer,
1058
+ filterDrawer: FilterDrawer.Tag,
1059
+ });
1060
+ }}
1061
+ >
1062
+ <FontAwesomeIcon
1063
+ icon={faTag}
1064
+ className="me-2"
1065
+ />
1066
+ Tag
1067
+ </button>
1068
+ )}
1069
+ {/* Action */}
1070
+ <button
1071
+ type="button"
1072
+ id="LogReviewer-toggle-action-filter-drawer"
1073
+ className={`btn btn-${FilterDrawer.Action === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1074
+ aria-label="toggle action and error filter drawer"
1075
+ onClick={() => {
1076
+ dispatch({
1077
+ type: ActionType.ToggleFilterDrawer,
1078
+ filterDrawer: FilterDrawer.Action,
1079
+ });
1080
+ }}
1081
+ >
1082
+ <FontAwesomeIcon
1083
+ icon={faHammer}
1084
+ className="me-2"
1085
+ />
1086
+ Action
1087
+ </button>
1088
+ {/* Advanced */}
1089
+ <button
1090
+ type="button"
1091
+ id="LogReviewer-toggle-advanced-filter-drawer"
1092
+ className={`btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1093
+ aria-label="toggle advanced filter drawer"
1094
+ onClick={() => {
1095
+ dispatch({
1096
+ type: ActionType.ToggleFilterDrawer,
1097
+ filterDrawer: FilterDrawer.Advanced,
1098
+ });
1099
+ }}
1100
+ >
1101
+ <FontAwesomeIcon
1102
+ icon={faList}
1103
+ className="me-2"
1104
+ />
1105
+ Advanced
1106
+ </button>
1107
+ {/* Reset */}
1108
+ <button
1109
+ type="button"
1110
+ id="LogReviewer-reset-filters-button"
1111
+ className="btn btn-light"
1112
+ aria-label="reset filters"
1113
+ onClick={() => {
1114
+ // Save active filters
1115
+ // Deep clone the initial filter states to avoid any reference issues
1116
+ // if the filter states are modified later
1117
+ activeFiltersRef.current = cloneDeep({
1118
+ dateFilterState: initDateFilterState,
1119
+ contextFilterState: initContextFilterState,
1120
+ tagFilterState: initTagFilterState,
1121
+ actionErrorFilterState: initActionErrorFilterState,
1122
+ advancedFilterState: initAdvancedFilterState,
1123
+ });
1124
+ fetchLogs(
1125
+ {
1126
+ filters: {
1127
+ dateFilterState: initDateFilterState,
1128
+ contextFilterState: initContextFilterState,
1129
+ tagFilterState: initTagFilterState,
1130
+ actionErrorFilterState: initActionErrorFilterState,
1131
+ advancedFilterState: initAdvancedFilterState,
1132
+ },
1133
+ pageNum: 1,
1134
+ filtersChanged: true,
1135
+ },
1136
+ );
1137
+ dispatch({
1138
+ type: ActionType.ResetFilters,
1139
+ initActionErrorFilterState,
1140
+ initAdvancedFilterState,
1141
+ initContextFilterState,
1142
+ initDateFilterState,
1143
+ initTagFilterState,
1144
+ });
1145
+ dispatch({
1146
+ type: ActionType.HideFilterDrawer,
1147
+ });
1148
+ }}
1149
+ >
1150
+ <FontAwesomeIcon
1151
+ icon={faTimes}
1152
+ />
1153
+ {' '}
1154
+ Reset
1155
+ </button>
1156
+
1157
+ {/* Submit filter changes */}
1158
+ { userMadeFilterChange && (
1159
+ <button
1160
+ type="button"
1161
+ id="LogReviewer-submit-filters-button"
1162
+ className="btn btn-primary ms-2"
1163
+ aria-label="submit filters"
1164
+ onClick={() => {
1165
+ dispatch({
1166
+ type: ActionType.HideFilterDrawer,
1167
+ });
1168
+
1169
+ // Reset user made filter change indicator
1170
+ dispatch({
1171
+ type: ActionType.ResetUserMadeFilterChange,
1172
+ });
1173
+
1174
+ // Save active filters
1175
+ // Deep clone the pending filter states to avoid any reference issues
1176
+ // if the filter states are modified later
1177
+ activeFiltersRef.current = cloneDeep({
1178
+ dateFilterState: pendingDateFilterState,
1179
+ contextFilterState: pendingContextFilterState,
1180
+ tagFilterState: pendingTagFilterState,
1181
+ actionErrorFilterState: pendingActionErrorFilterState,
1182
+ advancedFilterState: pendingAdvancedFilterState,
1183
+ });
1184
+
1185
+ fetchLogs(
1186
+ {
1187
+ filters: {
1188
+ dateFilterState: pendingDateFilterState,
1189
+ contextFilterState: pendingContextFilterState,
1190
+ tagFilterState: pendingTagFilterState,
1191
+ actionErrorFilterState: pendingActionErrorFilterState,
1192
+ advancedFilterState: pendingAdvancedFilterState,
1193
+ },
1194
+ pageNum: 1,
1195
+ filtersChanged: true,
1196
+ },
1197
+ );
1198
+ }}
1199
+ >
1200
+ <FontAwesomeIcon
1201
+ icon={faSearch}
1202
+ />
1203
+ {' '}
1204
+ Apply Filters
1205
+ </button>
1206
+ )}
1030
1207
  </div>
1031
- );
1032
- }
1208
+ </div>
1209
+ );
1033
1210
 
1034
- /* ------------ Review UI ----------- */
1035
-
1036
- if (!loading) {
1037
- /*----------------------------------------*/
1038
- /* --------------- Filters -------------- */
1039
- /*----------------------------------------*/
1040
-
1041
- // Filter toggle
1042
- const filterToggles = (
1043
- <div className="LogReviewer-filter-toggles">
1044
- <h3 className="m-0">
1045
- Filters:
1046
- </h3>
1047
- <div className="LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0">
1048
- {/* Date */}
1049
- <button
1050
- type="button"
1051
- id="LogReviewer-toggle-date-filter-drawer"
1052
- className={`btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1053
- aria-label="toggle date filter drawer"
1054
- onClick={() => {
1055
- dispatch({
1056
- type: ActionType.ToggleFilterDrawer,
1057
- filterDrawer: FilterDrawer.Date,
1058
- });
1059
- }}
1060
- >
1061
- <FontAwesomeIcon
1062
- icon={faCalendar}
1063
- className="me-2"
1064
- />
1065
- Date
1066
- </button>
1067
- {/* Context */}
1068
- <button
1069
- type="button"
1070
- id="LogReviewer-toggle-context-filter-drawer"
1071
- className={`btn btn-${FilterDrawer.Context === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1072
- aria-label="toggle context filter drawer"
1073
- onClick={() => {
1074
- dispatch({
1075
- type: ActionType.ToggleFilterDrawer,
1076
- filterDrawer: FilterDrawer.Context,
1077
- });
1078
- }}
1079
- >
1080
- <FontAwesomeIcon
1081
- icon={faCircle}
1082
- className="me-2"
1083
- />
1084
- Context
1085
- </button>
1086
- {/* Tag */}
1087
- {/* Skip if no tags are used */}
1088
- {(LogMetadata.Tag && Object.keys(LogMetadata.Tag).length > 0) && (
1089
- <button
1090
- type="button"
1091
- id="LogReviewer-toggle-tag-filter-drawer"
1092
- className={`btn btn-${FilterDrawer.Tag === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1093
- aria-label="toggle tag filter drawer"
1094
- onClick={() => {
1095
- dispatch({
1096
- type: ActionType.ToggleFilterDrawer,
1097
- filterDrawer: FilterDrawer.Tag,
1098
- });
1099
- }}
1100
- >
1101
- <FontAwesomeIcon
1102
- icon={faTag}
1103
- className="me-2"
1104
- />
1105
- Tag
1106
- </button>
1107
- )}
1108
- {/* Action */}
1109
- <button
1110
- type="button"
1111
- id="LogReviewer-toggle-action-filter-drawer"
1112
- className={`btn btn-${FilterDrawer.Action === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1113
- aria-label="toggle action and error filter drawer"
1114
- onClick={() => {
1115
- dispatch({
1116
- type: ActionType.ToggleFilterDrawer,
1117
- filterDrawer: FilterDrawer.Action,
1118
- });
1119
- }}
1120
- >
1121
- <FontAwesomeIcon
1122
- icon={faHammer}
1123
- className="me-2"
1124
- />
1125
- Action
1126
- </button>
1127
- {/* Advanced */}
1128
- <button
1129
- type="button"
1130
- id="LogReviewer-toggle-advanced-filter-drawer"
1131
- className={`btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1132
- aria-label="toggle advanced filter drawer"
1133
- onClick={() => {
1134
- dispatch({
1135
- type: ActionType.ToggleFilterDrawer,
1136
- filterDrawer: FilterDrawer.Advanced,
1137
- });
1138
- }}
1139
- >
1140
- <FontAwesomeIcon
1141
- icon={faList}
1142
- className="me-2"
1143
- />
1144
- Advanced
1145
- </button>
1146
- {/* Reset */}
1147
- <button
1148
- type="button"
1149
- id="LogReviewer-reset-filters-button"
1150
- className="btn btn-light"
1151
- aria-label="reset filters"
1152
- onClick={() => {
1211
+ // Filter drawer
1212
+ let filterDrawer: React.ReactNode;
1213
+ if (expandedFilterDrawer) {
1214
+ if (expandedFilterDrawer === FilterDrawer.Date) {
1215
+ filterDrawer = (
1216
+ <TabBox title="Date">
1217
+ <SimpleDateChooser
1218
+ ariaLabel="filter start date"
1219
+ name="filter-start-date"
1220
+ year={pendingDateFilterState.startDate.year}
1221
+ month={pendingDateFilterState.startDate.month}
1222
+ day={pendingDateFilterState.startDate.day}
1223
+ dontAllowFuture
1224
+ numMonthsToShow={36}
1225
+ onChange={(month, day, year) => {
1226
+ const newDateFilterState = {
1227
+ ...pendingDateFilterState,
1228
+ startDate: { month, day, year },
1229
+ };
1153
1230
  dispatch({
1154
- type: ActionType.ResetFilters,
1155
- initActionErrorFilterState,
1156
- initAdvancedFilterState,
1157
- initContextFilterState,
1158
- initDateFilterState,
1159
- initTagFilterState,
1231
+ type: ActionType.UpdateDateFilterState,
1232
+ dateFilterState: newDateFilterState,
1160
1233
  });
1161
1234
  }}
1162
- >
1163
- <FontAwesomeIcon
1164
- icon={faTimes}
1165
- />
1166
- {' '}
1167
- Reset
1168
- </button>
1169
- </div>
1170
- </div>
1171
- );
1172
-
1173
- // Filter drawer
1174
- let filterDrawer: React.ReactNode;
1175
- if (expandedFilterDrawer) {
1176
- if (expandedFilterDrawer === FilterDrawer.Date) {
1177
- filterDrawer = (
1178
- <TabBox title="Date">
1179
- <SimpleDateChooser
1180
- ariaLabel="filter start date"
1181
- name="filter-start-date"
1182
- year={dateFilterState.startDate.year}
1183
- month={dateFilterState.startDate.month}
1184
- day={dateFilterState.startDate.day}
1185
- dontAllowFuture
1186
- numMonthsToShow={36}
1187
- onChange={(month, day, year) => {
1188
- dateFilterState.startDate = { month, day, year };
1189
- handleDateRangeUpdated(dateFilterState);
1190
- }}
1191
- />
1192
- {' '}
1193
- to
1194
- {' '}
1195
- <SimpleDateChooser
1196
- ariaLabel="filter end date"
1197
- name="filter-end-date"
1198
- year={dateFilterState.endDate.year}
1199
- month={dateFilterState.endDate.month}
1200
- day={dateFilterState.endDate.day}
1201
- dontAllowFuture
1202
- numMonthsToShow={12}
1203
- onChange={(month, day, year) => {
1204
- if (
1205
- year < dateFilterState.startDate.year
1235
+ />
1236
+ {' '}
1237
+ to
1238
+ {' '}
1239
+ <SimpleDateChooser
1240
+ ariaLabel="filter end date"
1241
+ name="filter-end-date"
1242
+ year={pendingDateFilterState.endDate.year}
1243
+ month={pendingDateFilterState.endDate.month}
1244
+ day={pendingDateFilterState.endDate.day}
1245
+ dontAllowFuture
1246
+ numMonthsToShow={12}
1247
+ onChange={(month, day, year) => {
1248
+ if (
1249
+ year < pendingDateFilterState.startDate.year
1206
1250
  || (
1207
- year === dateFilterState.startDate.year
1208
- && month < dateFilterState.startDate.month
1251
+ year === pendingDateFilterState.startDate.year
1252
+ && month < pendingDateFilterState.startDate.month
1209
1253
  )
1210
1254
  || (
1211
- year === dateFilterState.startDate.year
1212
- && month === dateFilterState.startDate.month
1213
- && day < dateFilterState.startDate.day
1255
+ year === pendingDateFilterState.startDate.year
1256
+ && month === pendingDateFilterState.startDate.month
1257
+ && day < pendingDateFilterState.startDate.day
1214
1258
  )
1215
- ) {
1216
- return alert(
1217
- 'Invalid Start Date',
1218
- 'The start date cannot be before the end date.',
1219
- );
1220
- }
1221
- dateFilterState.endDate = { month, day, year };
1222
- handleDateRangeUpdated(dateFilterState);
1223
- }}
1224
- />
1225
- </TabBox>
1226
- );
1227
- } else if (expandedFilterDrawer === FilterDrawer.Context) {
1228
- // Create item picker items
1229
- const builtInPickableItem: PickableItem = {
1230
- id: 'built-in-contexts',
1231
- name: 'Auto-logged',
1232
- isGroup: true,
1233
- children: [],
1234
- };
1235
- const pickableItems: PickableItem[] = [];
1236
- Object.keys(contextMap)
1237
- .forEach((context) => {
1238
- const value = contextMap[context];
1239
- if (typeof value === 'string') {
1240
- // No subcategories
1241
- const item: PickableItem = {
1242
- id: context,
1243
- name: genHumanReadableName(context),
1244
- isGroup: false,
1245
- checked: !!contextFilterState[context],
1246
- };
1247
-
1248
- // Add built-in items to its own folder
1249
- const isBuiltIn = context in LogBuiltInMetadata.Context;
1250
- if (isBuiltIn) {
1251
- // Add to built-in pickable item
1252
- builtInPickableItem.children.push(item);
1253
- } else {
1254
- // Add to pickable items list
1255
- pickableItems.push(item);
1259
+ ) {
1260
+ return alert(
1261
+ 'Invalid End Date',
1262
+ 'The end date cannot be before the start date.',
1263
+ );
1256
1264
  }
1257
- return;
1258
- }
1259
-
1260
- // Has subcategories
1261
- const children: PickableItem[] = (
1262
- Object.keys(value)
1263
- // Remove parent name
1264
- .filter((subcontext) => {
1265
- return subcontext !== '_';
1266
- })
1267
- // Create child pickable items
1268
- .map((subcontext) => {
1269
- return {
1270
- id: subcontext,
1271
- name: genHumanReadableName(subcontext),
1272
- isGroup: false,
1273
- checked: (contextFilterState[context] as any)[subcontext],
1274
- };
1275
- })
1276
- );
1265
+ const newDateFilterState = {
1266
+ ...pendingDateFilterState,
1267
+ endDate: { month, day, year },
1268
+ };
1269
+ dispatch({
1270
+ type: ActionType.UpdateDateFilterState,
1271
+ dateFilterState: newDateFilterState,
1272
+ });
1273
+ }}
1274
+ />
1275
+ </TabBox>
1276
+ );
1277
+ } else if (expandedFilterDrawer === FilterDrawer.Context) {
1278
+ // Create item picker items
1279
+ const builtInPickableItem: PickableItem = {
1280
+ id: 'built-in-contexts',
1281
+ name: 'Auto-logged',
1282
+ isGroup: true,
1283
+ children: [],
1284
+ };
1285
+ const pickableItems: PickableItem[] = [];
1286
+ Object.keys(contextMap)
1287
+ .forEach((context) => {
1288
+ const value = contextMap[context];
1289
+ if (typeof value === 'string') {
1290
+ // No subcategories
1277
1291
  const item: PickableItem = {
1278
1292
  id: context,
1279
1293
  name: genHumanReadableName(context),
1280
- isGroup: true,
1281
- children,
1294
+ isGroup: false,
1295
+ checked: !!pendingContextFilterState[context],
1282
1296
  };
1283
- pickableItems.push(item);
1284
- });
1285
- // Add built-in contexts to end ofl ist
1286
- pickableItems.push(builtInPickableItem);
1287
-
1288
- // Create filter UI
1289
- filterDrawer = (
1290
- <ItemPicker
1291
- title="Context"
1292
- items={pickableItems}
1293
- onChanged={(updatedItems) => {
1294
- // Update our state
1295
- updatedItems.forEach((pickableItem) => {
1296
- if (pickableItem.isGroup) {
1297
- // Has subcontexts
1298
-
1299
- if (pickableItem.id === 'built-in-contexts') {
1300
- // Built-in
1301
-
1302
- // Treat as if these were top-level contexts
1303
- pickableItem.children.forEach((subcontextItem) => {
1304
- contextFilterState[subcontextItem.id] = (
1305
- 'checked' in subcontextItem
1297
+
1298
+ // Add built-in items to its own folder
1299
+ const isBuiltIn = context in LogBuiltInMetadata.Context;
1300
+ if (isBuiltIn) {
1301
+ // Add to built-in pickable item
1302
+ builtInPickableItem.children.push(item);
1303
+ } else {
1304
+ // Add to pickable items list
1305
+ pickableItems.push(item);
1306
+ }
1307
+ return;
1308
+ }
1309
+
1310
+ // Has subcategories
1311
+ const children: PickableItem[] = (
1312
+ Object.keys(value)
1313
+ // Remove parent name
1314
+ .filter((subcontext) => {
1315
+ return (subcontext !== '_');
1316
+ })
1317
+ // Create child pickable items
1318
+ .map((subcontext) => {
1319
+ return {
1320
+ id: subcontext,
1321
+ name: genHumanReadableName(subcontext),
1322
+ isGroup: false,
1323
+ checked: (pendingContextFilterState[context] as any)[subcontext],
1324
+ };
1325
+ })
1326
+ );
1327
+ const item: PickableItem = {
1328
+ id: context,
1329
+ name: genHumanReadableName(context),
1330
+ isGroup: true,
1331
+ children,
1332
+ };
1333
+ pickableItems.push(item);
1334
+ });
1335
+ // Add built-in contexts to end of list
1336
+ pickableItems.push(builtInPickableItem);
1337
+
1338
+ // Create filter UI
1339
+ filterDrawer = (
1340
+ <ItemPicker
1341
+ title="Context"
1342
+ items={pickableItems}
1343
+ onChanged={(updatedItems) => {
1344
+ // Update our state
1345
+ const newContextFilterState = { ...pendingContextFilterState };
1346
+ updatedItems.forEach((pickableItem) => {
1347
+ if (pickableItem.isGroup) {
1348
+ // Has subcontexts
1349
+
1350
+ if (pickableItem.id === 'built-in-contexts') {
1351
+ // Built-in
1352
+
1353
+ // Treat as if these were top-level contexts
1354
+ pickableItem.children.forEach((subcontextItem) => {
1355
+ newContextFilterState[subcontextItem.id] = (
1356
+ 'checked' in subcontextItem
1306
1357
  && subcontextItem.checked
1307
- );
1308
- });
1309
- } else {
1310
- // Not built-in
1311
- pickableItem.children.forEach((subcontextItem) => {
1312
- if (!subcontextItem.isGroup) {
1313
- (
1314
- contextFilterState[pickableItem.id] as { [k: string]: boolean }
1315
- )[subcontextItem.id] = (
1316
- subcontextItem.checked
1317
- );
1318
- }
1319
- });
1320
- }
1358
+ );
1359
+ });
1321
1360
  } else {
1322
- // No subcontexts
1323
- (contextFilterState as any)[pickableItem.id] = (
1324
- pickableItem.checked
1325
- );
1361
+ // Not built-in
1362
+ pickableItem.children.forEach((subcontextItem) => {
1363
+ if (!subcontextItem.isGroup) {
1364
+ (
1365
+ newContextFilterState[pickableItem.id] as {
1366
+ [k: string]: boolean
1367
+ }
1368
+ )[subcontextItem.id] = (
1369
+ subcontextItem.checked
1370
+ );
1371
+ }
1372
+ });
1326
1373
  }
1327
- });
1328
- dispatch({
1329
- type: ActionType.UpdateContextFilterState,
1330
- contextFilterState,
1331
- });
1332
- }}
1333
- />
1334
- );
1335
- } else if (expandedFilterDrawer === FilterDrawer.Tag) {
1336
- // Create filter UI
1337
- filterDrawer = (
1338
- <TabBox title="Tags">
1339
- <div>
1340
- If any tags are selected, logs must contain at least one
1341
- (but not necessarily all) of the
1342
- selected tags.
1343
- </div>
1344
- <div className="d-flex gap-1 flex-wrap">
1345
- {
1374
+ } else {
1375
+ // No subcontexts
1376
+ (newContextFilterState as any)[pickableItem.id] = (
1377
+ pickableItem.checked
1378
+ );
1379
+ }
1380
+ });
1381
+
1382
+ // Update state
1383
+ dispatch({
1384
+ type: ActionType.UpdateContextFilterState,
1385
+ contextFilterState: newContextFilterState,
1386
+ });
1387
+ }}
1388
+ />
1389
+ );
1390
+ } else if (expandedFilterDrawer === FilterDrawer.Tag) {
1391
+ // Create filter UI
1392
+ filterDrawer = (
1393
+ <TabBox title="Tags">
1394
+ <div>
1395
+ If any tags are selected, logs must contain at least one
1396
+ (but not necessarily all) of the selected tags.
1397
+ </div>
1398
+ <div className="d-flex gap-1 flex-wrap">
1399
+ {
1346
1400
  Object.keys(LogMetadata.Tag ?? {})
1347
1401
  .map((tag) => {
1348
1402
  const description = genHumanReadableName(tag);
@@ -1352,73 +1406,91 @@ const LogReviewer: React.FC<Props> = (props) => {
1352
1406
  id={`LogReviewer-tag-${tag}-checkbox`}
1353
1407
  text={description}
1354
1408
  ariaLabel={`require that logs be tagged with "${description}" or any other selected tag`}
1355
- checked={tagFilterState[tag]}
1409
+ checked={pendingTagFilterState[tag]}
1356
1410
  onChanged={(checked) => {
1357
- tagFilterState[tag] = checked;
1411
+ const newTagFilterState = {
1412
+ ...pendingTagFilterState,
1413
+ [tag]: checked,
1414
+ };
1358
1415
  dispatch({
1359
1416
  type: ActionType.UpdateTagFilterState,
1360
- tagFilterState,
1417
+ tagFilterState: newTagFilterState,
1361
1418
  });
1362
1419
  }}
1420
+ checkedVariant={Variant.Light}
1421
+ uncheckedVariant={Variant.Light}
1363
1422
  />
1364
1423
  );
1365
1424
  })
1366
1425
  }
1367
- </div>
1426
+ </div>
1427
+ </TabBox>
1428
+ );
1429
+ } else if (expandedFilterDrawer === FilterDrawer.Action) {
1430
+ // Create filter UI
1431
+ filterDrawer = (
1432
+ <>
1433
+ {/* Log Type */}
1434
+ <TabBox title="Log Type">
1435
+ <RadioButton
1436
+ id="LogReviewer-type-all"
1437
+ text="All Logs"
1438
+ onSelected={() => {
1439
+ const newActionErrorFilterState = {
1440
+ ...pendingActionErrorFilterState,
1441
+ type: undefined,
1442
+ };
1443
+ dispatch({
1444
+ type: ActionType.UpdateActionErrorFilterState,
1445
+ actionErrorFilterState: newActionErrorFilterState,
1446
+ });
1447
+ }}
1448
+ ariaLabel="show logs of all types"
1449
+ selected={pendingActionErrorFilterState.type === undefined}
1450
+ unselectedVariant={Variant.Light}
1451
+ />
1452
+ <RadioButton
1453
+ id="LogReviewer-type-action-only"
1454
+ text="Action Logs Only"
1455
+ onSelected={() => {
1456
+ const newActionErrorFilterState = {
1457
+ ...pendingActionErrorFilterState,
1458
+ type: LogType.Action,
1459
+ };
1460
+ dispatch({
1461
+ type: ActionType.UpdateActionErrorFilterState,
1462
+ actionErrorFilterState: newActionErrorFilterState,
1463
+ });
1464
+ }}
1465
+ ariaLabel="only show action logs"
1466
+ selected={pendingActionErrorFilterState.type === LogType.Action}
1467
+ unselectedVariant={Variant.Light}
1468
+ />
1469
+ <RadioButton
1470
+ id="LogReviewer-type-error-only"
1471
+ text="Action Error Only"
1472
+ onSelected={() => {
1473
+ const newActionErrorFilterState = {
1474
+ ...pendingActionErrorFilterState,
1475
+ type: LogType.Error,
1476
+ };
1477
+ dispatch({
1478
+ type: ActionType.UpdateActionErrorFilterState,
1479
+ actionErrorFilterState: newActionErrorFilterState,
1480
+ });
1481
+ }}
1482
+ ariaLabel="only show error logs"
1483
+ selected={pendingActionErrorFilterState.type === LogType.Error}
1484
+ noMarginOnRight
1485
+ selectedVariant={Variant.Light}
1486
+ unselectedVariant={Variant.Light}
1487
+ />
1368
1488
  </TabBox>
1369
- );
1370
- } else if (expandedFilterDrawer === FilterDrawer.Action) {
1371
- // Create filter UI
1372
- filterDrawer = (
1373
- <>
1374
- {/* Log Type */}
1375
- <TabBox title="Log Type">
1376
- <RadioButton
1377
- id="LogReviewer-type-all"
1378
- text="All Logs"
1379
- onSelected={() => {
1380
- actionErrorFilterState.type = undefined;
1381
- dispatch({
1382
- type: ActionType.UpdateActionErrorFilterState,
1383
- actionErrorFilterState,
1384
- });
1385
- }}
1386
- ariaLabel="show logs of all types"
1387
- selected={actionErrorFilterState.type === undefined}
1388
- />
1389
- <RadioButton
1390
- id="LogReviewer-type-action-only"
1391
- text="Action Logs Only"
1392
- onSelected={() => {
1393
- actionErrorFilterState.type = LogType.Action;
1394
- dispatch({
1395
- type: ActionType.UpdateActionErrorFilterState,
1396
- actionErrorFilterState,
1397
- });
1398
- }}
1399
- ariaLabel="only show action logs"
1400
- selected={actionErrorFilterState.type === LogType.Action}
1401
- />
1402
- <RadioButton
1403
- id="LogReviewer-type-error-only"
1404
- text="Action Error Only"
1405
- onSelected={() => {
1406
- actionErrorFilterState.type = LogType.Error;
1407
- dispatch({
1408
- type: ActionType.UpdateActionErrorFilterState,
1409
- actionErrorFilterState,
1410
- });
1411
- }}
1412
- ariaLabel="only show error logs"
1413
- selected={actionErrorFilterState.type === LogType.Error}
1414
- noMarginOnRight
1415
- />
1416
- </TabBox>
1417
- {/* Actions */}
1418
- {
1489
+ {/* Actions */}
1490
+ {
1419
1491
  (
1420
- actionErrorFilterState.type === undefined
1421
- || actionErrorFilterState.type === LogType.Action
1492
+ pendingActionErrorFilterState.type === undefined
1493
+ || pendingActionErrorFilterState.type === LogType.Action
1422
1494
  ) && (
1423
1495
  <TabBox title="Action Log Details">
1424
1496
  {/* Action */}
@@ -1438,14 +1510,22 @@ const LogReviewer: React.FC<Props> = (props) => {
1438
1510
  text={description}
1439
1511
  ariaLabel={`include logs with action type "${description}" in results`}
1440
1512
  noMarginOnRight
1441
- checked={actionErrorFilterState.action[action]}
1513
+ checked={pendingActionErrorFilterState.action[action]}
1442
1514
  onChanged={(checked) => {
1443
- actionErrorFilterState.action[action] = checked;
1515
+ const newActionErrorFilterState = {
1516
+ ...pendingActionErrorFilterState,
1517
+ action: {
1518
+ ...pendingActionErrorFilterState.action,
1519
+ [action]: checked,
1520
+ },
1521
+ };
1444
1522
  dispatch({
1445
1523
  type: ActionType.UpdateActionErrorFilterState,
1446
- actionErrorFilterState,
1524
+ actionErrorFilterState: newActionErrorFilterState,
1447
1525
  });
1448
1526
  }}
1527
+ checkedVariant={Variant.Light}
1528
+ uncheckedVariant={Variant.Light}
1449
1529
  />
1450
1530
  );
1451
1531
  })
@@ -1467,15 +1547,23 @@ const LogReviewer: React.FC<Props> = (props) => {
1467
1547
  id={`LogReviewer-target-${target}-checkbox`}
1468
1548
  text={description}
1469
1549
  ariaLabel={`include logs with target "${description}" in results`}
1470
- checked={actionErrorFilterState.target[target]}
1550
+ checked={pendingActionErrorFilterState.target[target]}
1471
1551
  noMarginOnRight
1472
1552
  onChanged={(checked) => {
1473
- actionErrorFilterState.target[target] = checked;
1553
+ const newActionErrorFilterState = {
1554
+ ...pendingActionErrorFilterState,
1555
+ target: {
1556
+ ...pendingActionErrorFilterState.target,
1557
+ [target]: checked,
1558
+ },
1559
+ };
1474
1560
  dispatch({
1475
1561
  type: ActionType.UpdateActionErrorFilterState,
1476
- actionErrorFilterState,
1562
+ actionErrorFilterState: newActionErrorFilterState,
1477
1563
  });
1478
1564
  }}
1565
+ checkedVariant={Variant.Light}
1566
+ uncheckedVariant={Variant.Light}
1479
1567
  />
1480
1568
  );
1481
1569
  })
@@ -1484,11 +1572,11 @@ const LogReviewer: React.FC<Props> = (props) => {
1484
1572
  </TabBox>
1485
1573
  )
1486
1574
  }
1487
- {/* Errors */}
1488
- {
1575
+ {/* Errors */}
1576
+ {
1489
1577
  (
1490
- actionErrorFilterState.type === undefined
1491
- || actionErrorFilterState.type === LogType.Error
1578
+ pendingActionErrorFilterState.type === undefined
1579
+ || pendingActionErrorFilterState.type === LogType.Error
1492
1580
  ) && (
1493
1581
  <TabBox title="Error Log Details">
1494
1582
  {/* Message */}
@@ -1500,13 +1588,16 @@ const LogReviewer: React.FC<Props> = (props) => {
1500
1588
  type="text"
1501
1589
  className="form-control"
1502
1590
  aria-label="query for error message"
1503
- value={actionErrorFilterState.errorMessage}
1591
+ value={pendingActionErrorFilterState.errorMessage}
1504
1592
  placeholder="e.g. undefined is not a function"
1505
1593
  onChange={(e) => {
1506
- actionErrorFilterState.errorMessage = e.target.value;
1594
+ const newActionErrorFilterState = {
1595
+ ...pendingActionErrorFilterState,
1596
+ errorMessage: e.target.value,
1597
+ };
1507
1598
  dispatch({
1508
1599
  type: ActionType.UpdateActionErrorFilterState,
1509
- actionErrorFilterState,
1600
+ actionErrorFilterState: newActionErrorFilterState,
1510
1601
  });
1511
1602
  }}
1512
1603
  />
@@ -1520,17 +1611,20 @@ const LogReviewer: React.FC<Props> = (props) => {
1520
1611
  type="text"
1521
1612
  className="form-control"
1522
1613
  aria-label="query for error code"
1523
- value={actionErrorFilterState.errorCode}
1614
+ value={pendingActionErrorFilterState.errorCode}
1524
1615
  placeholder="e.g. GC22"
1525
1616
  onChange={(e) => {
1526
- actionErrorFilterState.errorCode = (
1527
- (e.target.value)
1528
- .trim()
1529
- .toUpperCase()
1530
- );
1617
+ const newActionErrorFilterState = {
1618
+ ...pendingActionErrorFilterState,
1619
+ errorCode: (
1620
+ (e.target.value)
1621
+ .trim()
1622
+ .toUpperCase()
1623
+ ),
1624
+ };
1531
1625
  dispatch({
1532
1626
  type: ActionType.UpdateActionErrorFilterState,
1533
- actionErrorFilterState,
1627
+ actionErrorFilterState: newActionErrorFilterState,
1534
1628
  });
1535
1629
  }}
1536
1630
  />
@@ -1538,722 +1632,460 @@ const LogReviewer: React.FC<Props> = (props) => {
1538
1632
  </TabBox>
1539
1633
  )
1540
1634
  }
1541
- </>
1542
- );
1543
- } else if (expandedFilterDrawer === FilterDrawer.Advanced) {
1544
- // Create advanced filter ui
1545
- filterDrawer = (
1546
- <>
1547
- {/* User Info */}
1548
- <TabBox title="User">
1549
- {/* First Name */}
1550
- <div className="input-group mb-2">
1551
- <span className="input-group-text">
1552
- User First Name
1553
- </span>
1554
- <input
1555
- type="text"
1556
- className="form-control"
1557
- aria-label="query for user first name"
1558
- value={advancedFilterState.userFirstName}
1559
- placeholder="e.g. Divardo"
1560
- onChange={(e) => {
1561
- advancedFilterState.userFirstName = e.target.value;
1635
+ </>
1636
+ );
1637
+ } else if (expandedFilterDrawer === FilterDrawer.Advanced) {
1638
+ // Create advanced filter ui
1639
+ filterDrawer = (
1640
+ <>
1641
+ {/* User Info */}
1642
+ <TabBox title="User">
1643
+ {/* First Name */}
1644
+ <div className="input-group mb-2">
1645
+ <span className="input-group-text">
1646
+ User First Name
1647
+ </span>
1648
+ <input
1649
+ type="text"
1650
+ className="form-control"
1651
+ aria-label="query for user first name"
1652
+ value={pendingAdvancedFilterState.userFirstName}
1653
+ placeholder="e.g. Divardo"
1654
+ onChange={(e) => {
1655
+ const newAdvancedFilterState = {
1656
+ ...pendingAdvancedFilterState,
1657
+ userFirstName: e.target.value,
1658
+ };
1659
+ dispatch({
1660
+ type: ActionType.UpdateAdvancedFilterState,
1661
+ advancedFilterState: newAdvancedFilterState,
1662
+ });
1663
+ }}
1664
+ />
1665
+ </div>
1666
+ {/* Last Name */}
1667
+ <div className="input-group mb-2">
1668
+ <span className="input-group-text">
1669
+ User Last Name
1670
+ </span>
1671
+ <input
1672
+ type="text"
1673
+ className="form-control"
1674
+ aria-label="query for user last name"
1675
+ value={pendingAdvancedFilterState.userLastName}
1676
+ placeholder="e.g. Calicci"
1677
+ onChange={(e) => {
1678
+ const newAdvancedFilterState = {
1679
+ ...pendingAdvancedFilterState,
1680
+ userLastName: e.target.value,
1681
+ };
1682
+ dispatch({
1683
+ type: ActionType.UpdateAdvancedFilterState,
1684
+ advancedFilterState: newAdvancedFilterState,
1685
+ });
1686
+ }}
1687
+ />
1688
+ </div>
1689
+ {/* Email */}
1690
+ <div className="input-group mb-2">
1691
+ <span className="input-group-text">
1692
+ User Email
1693
+ </span>
1694
+ <input
1695
+ type="text"
1696
+ className="form-control"
1697
+ aria-label="query for user email"
1698
+ value={pendingAdvancedFilterState.userEmail}
1699
+ placeholder="e.g. calicci@fas.harvard.edu"
1700
+ onChange={(e) => {
1701
+ const newAdvancedFilterState = {
1702
+ ...pendingAdvancedFilterState,
1703
+ userEmail: (
1704
+ (e.target.value)
1705
+ .trim()
1706
+ ),
1707
+ };
1708
+ dispatch({
1709
+ type: ActionType.UpdateAdvancedFilterState,
1710
+ advancedFilterState: newAdvancedFilterState,
1711
+ });
1712
+ }}
1713
+ />
1714
+ </div>
1715
+ {/* Canvas Id */}
1716
+ <div className="input-group mb-2">
1717
+ <span className="input-group-text">
1718
+ User Canvas Id
1719
+ </span>
1720
+ <input
1721
+ type="text"
1722
+ className="form-control"
1723
+ aria-label="query for user canvas id"
1724
+ value={pendingAdvancedFilterState.userId}
1725
+ placeholder="e.g. 104985"
1726
+ onChange={(e) => {
1727
+ const { value } = e.target;
1728
+ // Only update if value contains only numbers
1729
+ if (/^\d+$/.test(value) || value === '') {
1730
+ const newAdvancedFilterState = {
1731
+ ...pendingAdvancedFilterState,
1732
+ userId: (
1733
+ (e.target.value)
1734
+ .trim()
1735
+ ),
1736
+ };
1562
1737
  dispatch({
1563
1738
  type: ActionType.UpdateAdvancedFilterState,
1564
- advancedFilterState,
1739
+ advancedFilterState: newAdvancedFilterState,
1565
1740
  });
1566
- }}
1567
- />
1568
- </div>
1569
- {/* Last Name */}
1570
- <div className="input-group mb-2">
1571
- <span className="input-group-text">
1572
- User Last Name
1573
- </span>
1574
- <input
1575
- type="text"
1576
- className="form-control"
1577
- aria-label="query for user last name"
1578
- value={advancedFilterState.userLastName}
1579
- placeholder="e.g. Calicci"
1580
- onChange={(e) => {
1581
- advancedFilterState.userLastName = e.target.value;
1582
- dispatch({
1583
- type: ActionType.UpdateAdvancedFilterState,
1584
- advancedFilterState,
1585
- });
1586
- }}
1587
- />
1588
- </div>
1589
- {/* Email */}
1590
- <div className="input-group mb-2">
1591
- <span className="input-group-text">
1592
- User Email
1593
- </span>
1594
- <input
1595
- type="text"
1596
- className="form-control"
1597
- aria-label="query for user email"
1598
- value={advancedFilterState.userEmail}
1599
- placeholder="e.g. calicci@fas.harvard.edu"
1600
- onChange={(e) => {
1601
- advancedFilterState.userEmail = (
1602
- (e.target.value)
1603
- .trim()
1604
- );
1605
- dispatch({
1606
- type: ActionType.UpdateAdvancedFilterState,
1607
- advancedFilterState,
1608
- });
1609
- }}
1610
- />
1611
- </div>
1612
- {/* Canvas Id */}
1613
- <div className="input-group mb-2">
1614
- <span className="input-group-text">
1615
- User Canvas Id
1616
- </span>
1617
- <input
1618
- type="text"
1619
- className="form-control"
1620
- aria-label="query for user canvas id"
1621
- value={advancedFilterState.userId}
1622
- placeholder="e.g. 104985"
1623
- onChange={(e) => {
1624
- const { value } = e.target;
1625
- // Only update if value contains only numbers
1626
- if (/^\d+$/.test(value)) {
1627
- advancedFilterState.userId = (
1741
+ }
1742
+ }}
1743
+ />
1744
+ </div>
1745
+ {/* Role */}
1746
+ <ButtonInputGroup label="Role">
1747
+ <CheckboxButton
1748
+ text="Students"
1749
+ onChanged={(checked) => {
1750
+ const newAdvancedFilterState = {
1751
+ ...pendingAdvancedFilterState,
1752
+ includeLearners: checked,
1753
+ };
1754
+ dispatch({
1755
+ type: ActionType.UpdateAdvancedFilterState,
1756
+ advancedFilterState: newAdvancedFilterState,
1757
+ });
1758
+ }}
1759
+ checked={pendingAdvancedFilterState.includeLearners}
1760
+ ariaLabel="show logs from students"
1761
+ checkedVariant={Variant.Light}
1762
+ uncheckedVariant={Variant.Light}
1763
+ />
1764
+ <CheckboxButton
1765
+ text="Teaching Team Members"
1766
+ onChanged={(checked) => {
1767
+ const newAdvancedFilterState = {
1768
+ ...pendingAdvancedFilterState,
1769
+ includeTTMs: checked,
1770
+ };
1771
+ dispatch({
1772
+ type: ActionType.UpdateAdvancedFilterState,
1773
+ advancedFilterState: newAdvancedFilterState,
1774
+ });
1775
+ }}
1776
+ checked={pendingAdvancedFilterState.includeTTMs}
1777
+ ariaLabel="show logs from teaching team members"
1778
+ checkedVariant={Variant.Light}
1779
+ uncheckedVariant={Variant.Light}
1780
+ />
1781
+ <CheckboxButton
1782
+ text="Admins"
1783
+ onChanged={(checked) => {
1784
+ const newAdvancedFilterState = {
1785
+ ...pendingAdvancedFilterState,
1786
+ includeAdmins: checked,
1787
+ };
1788
+ dispatch({
1789
+ type: ActionType.UpdateAdvancedFilterState,
1790
+ advancedFilterState: newAdvancedFilterState,
1791
+ });
1792
+ }}
1793
+ checked={pendingAdvancedFilterState.includeAdmins}
1794
+ ariaLabel="show logs from admins"
1795
+ checkedVariant={Variant.Light}
1796
+ uncheckedVariant={Variant.Light}
1797
+ />
1798
+ </ButtonInputGroup>
1799
+ </TabBox>
1800
+
1801
+ {/* Course Info */}
1802
+ <TabBox title="Course">
1803
+ {/* Name */}
1804
+ <div className="input-group mb-2">
1805
+ <span className="input-group-text">
1806
+ Course Name
1807
+ </span>
1808
+ <input
1809
+ type="text"
1810
+ className="form-control"
1811
+ aria-label="query for course name"
1812
+ value={pendingAdvancedFilterState.courseName}
1813
+ placeholder="e.g. GLC 200"
1814
+ onChange={(e) => {
1815
+ const newAdvancedFilterState = {
1816
+ ...pendingAdvancedFilterState,
1817
+ courseName: e.target.value,
1818
+ };
1819
+ dispatch({
1820
+ type: ActionType.UpdateAdvancedFilterState,
1821
+ advancedFilterState: newAdvancedFilterState,
1822
+ });
1823
+ }}
1824
+ />
1825
+ </div>
1826
+ {/* Canvas Id */}
1827
+ <div className="input-group mb-2">
1828
+ <span className="input-group-text">
1829
+ Course Canvas Id
1830
+ </span>
1831
+ <input
1832
+ type="text"
1833
+ className="form-control"
1834
+ aria-label="query for course canvas id"
1835
+ value={pendingAdvancedFilterState.courseId}
1836
+ placeholder="e.g. 15948"
1837
+ onChange={(e) => {
1838
+ const { value } = e.target;
1839
+ // Only update if value contains only numbers
1840
+ if (/^\d+$/.test(value) || value === '') {
1841
+ const newAdvancedFilterState = {
1842
+ ...pendingAdvancedFilterState,
1843
+ courseId: (
1628
1844
  (e.target.value)
1629
1845
  .trim()
1630
- );
1631
- }
1632
- dispatch({
1633
- type: ActionType.UpdateAdvancedFilterState,
1634
- advancedFilterState,
1635
- });
1636
- }}
1637
- />
1638
- </div>
1639
- {/* Role */}
1640
- <ButtonInputGroup label="Role">
1641
- <CheckboxButton
1642
- text="Students"
1643
- onChanged={(checked) => {
1644
- advancedFilterState.includeLearners = checked;
1846
+ ),
1847
+ };
1645
1848
  dispatch({
1646
1849
  type: ActionType.UpdateAdvancedFilterState,
1647
- advancedFilterState,
1850
+ advancedFilterState: newAdvancedFilterState,
1648
1851
  });
1649
- }}
1650
- checked={advancedFilterState.includeLearners}
1651
- ariaLabel="show logs from students"
1652
- />
1653
- <CheckboxButton
1654
- text="Teaching Team Members"
1655
- onChanged={(checked) => {
1656
- advancedFilterState.includeTTMs = checked;
1657
- dispatch({
1658
- type: ActionType.UpdateAdvancedFilterState,
1659
- advancedFilterState,
1660
- });
1661
- }}
1662
- checked={advancedFilterState.includeTTMs}
1663
- ariaLabel="show logs from teaching team members"
1664
- />
1665
- <CheckboxButton
1666
- text="Admins"
1667
- onChanged={(checked) => {
1668
- advancedFilterState.includeAdmins = checked;
1669
- dispatch({
1670
- type: ActionType.UpdateAdvancedFilterState,
1671
- advancedFilterState,
1672
- });
1673
- }}
1674
- checked={advancedFilterState.includeAdmins}
1675
- ariaLabel="show logs from admins"
1676
- />
1677
- </ButtonInputGroup>
1678
- </TabBox>
1852
+ }
1853
+ }}
1854
+ />
1855
+ </div>
1856
+ </TabBox>
1857
+
1858
+ {/* Device Info */}
1859
+ <TabBox title="Device">
1860
+ <ButtonInputGroup label="Device Type">
1861
+ <RadioButton
1862
+ text="All Devices"
1863
+ ariaLabel="show logs from all devices"
1864
+ selected={pendingAdvancedFilterState.isMobile === undefined}
1865
+ onSelected={() => {
1866
+ const newAdvancedFilterState = {
1867
+ ...pendingAdvancedFilterState,
1868
+ isMobile: undefined,
1869
+ };
1870
+ dispatch({
1871
+ type: ActionType.UpdateAdvancedFilterState,
1872
+ advancedFilterState: newAdvancedFilterState,
1873
+ });
1874
+ }}
1875
+ selectedVariant={Variant.Light}
1876
+ unselectedVariant={Variant.Light}
1877
+ />
1878
+ <RadioButton
1879
+ text="Mobile Only"
1880
+ ariaLabel="show logs from mobile devices"
1881
+ selected={pendingAdvancedFilterState.isMobile === true}
1882
+ onSelected={() => {
1883
+ const newAdvancedFilterState = {
1884
+ ...pendingAdvancedFilterState,
1885
+ isMobile: true,
1886
+ };
1887
+ dispatch({
1888
+ type: ActionType.UpdateAdvancedFilterState,
1889
+ advancedFilterState: newAdvancedFilterState,
1890
+ });
1891
+ }}
1892
+ selectedVariant={Variant.Light}
1893
+ unselectedVariant={Variant.Light}
1894
+ />
1895
+ <RadioButton
1896
+ text="Desktop Only"
1897
+ ariaLabel="show logs from desktop devices"
1898
+ selected={pendingAdvancedFilterState.isMobile === false}
1899
+ onSelected={() => {
1900
+ const newAdvancedFilterState = {
1901
+ ...pendingAdvancedFilterState,
1902
+ isMobile: false,
1903
+ };
1904
+ dispatch({
1905
+ type: ActionType.UpdateAdvancedFilterState,
1906
+ advancedFilterState: newAdvancedFilterState,
1907
+ });
1908
+ }}
1909
+ noMarginOnRight
1910
+ selectedVariant={Variant.Light}
1911
+ unselectedVariant={Variant.Light}
1912
+ />
1913
+ </ButtonInputGroup>
1914
+ </TabBox>
1679
1915
 
1680
- {/* Course Info */}
1681
- <TabBox title="Course">
1682
- {/* Name */}
1916
+ {/* Source */}
1917
+ <TabBox title="Source">
1918
+ <ButtonInputGroup label="Source Type">
1919
+ <RadioButton
1920
+ text="Both"
1921
+ ariaLabel="show logs from all sources"
1922
+ selected={pendingAdvancedFilterState.source === undefined}
1923
+ onSelected={() => {
1924
+ const newAdvancedFilterState = {
1925
+ ...pendingAdvancedFilterState,
1926
+ source: undefined,
1927
+ };
1928
+ dispatch({
1929
+ type: ActionType.UpdateAdvancedFilterState,
1930
+ advancedFilterState: newAdvancedFilterState,
1931
+ });
1932
+ }}
1933
+ selectedVariant={Variant.Light}
1934
+ unselectedVariant={Variant.Light}
1935
+ />
1936
+ <RadioButton
1937
+ text="Client Only"
1938
+ ariaLabel="show logs from client source"
1939
+ selected={pendingAdvancedFilterState.source === LogSource.Client}
1940
+ onSelected={() => {
1941
+ const newAdvancedFilterState = {
1942
+ ...pendingAdvancedFilterState,
1943
+ source: LogSource.Client,
1944
+ };
1945
+ dispatch({
1946
+ type: ActionType.UpdateAdvancedFilterState,
1947
+ advancedFilterState: newAdvancedFilterState,
1948
+ });
1949
+ }}
1950
+ selectedVariant={Variant.Light}
1951
+ unselectedVariant={Variant.Light}
1952
+ />
1953
+ <RadioButton
1954
+ text="Server Only"
1955
+ ariaLabel="show logs from server source"
1956
+ selected={pendingAdvancedFilterState.source === LogSource.Server}
1957
+ onSelected={() => {
1958
+ const newAdvancedFilterState = {
1959
+ ...pendingAdvancedFilterState,
1960
+ source: LogSource.Server,
1961
+ };
1962
+ dispatch({
1963
+ type: ActionType.UpdateAdvancedFilterState,
1964
+ advancedFilterState: newAdvancedFilterState,
1965
+ });
1966
+ }}
1967
+ noMarginOnRight
1968
+ selectedVariant={Variant.Light}
1969
+ unselectedVariant={Variant.Light}
1970
+ />
1971
+ </ButtonInputGroup>
1972
+
1973
+ {/* Server filters */}
1974
+ {pendingAdvancedFilterState.source !== LogSource.Client && (
1975
+ <div className="mt-2">
1976
+ {/* Route path */}
1683
1977
  <div className="input-group mb-2">
1684
1978
  <span className="input-group-text">
1685
- Course Name
1979
+ Server Route Path
1686
1980
  </span>
1687
1981
  <input
1688
1982
  type="text"
1689
1983
  className="form-control"
1690
- aria-label="query for course name"
1691
- value={advancedFilterState.courseName}
1692
- placeholder="e.g. GLC 200"
1984
+ aria-label="query for server route path"
1985
+ value={pendingAdvancedFilterState.routePath}
1986
+ placeholder="e.g. /api/ttm/courses/12345"
1693
1987
  onChange={(e) => {
1694
- advancedFilterState.courseName = e.target.value;
1988
+ const newAdvancedFilterState = {
1989
+ ...pendingAdvancedFilterState,
1990
+ routePath: (
1991
+ (e.target.value)
1992
+ .trim()
1993
+ ),
1994
+ };
1695
1995
  dispatch({
1696
1996
  type: ActionType.UpdateAdvancedFilterState,
1697
- advancedFilterState,
1997
+ advancedFilterState: newAdvancedFilterState,
1698
1998
  });
1699
1999
  }}
1700
2000
  />
1701
2001
  </div>
1702
- {/* Canvas Id */}
2002
+
2003
+ {/* Route template */}
1703
2004
  <div className="input-group mb-2">
1704
2005
  <span className="input-group-text">
1705
- Course Canvas Id
2006
+ Server Route Template
1706
2007
  </span>
1707
2008
  <input
1708
2009
  type="text"
1709
2010
  className="form-control"
1710
- aria-label="query for course canvas id"
1711
- value={advancedFilterState.courseId}
1712
- placeholder="e.g. 15948"
2011
+ aria-label="query for server route template"
2012
+ value={pendingAdvancedFilterState.routeTemplate}
2013
+ placeholder="e.g. /api/ttm/courses/:courseId"
1713
2014
  onChange={(e) => {
1714
- const { value } = e.target;
1715
- // Only update if value contains only numbers
1716
- if (/^\d+$/.test(value)) {
1717
- advancedFilterState.courseId = (
2015
+ const newAdvancedFilterState = {
2016
+ ...pendingAdvancedFilterState,
2017
+ routeTemplate: (
1718
2018
  (e.target.value)
1719
2019
  .trim()
1720
- );
1721
- }
2020
+ ),
2021
+ };
1722
2022
  dispatch({
1723
2023
  type: ActionType.UpdateAdvancedFilterState,
1724
- advancedFilterState,
2024
+ advancedFilterState: newAdvancedFilterState,
1725
2025
  });
1726
2026
  }}
1727
2027
  />
1728
2028
  </div>
1729
- </TabBox>
1730
-
1731
- {/* Device Info */}
1732
- <TabBox title="Device">
1733
- <ButtonInputGroup label="Device Type">
1734
- <RadioButton
1735
- text="All Devices"
1736
- ariaLabel="show logs from all devices"
1737
- selected={advancedFilterState.isMobile === undefined}
1738
- onSelected={() => {
1739
- advancedFilterState.isMobile = undefined;
1740
- dispatch({
1741
- type: ActionType.UpdateAdvancedFilterState,
1742
- advancedFilterState,
1743
- });
1744
- }}
1745
- />
1746
- <RadioButton
1747
- text="Mobile Only"
1748
- ariaLabel="show logs from mobile devices"
1749
- selected={advancedFilterState.isMobile === true}
1750
- onSelected={() => {
1751
- advancedFilterState.isMobile = true;
1752
- dispatch({
1753
- type: ActionType.UpdateAdvancedFilterState,
1754
- advancedFilterState,
1755
- });
1756
- }}
1757
- />
1758
- <RadioButton
1759
- text="Desktop Only"
1760
- ariaLabel="show logs from desktop devices"
1761
- selected={advancedFilterState.isMobile === false}
1762
- onSelected={() => {
1763
- advancedFilterState.isMobile = false;
1764
- dispatch({
1765
- type: ActionType.UpdateAdvancedFilterState,
1766
- advancedFilterState,
1767
- });
1768
- }}
1769
- noMarginOnRight
1770
- />
1771
- </ButtonInputGroup>
1772
- </TabBox>
1773
-
1774
- {/* Source */}
1775
- <TabBox title="Source">
1776
- <ButtonInputGroup label="Source Type">
1777
- <RadioButton
1778
- text="Both"
1779
- ariaLabel="show logs from all sources"
1780
- selected={advancedFilterState.source === undefined}
1781
- onSelected={() => {
1782
- advancedFilterState.source = undefined;
1783
- dispatch({
1784
- type: ActionType.UpdateAdvancedFilterState,
1785
- advancedFilterState,
1786
- });
1787
- }}
1788
- />
1789
- <RadioButton
1790
- text="Client Only"
1791
- ariaLabel="show logs from client source"
1792
- selected={advancedFilterState.source === LogSource.Client}
1793
- onSelected={() => {
1794
- advancedFilterState.source = LogSource.Client;
1795
- dispatch({
1796
- type: ActionType.UpdateAdvancedFilterState,
1797
- advancedFilterState,
1798
- });
1799
- }}
1800
- />
1801
- <RadioButton
1802
- text="Server Only"
1803
- ariaLabel="show logs from server source"
1804
- selected={advancedFilterState.source === LogSource.Server}
1805
- onSelected={() => {
1806
- advancedFilterState.source = LogSource.Server;
1807
- dispatch({
1808
- type: ActionType.UpdateAdvancedFilterState,
1809
- advancedFilterState,
1810
- });
1811
- }}
1812
- noMarginOnRight
1813
- />
1814
- </ButtonInputGroup>
1815
-
1816
- {/* Server filters */}
1817
- {advancedFilterState.source !== LogSource.Client && (
1818
- <div className="mt-2">
1819
- {/* Route path */}
1820
- <div className="input-group mb-2">
1821
- <span className="input-group-text">
1822
- Server Route Path
1823
- </span>
1824
- <input
1825
- type="text"
1826
- className="form-control"
1827
- aria-label="query for server route path"
1828
- value={advancedFilterState.routePath}
1829
- placeholder="e.g. /api/ttm/courses/12345"
1830
- onChange={(e) => {
1831
- advancedFilterState.courseName = (
1832
- (e.target.value)
1833
- .trim()
1834
- );
1835
- dispatch({
1836
- type: ActionType.UpdateAdvancedFilterState,
1837
- advancedFilterState,
1838
- });
1839
- }}
1840
- />
1841
- </div>
1842
-
1843
- {/* Route template */}
1844
- <div className="input-group mb-2">
1845
- <span className="input-group-text">
1846
- Server Route Template
1847
- </span>
1848
- <input
1849
- type="text"
1850
- className="form-control"
1851
- aria-label="query for server route template"
1852
- value={advancedFilterState.routeTemplate}
1853
- placeholder="e.g. /api/ttm/courses/:courseId"
1854
- onChange={(e) => {
1855
- advancedFilterState.courseName = (
1856
- (e.target.value)
1857
- .trim()
1858
- );
1859
- dispatch({
1860
- type: ActionType.UpdateAdvancedFilterState,
1861
- advancedFilterState,
1862
- });
1863
- }}
1864
- />
1865
- </div>
1866
- </div>
1867
- )}
1868
- </TabBox>
1869
- </>
1870
- );
1871
- }
2029
+ </div>
2030
+ )}
2031
+ </TabBox>
2032
+ </>
2033
+ );
1872
2034
  }
2035
+ }
1873
2036
 
1874
- // Filters UI
1875
- const filters = (
1876
- <>
1877
- {filterToggles}
1878
- {filterDrawer && (
1879
- <Drawer customBackgroundColor="#eee">
1880
- {filterDrawer}
1881
- </Drawer>
1882
- )}
1883
- </>
1884
- );
1885
-
1886
- // Actually filter the logs
1887
- // > Perform filters
1888
- const logs: Log[] = [];
1889
- Object.keys(logMap).forEach((year) => {
1890
- Object.keys(logMap[year]).forEach((month) => {
1891
- logMap[year][month].forEach((log) => {
1892
- /* ----------- Date Filter ---------- */
1893
-
1894
- // Before start date
1895
- if (
1896
- // Previous year
1897
- log.year < dateFilterState.startDate.year
1898
- // Same year, earlier month
1899
- || (
1900
- (log.year === dateFilterState.startDate.year)
1901
- && (log.month < dateFilterState.startDate.month)
1902
- )
1903
- // Same year, same month, earlier day
1904
- || (
1905
- (log.year === dateFilterState.startDate.year)
1906
- && (log.month === dateFilterState.startDate.month)
1907
- && (log.day < dateFilterState.startDate.day)
1908
- )
1909
- ) {
1910
- return;
1911
- }
1912
-
1913
- // After end date
1914
- if (
1915
- // Later year
1916
- log.year > dateFilterState.endDate.year
1917
- // Same year, later month
1918
- || (
1919
- (log.year === dateFilterState.endDate.year)
1920
- && (log.month > dateFilterState.endDate.month)
1921
- )
1922
- // Same year, same month, later day
1923
- || (
1924
- (log.year === dateFilterState.endDate.year)
1925
- && (log.month === dateFilterState.endDate.month)
1926
- && (log.day > dateFilterState.endDate.day)
1927
- )
1928
- ) {
1929
- return;
1930
- }
1931
-
1932
- /* --------- Context Filter --------- */
1933
-
1934
- // Context doesn't match
1935
- if (
1936
- // Whole context is deselected
1937
- contextFilterState[log.context] === false
1938
- // None of the subcontexts are selected
1939
- || (
1940
- // Has subcontexts
1941
- typeof contextFilterState[log.context] !== 'boolean'
1942
- // None of the subcontexts are selected
1943
- && Object.values(contextFilterState[log.context] ?? {})
1944
- .every((isSelected) => {
1945
- return !isSelected;
1946
- })
1947
- )
1948
- ) {
1949
- return;
1950
- }
1951
-
1952
- // Subcontext doesn't match
1953
- if (
1954
- // Log context is not "uncategorized" (no point in further filters)
1955
- log.context !== LogBuiltInMetadata.Context.Uncategorized
1956
- // Log has a subcontext
1957
- && log.subcontext
1958
- // Context has subcontexts
1959
- && (
1960
- contextFilterState[log.context]
1961
- && contextFilterState[log.context] !== false
1962
- && contextFilterState[log.context] !== true
1963
- )
1964
- // Subcontext is not selected
1965
- && !(contextFilterState as any)[log.context][log.subcontext]
1966
- ) {
1967
- return;
1968
- }
1969
-
1970
- /* -------------- Tags -------------- */
1971
-
1972
- // No tags match
1973
- if (
1974
- // At least one tag is required
1975
- Object.values(tagFilterState)
1976
- .filter((isSelected) => {
1977
- return isSelected;
1978
- })
1979
- .length > 0
1980
- // No tags match
1981
- && log.tags.every((tag) => {
1982
- return !tagFilterState[tag];
1983
- })
1984
- ) {
1985
- return;
1986
- }
1987
-
1988
- /* ------- Actions and Errors ------- */
1989
-
1990
- // Log type doesn't match
1991
- if (
1992
- // Filter won't allow all types
1993
- actionErrorFilterState.type !== undefined
1994
- // Log type doesn't match
1995
- && actionErrorFilterState.type !== log.type
1996
- ) {
1997
- return;
1998
- }
1999
-
2000
- // Filter errors
2001
- if (log.type === LogType.Error) {
2002
- // Message doesn't match
2003
- if (
2004
- // Message exists
2005
- log.errorMessage
2006
- // Message filter exists
2007
- && actionErrorFilterState.errorMessage.trim().length > 0
2008
- // Message doesn't match
2009
- && log.errorMessage.toLowerCase().includes(
2010
- actionErrorFilterState.errorMessage.trim().toLowerCase(),
2011
- )
2012
- ) {
2013
- return;
2014
- }
2015
-
2016
- // Code doesn't match
2017
- if (
2018
- // Code exists
2019
- log.errorCode
2020
- // Code filter exists
2021
- && actionErrorFilterState.errorCode.trim().length > 0
2022
- // Code doesn't match
2023
- && log.errorCode.toUpperCase().includes(
2024
- actionErrorFilterState.errorCode.trim().toUpperCase(),
2025
- )
2026
- ) {
2027
- return;
2028
- }
2029
- }
2030
-
2031
- // Filter actions
2032
- if (log.type === LogType.Action) {
2033
- // Target isn't selected
2034
- if (
2035
- // Target exists
2036
- log.target
2037
- // Target isn't selected
2038
- && !actionErrorFilterState.target[log.target]
2039
- ) {
2040
- return;
2041
- }
2042
-
2043
- // Action
2044
- if (
2045
- // Action exists
2046
- log.action
2047
- // Action isn't selected
2048
- && !actionErrorFilterState.action[log.action]
2049
- ) {
2050
- return;
2051
- }
2052
- }
2053
-
2054
- /* --------- Advanced Filter -------- */
2055
-
2056
- // First name doesn't match
2057
- if (
2058
- // First name exists
2059
- log.userFirstName
2060
- // First name query doesn't match
2061
- && !log.userFirstName.toLowerCase().includes(
2062
- advancedFilterState.userFirstName.toLowerCase().trim(),
2063
- )
2064
- ) {
2065
- return;
2066
- }
2067
-
2068
- // Last name doesn't match
2069
- if (
2070
- // Last name exists
2071
- log.userLastName
2072
- // Last name query doesn't match
2073
- && !log.userLastName.toLowerCase().includes(
2074
- advancedFilterState.userLastName.toLowerCase().trim(),
2075
- )
2076
- ) {
2077
- return;
2078
- }
2079
-
2080
- // Email doesn't match
2081
- if (
2082
- // Email exists
2083
- log.userEmail
2084
- // Email query doesn't match
2085
- && !log.userEmail.toLowerCase().includes(
2086
- advancedFilterState.userEmail.toLowerCase().trim(),
2087
- )
2088
- ) {
2089
- return;
2090
- }
2091
-
2092
- // User id doesn't match
2093
- if (
2094
- // User id exists
2095
- log.userId
2096
- // User id doesn't match
2097
- && !String(log.userId).includes(
2098
- advancedFilterState.userId.trim(),
2099
- )
2100
- ) {
2101
- return;
2102
- }
2103
-
2104
- // Learner not allowed
2105
- if (
2106
- // User is a learner
2107
- log.isLearner
2108
- // Learners aren't included
2109
- && !advancedFilterState.includeLearners
2110
- ) {
2111
- return;
2112
- }
2113
-
2114
- // TTM not allowed
2115
- if (
2116
- // User is a ttm
2117
- log.isTTM
2118
- // TTMs aren't included
2119
- && !advancedFilterState.includeTTMs
2120
- ) {
2121
- return;
2122
- }
2123
-
2124
- // Admin not allowed
2125
- if (
2126
- // User is an admin
2127
- log.isAdmin
2128
- // Admins aren't included
2129
- && !advancedFilterState.includeAdmins
2130
- ) {
2131
- return;
2132
- }
2133
-
2134
- // Course Id doesn't match
2135
- if (
2136
- // Course Id exists
2137
- log.courseId
2138
- // Course Id doesn't match
2139
- && !String(log.courseId).includes(
2140
- advancedFilterState.courseId.trim(),
2141
- )
2142
- ) {
2143
- return;
2144
- }
2145
-
2146
- // Course name doesn't match
2147
- if (
2148
- // Course name exists
2149
- log.courseName
2150
- // Course name doesn't match
2151
- && !String(log.courseName).includes(
2152
- advancedFilterState.courseName.trim(),
2153
- )
2154
- ) {
2155
- return;
2156
- }
2157
-
2158
- // Mobile filter doesn't match
2159
- if (
2160
- // Mobile filter exists
2161
- advancedFilterState.isMobile !== undefined
2162
- // Device info exists
2163
- && log.device
2164
- // Mobile filter doesn't match
2165
- && (advancedFilterState.isMobile !== log.device.isMobile)
2166
- ) {
2167
- return;
2168
- }
2169
-
2170
- // Log source doesn't match
2171
- if (
2172
- // Source filter exists
2173
- advancedFilterState.source !== undefined
2174
- // Source info exists
2175
- && log.source
2176
- // Source filter doesn't match
2177
- && (advancedFilterState.source !== log.source)
2178
- ) {
2179
- return;
2180
- }
2181
-
2182
- // Route path doesn't match (Only for server source)
2183
- if (
2184
- // Source is server
2185
- (log.source === LogSource.Server)
2186
- // Route path is being filtered
2187
- && (advancedFilterState.routePath.trim().length)
2188
- // Route path doesn't match
2189
- && !(log.routePath.includes(advancedFilterState.routePath.trim()))
2190
- ) {
2191
- return;
2192
- }
2193
-
2194
- // Route template doesn't match (Only for server source)
2195
- if (
2196
- // Source is server
2197
- (log.source === LogSource.Server)
2198
- // Route template is being filtered
2199
- && (advancedFilterState.routeTemplate.trim().length)
2200
- // Route template doesn't match
2201
- && !(log.routeTemplate.includes(advancedFilterState.routeTemplate.trim()))
2202
- ) {
2203
- return;
2204
- }
2205
-
2206
- /* -------------- Done -------------- */
2207
-
2208
- // Made it past all filters. Add to the list
2209
- logs.push(log);
2210
- });
2211
- });
2212
- });
2037
+ // Filters UI
2038
+ const filters = (
2039
+ <>
2040
+ {filterToggles}
2041
+ {filterDrawer && (
2042
+ <Drawer customBackgroundColor="#eee">
2043
+ {filterDrawer}
2044
+ </Drawer>
2045
+ )}
2046
+ </>
2047
+ );
2213
2048
 
2214
- /*----------------------------------------*/
2215
- /* ---------------- Data ---------------- */
2216
- /*----------------------------------------*/
2217
-
2218
- // Nothing to show notice
2219
- const noLogsNotice = (
2220
- logs.length === 0
2221
- ? (
2222
- <div className="alert alert-warning text-center mt-2">
2223
- <h4 className="m-1">
2224
- No Logs to Show
2225
- </h4>
2226
- <div>
2227
- Either your filters are too strict or no matching logs have been
2228
- created yet.
2229
- </div>
2049
+ // Body that will be filled with the contents of the panel
2050
+ const body: React.ReactNode = (
2051
+ <>
2052
+ {
2053
+ loading && (
2054
+ <div className="text-center p-5">
2055
+ <LoadingSpinner />
2230
2056
  </div>
2231
2057
  )
2232
- : undefined
2233
- );
2234
-
2235
- // Create intelliTable
2236
- const dataTable = (
2237
- <IntelliTable
2238
- title="Matching Logs:"
2239
- csvName={`Logs from ${getHumanReadableDate()}`}
2240
- id="logs"
2241
- data={logs}
2242
- columns={columns}
2243
- />
2244
- );
2245
-
2246
- // Main body
2247
- body = (
2248
- <>
2249
- {filters}
2250
- <div className="mt-2">
2251
- {dataTable}
2252
- {noLogsNotice}
2058
+ }
2059
+ {!loading && (
2060
+ <div>
2061
+ {filters}
2253
2062
  </div>
2254
- </>
2255
- );
2256
- }
2063
+ )}
2064
+ {
2065
+ !loading && logs.length === 0 && (
2066
+ <div className="alert alert-warning text-center mt-2">
2067
+ <h4 className="m-1">No Logs to Show</h4>
2068
+ <div>
2069
+ Either your filters are too strict or no matching logs have been
2070
+ created yet.
2071
+ </div>
2072
+ </div>
2073
+ )
2074
+ }
2075
+ <div
2076
+ className={loading || logs.length === 0 ? 'd-none' : undefined}
2077
+ >
2078
+ <IntelliTable
2079
+ title="Matching Logs"
2080
+ csvName={`Logs from ${getHumanReadableDate()}`}
2081
+ id="logs"
2082
+ data={logs}
2083
+ columns={columns}
2084
+ />
2085
+ </div>
2086
+ {!loading && logs.length > 0 && paginationControls}
2087
+ </>
2088
+ );
2257
2089
 
2258
2090
  /* ---------- Wrap in Modal --------- */
2259
2091
 
@@ -2265,9 +2097,7 @@ const LogReviewer: React.FC<Props> = (props) => {
2265
2097
  <div className="LogReviewer-inner-container">
2266
2098
  <div className="LogReviewer-header">
2267
2099
  <div className="LogReviewer-header-title">
2268
- <h3 className="text-center m-0">
2269
- Log Review Dashboard
2270
- </h3>
2100
+ <h3 className="text-center">Log Review Dashboard</h3>
2271
2101
  </div>
2272
2102
  <div style={{ width: 0 }}>
2273
2103
  <button
@@ -2276,15 +2106,11 @@ const LogReviewer: React.FC<Props> = (props) => {
2276
2106
  aria-label="close log reviewer panel"
2277
2107
  onClick={onClose}
2278
2108
  >
2279
- <FontAwesomeIcon
2280
- icon={faTimes}
2281
- />
2109
+ <FontAwesomeIcon icon={faTimes} />
2282
2110
  </button>
2283
2111
  </div>
2284
2112
  </div>
2285
- <div className="LogReviewer-contents">
2286
- {body}
2287
- </div>
2113
+ <div className="LogReviewer-contents">{body}</div>
2288
2114
  </div>
2289
2115
  </div>
2290
2116
  );