dce-reactkit 3.9.4-beta-prompt.2 → 3.9.4-beta-logreviewer.3

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 (42) hide show
  1. package/dist/cjs/index.js +670 -869
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/AppWrapper.d.ts +0 -24
  4. package/dist/cjs/types/constants/LOG_REVIEW_GET_LOGS_ROUTE.d.ts +6 -0
  5. package/dist/cjs/types/index.d.ts +2 -2
  6. package/dist/cjs/types/types/LogReviewerFilterState/ActionErrorFilterState.d.ts +13 -0
  7. package/dist/cjs/types/types/LogReviewerFilterState/AdvancedFilterState.d.ts +17 -0
  8. package/dist/cjs/types/types/LogReviewerFilterState/ContextFilterState.d.ts +6 -0
  9. package/dist/cjs/types/types/LogReviewerFilterState/DateFilterState.d.ts +13 -0
  10. package/dist/cjs/types/types/LogReviewerFilterState/TagFilterState.d.ts +4 -0
  11. package/dist/cjs/types/types/LogReviewerFilterState/index.d.ts +17 -0
  12. package/dist/cjs/types/types/ModalType.d.ts +0 -1
  13. package/dist/esm/index.js +672 -870
  14. package/dist/esm/index.js.map +1 -1
  15. package/dist/esm/types/components/AppWrapper.d.ts +0 -24
  16. package/dist/esm/types/constants/LOG_REVIEW_GET_LOGS_ROUTE.d.ts +6 -0
  17. package/dist/esm/types/index.d.ts +2 -2
  18. package/dist/esm/types/types/LogReviewerFilterState/ActionErrorFilterState.d.ts +13 -0
  19. package/dist/esm/types/types/LogReviewerFilterState/AdvancedFilterState.d.ts +17 -0
  20. package/dist/esm/types/types/LogReviewerFilterState/ContextFilterState.d.ts +6 -0
  21. package/dist/esm/types/types/LogReviewerFilterState/DateFilterState.d.ts +13 -0
  22. package/dist/esm/types/types/LogReviewerFilterState/TagFilterState.d.ts +4 -0
  23. package/dist/esm/types/types/LogReviewerFilterState/index.d.ts +17 -0
  24. package/dist/esm/types/types/ModalType.d.ts +0 -1
  25. package/dist/index.d.ts +1 -26
  26. package/package.json +1 -1
  27. package/src/components/AppWrapper.tsx +0 -216
  28. package/src/components/LogReviewer.tsx +840 -1232
  29. package/src/components/Modal/index.tsx +0 -3
  30. package/src/constants/LOG_REVIEW_GET_LOGS_ROUTE.ts +9 -0
  31. package/src/index.ts +0 -2
  32. package/src/server/initServer.ts +216 -19
  33. package/src/types/LogReviewerFilterState/ActionErrorFilterState.ts +21 -0
  34. package/src/types/LogReviewerFilterState/AdvancedFilterState.ts +33 -0
  35. package/src/types/LogReviewerFilterState/ContextFilterState.ts +13 -0
  36. package/src/types/LogReviewerFilterState/DateFilterState.ts +23 -0
  37. package/src/types/LogReviewerFilterState/TagFilterState.ts +6 -0
  38. package/src/types/LogReviewerFilterState/index.ts +24 -0
  39. package/src/types/ModalType.tsx +0 -1
  40. package/dist/cjs/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +0 -6
  41. package/dist/esm/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +0 -6
  42. package/src/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.ts +0 -9
@@ -10,12 +10,15 @@ import React, { useReducer, useEffect } from 'react';
10
10
  // Import FontAwesome
11
11
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
12
12
  import {
13
+ faArrowLeft,
14
+ faArrowRight,
13
15
  faCalendar,
14
16
  faCircle,
15
17
  faHammer,
16
18
  faList,
17
19
  faTag,
18
20
  faTimes,
21
+ faSearch,
19
22
  } from '@fortawesome/free-solid-svg-icons';
20
23
 
21
24
  // Import shared helpers
@@ -25,7 +28,7 @@ import { alert, showFatalError } from './AppWrapper';
25
28
  import getHumanReadableDate from '../helpers/getHumanReadableDate';
26
29
 
27
30
  // Import shared constants
28
- import LOG_REVIEW_ROUTE_PATH_PREFIX from '../constants/LOG_REVIEW_ROUTE_PATH_PREFIX';
31
+ import LOG_REVIEW_GET_LOGS_ROUTE from '../constants/LOG_REVIEW_GET_LOGS_ROUTE';
29
32
 
30
33
  // Import shared types
31
34
  import Log from '../types/Log';
@@ -38,6 +41,12 @@ import IntelliTableColumn from '../types/IntelliTableColumn';
38
41
  import LogBuiltInMetadata from '../types/LogBuiltInMetadata';
39
42
  import LogMetadataContextMap from '../types/LogMetadataContextMap';
40
43
  import LogMetadataTargetMap from '../types/LogMetadataTargetMap';
44
+ import LogReviewerFilterState from '../types/LogReviewerFilterState';
45
+ import DateFilterState from '../types/LogReviewerFilterState/DateFilterState';
46
+ import ContextFilterState from '../types/LogReviewerFilterState/ContextFilterState';
47
+ import TagFilterState from '../types/LogReviewerFilterState/TagFilterState';
48
+ import ActionErrorFilterState from '../types/LogReviewerFilterState/ActionErrorFilterState';
49
+ import AdvancedFilterState from '../types/LogReviewerFilterState/AdvancedFilterState';
41
50
 
42
51
  // Import shared components
43
52
  import SimpleDateChooser from './SimpleDateChooser';
@@ -63,13 +72,6 @@ type Props = {
63
72
  onClose: () => void,
64
73
  };
65
74
 
66
- // Map of loaded logs (year => month => Log[])
67
- type LogMap = {
68
- [k: string]: {
69
- [k: string]: Log[]
70
- }
71
- };
72
-
73
75
  // Triple of year/month/day
74
76
  type DateTriple = {
75
77
  // Full year
@@ -89,93 +91,6 @@ enum FilterDrawer {
89
91
  Advanced = 'advanced',
90
92
  }
91
93
 
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
94
  /*------------------------------------------------------------------------*/
180
95
  /* -------------------------------- Style ------------------------------- */
181
96
  /*------------------------------------------------------------------------*/
@@ -532,8 +447,8 @@ type State = {
532
447
  /* -------------- Logs -------------- */
533
448
  // True if currently loading
534
449
  loading: boolean,
535
- // Loaded logs (year => month => Log[])
536
- logMap: LogMap,
450
+ // Loaded logs
451
+ logs: Log[],
537
452
  /* ------------- Filters ------------ */
538
453
  // Current expanded filter drawer
539
454
  expandedFilterDrawer: FilterDrawer | undefined,
@@ -547,6 +462,10 @@ type State = {
547
462
  actionErrorFilterState: ActionErrorFilterState,
548
463
  // State of the advanced filter
549
464
  advancedFilterState: AdvancedFilterState,
465
+ // Current page number
466
+ pageNumber: number,
467
+ // If true, there is another page to load
468
+ hasAnotherPage: boolean,
550
469
  };
551
470
 
552
471
  /* ------------- Actions ------------ */
@@ -555,7 +474,7 @@ type State = {
555
474
  enum ActionType {
556
475
  // Show the loading bar
557
476
  StartLoading = 'start-loading',
558
- // Finish loading one or more months of logs
477
+ // Finish loading logs
559
478
  FinishLoading = 'finish-loading',
560
479
  // Reset filters to initial values
561
480
  ResetFilters = 'reset-filters',
@@ -573,6 +492,12 @@ enum ActionType {
573
492
  UpdateActionErrorFilterState = 'update-action-error-filter-state',
574
493
  // Update the advanced filter state
575
494
  UpdateAdvancedFilterState = 'update-advanced-filter-state',
495
+ // Increment the page number
496
+ IncrementPageNumber = 'increment-page-number',
497
+ // Decrement the page number
498
+ DecrementPageNumber = 'decrement-page-number',
499
+ // Set has another page
500
+ SetHasAnotherPage = 'set-has-another-page',
576
501
  }
577
502
 
578
503
  // Action definitions
@@ -580,8 +505,8 @@ type Action = (
580
505
  | {
581
506
  // Action type
582
507
  type: ActionType.FinishLoading,
583
- // Updated logMap
584
- logMap: LogMap,
508
+ // Updated logs
509
+ logs: Log[],
585
510
  }
586
511
  | {
587
512
  // Action type
@@ -629,6 +554,16 @@ type Action = (
629
554
  // New advanced filter state
630
555
  advancedFilterState: AdvancedFilterState,
631
556
  }
557
+ | {
558
+ type: ActionType.IncrementPageNumber,
559
+ }
560
+ | {
561
+ type: ActionType.DecrementPageNumber,
562
+ }
563
+ | {
564
+ type: ActionType.SetHasAnotherPage,
565
+ hasAnotherPage: boolean,
566
+ }
632
567
  | {
633
568
  // Action type
634
569
  type: (
@@ -656,7 +591,7 @@ const reducer = (state: State, action: Action): State => {
656
591
  return {
657
592
  ...state,
658
593
  loading: false,
659
- logMap: action.logMap,
594
+ logs: action.logs,
660
595
  };
661
596
  }
662
597
  case ActionType.ToggleFilterDrawer: {
@@ -715,6 +650,24 @@ const reducer = (state: State, action: Action): State => {
715
650
  advancedFilterState: action.advancedFilterState,
716
651
  };
717
652
  }
653
+ case ActionType.IncrementPageNumber: {
654
+ return {
655
+ ...state,
656
+ pageNumber: state.pageNumber + 1,
657
+ };
658
+ }
659
+ case ActionType.DecrementPageNumber: {
660
+ return {
661
+ ...state,
662
+ pageNumber: state.pageNumber - 1,
663
+ };
664
+ }
665
+ case ActionType.SetHasAnotherPage: {
666
+ return {
667
+ ...state,
668
+ hasAnotherPage: action.hasAnotherPage,
669
+ };
670
+ }
718
671
  default: {
719
672
  return state;
720
673
  }
@@ -850,13 +803,15 @@ const LogReviewer: React.FC<Props> = (props) => {
850
803
  // Initial state
851
804
  const initialState: State = {
852
805
  loading: true,
853
- logMap: {},
806
+ logs: [],
854
807
  expandedFilterDrawer: undefined,
855
808
  dateFilterState: initDateFilterState,
856
809
  contextFilterState: initContextFilterState,
857
810
  tagFilterState: initTagFilterState,
858
811
  actionErrorFilterState: initActionErrorFilterState,
859
812
  advancedFilterState: initAdvancedFilterState,
813
+ pageNumber: 1,
814
+ hasAnotherPage: false,
860
815
  };
861
816
 
862
817
  // Initialize state
@@ -865,13 +820,15 @@ const LogReviewer: React.FC<Props> = (props) => {
865
820
  // Destructure common state
866
821
  const {
867
822
  loading,
868
- logMap,
823
+ logs,
869
824
  expandedFilterDrawer,
870
825
  dateFilterState,
871
826
  contextFilterState,
872
827
  tagFilterState,
873
828
  actionErrorFilterState,
874
829
  advancedFilterState,
830
+ pageNumber,
831
+ hasAnotherPage,
875
832
  } = state;
876
833
 
877
834
  /*------------------------------------------------------------------------*/
@@ -879,106 +836,48 @@ const LogReviewer: React.FC<Props> = (props) => {
879
836
  /*------------------------------------------------------------------------*/
880
837
 
881
838
  /**
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
839
+ * Fetch logs from the server based on current filters
932
840
  */
933
- const handleDateRangeUpdated = async (
934
- newDateFilterState: DateFilterState,
935
- ) => {
936
- // Update state
937
- dispatch({
938
- type: ActionType.UpdateDateFilterState,
939
- dateFilterState: newDateFilterState,
940
- });
941
-
942
- // Check which year/month combos we need to load
943
- const toLoad = listMonthsToLoad(newDateFilterState);
841
+ const fetchLogs = async () => {
842
+ dispatch({ type: ActionType.StartLoading });
944
843
 
945
- // If nothing to load, finished
946
- if (toLoad.length === 0) {
947
- return;
948
- }
844
+ try {
845
+ // Prepare filter parameters
846
+ const filters: LogReviewerFilterState = {
847
+ dateFilterState,
848
+ contextFilterState,
849
+ tagFilterState,
850
+ actionErrorFilterState,
851
+ advancedFilterState,
852
+ };
949
853
 
950
- // Start loading
951
- dispatch({
952
- type: ActionType.StartLoading,
953
- });
854
+ // Send filters to the server
855
+ let fetchedLogs: Log[] = [];
856
+
857
+ // Get logs from server
858
+ const response = await visitServerEndpoint({
859
+ path: LOG_REVIEW_GET_LOGS_ROUTE,
860
+ method: 'GET',
861
+ params: {
862
+ pageNumber,
863
+ filters,
864
+ },
865
+ });
954
866
 
955
- // Load required months
956
- try {
957
- for (let i = 0; i < toLoad.length; i++) {
958
- // Destructure
959
- const { year, month } = toLoad[i];
960
-
961
- // Load
962
- const logs = await visitServerEndpoint({
963
- path: `${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/${year}/months/${month}`,
964
- method: 'GET',
965
- });
867
+ fetchedLogs = fetchedLogs.concat(response.items);
868
+ dispatch({
869
+ type: ActionType.SetHasAnotherPage,
870
+ hasAnotherPage: response.hasAnotherPage,
871
+ });
966
872
 
967
- // Add to map
968
- if (!logMap[year]) {
969
- logMap[year] = {};
970
- }
971
- logMap[year][month] = logs;
972
- }
873
+ // Update logs in state
874
+ dispatch({
875
+ type: ActionType.FinishLoading,
876
+ logs: fetchedLogs,
877
+ });
973
878
  } catch (err) {
974
879
  return showFatalError(err);
975
880
  }
976
-
977
- // Finish loading
978
- dispatch({
979
- type: ActionType.FinishLoading,
980
- logMap,
981
- });
982
881
  };
983
882
 
984
883
  /*------------------------------------------------------------------------*/
@@ -986,21 +885,17 @@ const LogReviewer: React.FC<Props> = (props) => {
986
885
  /*------------------------------------------------------------------------*/
987
886
 
988
887
  /**
989
- * Mount
990
- * @author Gabe Abrams
888
+ * Fetch logs whenever page number changes
991
889
  */
992
- useEffect(
993
- () => {
994
- // Perform initial load
995
- handleDateRangeUpdated(dateFilterState);
996
- },
997
- [],
998
- );
890
+ useEffect(() => {
891
+ fetchLogs();
892
+ }, [
893
+ pageNumber,
894
+ ]);
999
895
 
1000
896
  /*------------------------------------------------------------------------*/
1001
897
  /* ------------------------------- Render ------------------------------- */
1002
898
  /*------------------------------------------------------------------------*/
1003
-
1004
899
  /*----------------------------------------*/
1005
900
  /* --------------- Main UI -------------- */
1006
901
  /*----------------------------------------*/
@@ -1020,176 +915,238 @@ const LogReviewer: React.FC<Props> = (props) => {
1020
915
 
1021
916
  /* ------------ Review UI ----------- */
1022
917
 
1023
- if (!loading) {
1024
- /*----------------------------------------*/
1025
- /* --------------- Filters -------------- */
1026
- /*----------------------------------------*/
1027
-
1028
- // Filter toggle
1029
- const filterToggles = (
1030
- <div className="LogReviewer-filter-toggles">
1031
- <h3 className="m-0">
1032
- Filters:
1033
- </h3>
1034
- <div className="LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0">
1035
- {/* Date */}
1036
- <button
1037
- type="button"
1038
- id="LogReviewer-toggle-date-filter-drawer"
1039
- className={`btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1040
- aria-label="toggle date filter drawer"
1041
- onClick={() => {
1042
- dispatch({
1043
- type: ActionType.ToggleFilterDrawer,
1044
- filterDrawer: FilterDrawer.Date,
1045
- });
1046
- }}
1047
- >
1048
- <FontAwesomeIcon
1049
- icon={faCalendar}
1050
- className="me-2"
1051
- />
1052
- Date
1053
- </button>
1054
- {/* Context */}
1055
- <button
1056
- type="button"
1057
- id="LogReviewer-toggle-context-filter-drawer"
1058
- className={`btn btn-${FilterDrawer.Context === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1059
- aria-label="toggle context filter drawer"
1060
- onClick={() => {
1061
- dispatch({
1062
- type: ActionType.ToggleFilterDrawer,
1063
- filterDrawer: FilterDrawer.Context,
1064
- });
1065
- }}
1066
- >
1067
- <FontAwesomeIcon
1068
- icon={faCircle}
1069
- className="me-2"
1070
- />
1071
- Context
1072
- </button>
1073
- {/* Tag */}
1074
- {/* Skip if no tags are used */}
1075
- {(LogMetadata.Tag && Object.keys(LogMetadata.Tag).length > 0) && (
1076
- <button
1077
- type="button"
1078
- id="LogReviewer-toggle-tag-filter-drawer"
1079
- className={`btn btn-${FilterDrawer.Tag === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1080
- aria-label="toggle tag filter drawer"
1081
- onClick={() => {
1082
- dispatch({
1083
- type: ActionType.ToggleFilterDrawer,
1084
- filterDrawer: FilterDrawer.Tag,
1085
- });
1086
- }}
1087
- >
1088
- <FontAwesomeIcon
1089
- icon={faTag}
1090
- className="me-2"
1091
- />
1092
- Tag
1093
- </button>
1094
- )}
1095
- {/* Action */}
1096
- <button
1097
- type="button"
1098
- id="LogReviewer-toggle-action-filter-drawer"
1099
- className={`btn btn-${FilterDrawer.Action === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1100
- aria-label="toggle action and error filter drawer"
1101
- onClick={() => {
1102
- dispatch({
1103
- type: ActionType.ToggleFilterDrawer,
1104
- filterDrawer: FilterDrawer.Action,
1105
- });
1106
- }}
1107
- >
1108
- <FontAwesomeIcon
1109
- icon={faHammer}
1110
- className="me-2"
1111
- />
1112
- Action
1113
- </button>
1114
- {/* Advanced */}
1115
- <button
1116
- type="button"
1117
- id="LogReviewer-toggle-advanced-filter-drawer"
1118
- className={`btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1119
- aria-label="toggle advanced filter drawer"
1120
- onClick={() => {
1121
- dispatch({
1122
- type: ActionType.ToggleFilterDrawer,
1123
- filterDrawer: FilterDrawer.Advanced,
1124
- });
1125
- }}
1126
- >
1127
- <FontAwesomeIcon
1128
- icon={faList}
1129
- className="me-2"
1130
- />
1131
- Advanced
1132
- </button>
1133
- {/* Reset */}
1134
- <button
1135
- type="button"
1136
- id="LogReviewer-reset-filters-button"
1137
- className="btn btn-light"
1138
- aria-label="reset filters"
1139
- onClick={() => {
918
+ /*----------------------------------------*/
919
+ /* ------------ Pagination -------------- */
920
+ /*----------------------------------------*/
921
+
922
+ const paginationControls = logs.length > 0 && (
923
+ <div className="text-center mt-3">
924
+ <button
925
+ type="button"
926
+ className="btn btn-secondary me-2"
927
+ disabled={pageNumber <= 1}
928
+ onClick={() => {
929
+ dispatch({
930
+ type: ActionType.DecrementPageNumber,
931
+ });
932
+ }}
933
+ >
934
+ <FontAwesomeIcon icon={faArrowLeft} className="me-2" />
935
+ Previous Page
936
+ </button>
937
+ <span className="mx-3">
938
+ Page
939
+ {' '}
940
+ {pageNumber}
941
+ </span>
942
+ <button
943
+ type="button"
944
+ className="btn btn-secondary ms-2"
945
+ disabled={!hasAnotherPage}
946
+ onClick={() => {
947
+ dispatch({
948
+ type: ActionType.IncrementPageNumber,
949
+ });
950
+ }}
951
+ >
952
+ Next Page
953
+ <FontAwesomeIcon icon={faArrowRight} className="ms-2" />
954
+ </button>
955
+ </div>
956
+ );
957
+
958
+ /*----------------------------------------*/
959
+ /* --------------- Filters -------------- */
960
+ /*----------------------------------------*/
961
+
962
+ // Filter toggle
963
+ const filterToggles = (
964
+ <div className="LogReviewer-filter-toggles">
965
+ <h3 className="m-0">
966
+ Filters:
967
+ </h3>
968
+ <div className="LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0">
969
+ {/* Date */}
970
+ <button
971
+ type="button"
972
+ id="LogReviewer-toggle-date-filter-drawer"
973
+ className={`btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
974
+ aria-label="toggle date filter drawer"
975
+ onClick={() => {
976
+ dispatch({
977
+ type: ActionType.ToggleFilterDrawer,
978
+ filterDrawer: FilterDrawer.Date,
979
+ });
980
+ }}
981
+ >
982
+ <FontAwesomeIcon
983
+ icon={faCalendar}
984
+ className="me-2"
985
+ />
986
+ Date
987
+ </button>
988
+ {/* Context */}
989
+ <button
990
+ type="button"
991
+ id="LogReviewer-toggle-context-filter-drawer"
992
+ className={`btn btn-${FilterDrawer.Context === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
993
+ aria-label="toggle context filter drawer"
994
+ onClick={() => {
995
+ dispatch({
996
+ type: ActionType.ToggleFilterDrawer,
997
+ filterDrawer: FilterDrawer.Context,
998
+ });
999
+ }}
1000
+ >
1001
+ <FontAwesomeIcon
1002
+ icon={faCircle}
1003
+ className="me-2"
1004
+ />
1005
+ Context
1006
+ </button>
1007
+ {/* Tag */}
1008
+ {/* Skip if no tags are used */}
1009
+ {(LogMetadata.Tag && Object.keys(LogMetadata.Tag).length > 0) && (
1010
+ <button
1011
+ type="button"
1012
+ id="LogReviewer-toggle-tag-filter-drawer"
1013
+ className={`btn btn-${FilterDrawer.Tag === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1014
+ aria-label="toggle tag filter drawer"
1015
+ onClick={() => {
1016
+ dispatch({
1017
+ type: ActionType.ToggleFilterDrawer,
1018
+ filterDrawer: FilterDrawer.Tag,
1019
+ });
1020
+ }}
1021
+ >
1022
+ <FontAwesomeIcon
1023
+ icon={faTag}
1024
+ className="me-2"
1025
+ />
1026
+ Tag
1027
+ </button>
1028
+ )}
1029
+ {/* Action */}
1030
+ <button
1031
+ type="button"
1032
+ id="LogReviewer-toggle-action-filter-drawer"
1033
+ className={`btn btn-${FilterDrawer.Action === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1034
+ aria-label="toggle action and error filter drawer"
1035
+ onClick={() => {
1036
+ dispatch({
1037
+ type: ActionType.ToggleFilterDrawer,
1038
+ filterDrawer: FilterDrawer.Action,
1039
+ });
1040
+ }}
1041
+ >
1042
+ <FontAwesomeIcon
1043
+ icon={faHammer}
1044
+ className="me-2"
1045
+ />
1046
+ Action
1047
+ </button>
1048
+ {/* Advanced */}
1049
+ <button
1050
+ type="button"
1051
+ id="LogReviewer-toggle-advanced-filter-drawer"
1052
+ className={`btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1053
+ aria-label="toggle advanced filter drawer"
1054
+ onClick={() => {
1055
+ dispatch({
1056
+ type: ActionType.ToggleFilterDrawer,
1057
+ filterDrawer: FilterDrawer.Advanced,
1058
+ });
1059
+ }}
1060
+ >
1061
+ <FontAwesomeIcon
1062
+ icon={faList}
1063
+ className="me-2"
1064
+ />
1065
+ Advanced
1066
+ </button>
1067
+ {/* Reset */}
1068
+ <button
1069
+ type="button"
1070
+ id="LogReviewer-reset-filters-button"
1071
+ className="btn btn-light"
1072
+ aria-label="reset filters"
1073
+ onClick={() => {
1074
+ dispatch({
1075
+ type: ActionType.ResetFilters,
1076
+ initActionErrorFilterState,
1077
+ initAdvancedFilterState,
1078
+ initContextFilterState,
1079
+ initDateFilterState,
1080
+ initTagFilterState,
1081
+ });
1082
+ }}
1083
+ >
1084
+ <FontAwesomeIcon
1085
+ icon={faTimes}
1086
+ />
1087
+ {' '}
1088
+ Reset
1089
+ </button>
1090
+
1091
+ {/* Submit filter changes */}
1092
+ <button
1093
+ type="button"
1094
+ id="LogReviewer-submit-filters-button"
1095
+ className="btn btn-primary ms-2"
1096
+ aria-label="submit filters"
1097
+ onClick={() => {
1098
+ dispatch({
1099
+ type: ActionType.HideFilterDrawer,
1100
+ });
1101
+ fetchLogs();
1102
+ }}
1103
+ >
1104
+ <FontAwesomeIcon
1105
+ icon={faSearch}
1106
+ />
1107
+ {' '}
1108
+ Filter
1109
+ </button>
1110
+ </div>
1111
+ </div>
1112
+ );
1113
+
1114
+ // Filter drawer
1115
+ let filterDrawer: React.ReactNode;
1116
+ if (expandedFilterDrawer) {
1117
+ if (expandedFilterDrawer === FilterDrawer.Date) {
1118
+ filterDrawer = (
1119
+ <TabBox title="Date">
1120
+ <SimpleDateChooser
1121
+ ariaLabel="filter start date"
1122
+ name="filter-start-date"
1123
+ year={dateFilterState.startDate.year}
1124
+ month={dateFilterState.startDate.month}
1125
+ day={dateFilterState.startDate.day}
1126
+ chooseFromPast
1127
+ numMonthsToShow={36}
1128
+ onChange={(month, day, year) => {
1129
+ dateFilterState.startDate = { month, day, year };
1140
1130
  dispatch({
1141
- type: ActionType.ResetFilters,
1142
- initActionErrorFilterState,
1143
- initAdvancedFilterState,
1144
- initContextFilterState,
1145
- initDateFilterState,
1146
- initTagFilterState,
1131
+ type: ActionType.UpdateDateFilterState,
1132
+ dateFilterState,
1147
1133
  });
1148
1134
  }}
1149
- >
1150
- <FontAwesomeIcon
1151
- icon={faTimes}
1152
- />
1153
- {' '}
1154
- Reset
1155
- </button>
1156
- </div>
1157
- </div>
1158
- );
1159
-
1160
- // Filter drawer
1161
- let filterDrawer: React.ReactNode;
1162
- if (expandedFilterDrawer) {
1163
- if (expandedFilterDrawer === FilterDrawer.Date) {
1164
- filterDrawer = (
1165
- <TabBox title="Date">
1166
- <SimpleDateChooser
1167
- ariaLabel="filter start date"
1168
- name="filter-start-date"
1169
- year={dateFilterState.startDate.year}
1170
- month={dateFilterState.startDate.month}
1171
- day={dateFilterState.startDate.day}
1172
- chooseFromPast
1173
- numMonthsToShow={36}
1174
- onChange={(month, day, year) => {
1175
- dateFilterState.startDate = { month, day, year };
1176
- handleDateRangeUpdated(dateFilterState);
1177
- }}
1178
- />
1179
- {' '}
1180
- to
1181
- {' '}
1182
- <SimpleDateChooser
1183
- ariaLabel="filter end date"
1184
- name="filter-end-date"
1185
- year={dateFilterState.endDate.year}
1186
- month={dateFilterState.endDate.month}
1187
- day={dateFilterState.endDate.day}
1188
- chooseFromPast
1189
- numMonthsToShow={12}
1190
- onChange={(month, day, year) => {
1191
- if (
1192
- year < dateFilterState.startDate.year
1135
+ />
1136
+ {' '}
1137
+ to
1138
+ {' '}
1139
+ <SimpleDateChooser
1140
+ ariaLabel="filter end date"
1141
+ name="filter-end-date"
1142
+ year={dateFilterState.endDate.year}
1143
+ month={dateFilterState.endDate.month}
1144
+ day={dateFilterState.endDate.day}
1145
+ chooseFromPast
1146
+ numMonthsToShow={12}
1147
+ onChange={(month, day, year) => {
1148
+ if (
1149
+ year < dateFilterState.startDate.year
1193
1150
  || (
1194
1151
  year === dateFilterState.startDate.year
1195
1152
  && month < dateFilterState.startDate.month
@@ -1199,137 +1156,140 @@ const LogReviewer: React.FC<Props> = (props) => {
1199
1156
  && month === dateFilterState.startDate.month
1200
1157
  && day < dateFilterState.startDate.day
1201
1158
  )
1202
- ) {
1203
- return alert(
1204
- 'Invalid Start Date',
1205
- 'The start date cannot be before the end date.',
1206
- );
1207
- }
1208
- dateFilterState.endDate = { month, day, year };
1209
- handleDateRangeUpdated(dateFilterState);
1210
- }}
1211
- />
1212
- </TabBox>
1213
- );
1214
- } else if (expandedFilterDrawer === FilterDrawer.Context) {
1215
- // Create item picker items
1216
- const builtInPickableItem: PickableItem = {
1217
- id: 'built-in-contexts',
1218
- name: 'Auto-logged',
1219
- isGroup: true,
1220
- children: [],
1221
- };
1222
- const pickableItems: PickableItem[] = [];
1223
- Object.keys(contextMap)
1224
- .forEach((context) => {
1225
- const value = contextMap[context];
1226
- if (typeof value === 'string') {
1227
- // No subcategories
1228
- const item: PickableItem = {
1229
- id: context,
1230
- name: genHumanReadableName(context),
1231
- isGroup: false,
1232
- checked: !!contextFilterState[context],
1233
- };
1234
-
1235
- // Add built-in items to its own folder
1236
- const isBuiltIn = context in LogBuiltInMetadata.Context;
1237
- if (isBuiltIn) {
1238
- // Add to built-in pickable item
1239
- builtInPickableItem.children.push(item);
1240
- } else {
1241
- // Add to pickable items list
1242
- pickableItems.push(item);
1159
+ ) {
1160
+ return alert(
1161
+ 'Invalid Start Date',
1162
+ 'The start date cannot be before the end date.',
1163
+ );
1243
1164
  }
1244
- return;
1245
- }
1246
-
1247
- // Has subcategories
1248
- const children: PickableItem[] = (
1249
- Object.keys(value)
1250
- // Remove parent name
1251
- .filter((subcontext) => {
1252
- return subcontext !== '_';
1253
- })
1254
- // Create child pickable items
1255
- .map((subcontext) => {
1256
- return {
1257
- id: subcontext,
1258
- name: genHumanReadableName(subcontext),
1259
- isGroup: false,
1260
- checked: (contextFilterState[context] as any)[subcontext],
1261
- };
1262
- })
1263
- );
1165
+ dateFilterState.endDate = { month, day, year };
1166
+ dispatch({
1167
+ type: ActionType.UpdateDateFilterState,
1168
+ dateFilterState,
1169
+ });
1170
+ }}
1171
+ />
1172
+ </TabBox>
1173
+ );
1174
+ } else if (expandedFilterDrawer === FilterDrawer.Context) {
1175
+ // Create item picker items
1176
+ const builtInPickableItem: PickableItem = {
1177
+ id: 'built-in-contexts',
1178
+ name: 'Auto-logged',
1179
+ isGroup: true,
1180
+ children: [],
1181
+ };
1182
+ const pickableItems: PickableItem[] = [];
1183
+ Object.keys(contextMap)
1184
+ .forEach((context) => {
1185
+ const value = contextMap[context];
1186
+ if (typeof value === 'string') {
1187
+ // No subcategories
1264
1188
  const item: PickableItem = {
1265
1189
  id: context,
1266
1190
  name: genHumanReadableName(context),
1267
- isGroup: true,
1268
- children,
1191
+ isGroup: false,
1192
+ checked: !!contextFilterState[context],
1269
1193
  };
1270
- pickableItems.push(item);
1271
- });
1272
- // Add built-in contexts to end ofl ist
1273
- pickableItems.push(builtInPickableItem);
1274
-
1275
- // Create filter UI
1276
- filterDrawer = (
1277
- <ItemPicker
1278
- title="Context"
1279
- items={pickableItems}
1280
- onChanged={(updatedItems) => {
1281
- // Update our state
1282
- updatedItems.forEach((pickableItem) => {
1283
- if (pickableItem.isGroup) {
1284
- // Has subcontexts
1285
-
1286
- if (pickableItem.id === 'built-in-contexts') {
1287
- // Built-in
1288
-
1289
- // Treat as if these were top-level contexts
1290
- pickableItem.children.forEach((subcontextItem) => {
1291
- contextFilterState[subcontextItem.id] = (
1292
- 'checked' in subcontextItem
1194
+
1195
+ // Add built-in items to its own folder
1196
+ const isBuiltIn = context in LogBuiltInMetadata.Context;
1197
+ if (isBuiltIn) {
1198
+ // Add to built-in pickable item
1199
+ builtInPickableItem.children.push(item);
1200
+ } else {
1201
+ // Add to pickable items list
1202
+ pickableItems.push(item);
1203
+ }
1204
+ return;
1205
+ }
1206
+
1207
+ // Has subcategories
1208
+ const children: PickableItem[] = (
1209
+ Object.keys(value)
1210
+ // Remove parent name
1211
+ .filter((subcontext) => {
1212
+ return subcontext !== '_';
1213
+ })
1214
+ // Create child pickable items
1215
+ .map((subcontext) => {
1216
+ return {
1217
+ id: subcontext,
1218
+ name: genHumanReadableName(subcontext),
1219
+ isGroup: false,
1220
+ checked: (contextFilterState[context] as any)[subcontext],
1221
+ };
1222
+ })
1223
+ );
1224
+ const item: PickableItem = {
1225
+ id: context,
1226
+ name: genHumanReadableName(context),
1227
+ isGroup: true,
1228
+ children,
1229
+ };
1230
+ pickableItems.push(item);
1231
+ });
1232
+ // Add built-in contexts to end ofl ist
1233
+ pickableItems.push(builtInPickableItem);
1234
+
1235
+ // Create filter UI
1236
+ filterDrawer = (
1237
+ <ItemPicker
1238
+ title="Context"
1239
+ items={pickableItems}
1240
+ onChanged={(updatedItems) => {
1241
+ // Update our state
1242
+ updatedItems.forEach((pickableItem) => {
1243
+ if (pickableItem.isGroup) {
1244
+ // Has subcontexts
1245
+
1246
+ if (pickableItem.id === 'built-in-contexts') {
1247
+ // Built-in
1248
+
1249
+ // Treat as if these were top-level contexts
1250
+ pickableItem.children.forEach((subcontextItem) => {
1251
+ contextFilterState[subcontextItem.id] = (
1252
+ 'checked' in subcontextItem
1293
1253
  && subcontextItem.checked
1294
- );
1295
- });
1296
- } else {
1297
- // Not built-in
1298
- pickableItem.children.forEach((subcontextItem) => {
1299
- if (!subcontextItem.isGroup) {
1300
- (
1301
- contextFilterState[pickableItem.id] as { [k: string]: boolean }
1302
- )[subcontextItem.id] = (
1303
- subcontextItem.checked
1304
- );
1305
- }
1306
- });
1307
- }
1254
+ );
1255
+ });
1308
1256
  } else {
1309
- // No subcontexts
1310
- (contextFilterState as any)[pickableItem.id] = (
1311
- pickableItem.checked
1312
- );
1257
+ // Not built-in
1258
+ pickableItem.children.forEach((subcontextItem) => {
1259
+ if (!subcontextItem.isGroup) {
1260
+ (
1261
+ contextFilterState[pickableItem.id] as { [k: string]: boolean }
1262
+ )[subcontextItem.id] = (
1263
+ subcontextItem.checked
1264
+ );
1265
+ }
1266
+ });
1313
1267
  }
1314
- });
1315
- dispatch({
1316
- type: ActionType.UpdateContextFilterState,
1317
- contextFilterState,
1318
- });
1319
- }}
1320
- />
1321
- );
1322
- } else if (expandedFilterDrawer === FilterDrawer.Tag) {
1323
- // Create filter UI
1324
- filterDrawer = (
1325
- <TabBox title="Tags">
1326
- <div>
1327
- If any tags are selected, logs must contain at least one
1328
- (but not necessarily all) of the
1329
- selected tags.
1330
- </div>
1331
- <div className="d-flex gap-1 flex-wrap">
1332
- {
1268
+ } else {
1269
+ // No subcontexts
1270
+ (contextFilterState as any)[pickableItem.id] = (
1271
+ pickableItem.checked
1272
+ );
1273
+ }
1274
+ });
1275
+ dispatch({
1276
+ type: ActionType.UpdateContextFilterState,
1277
+ contextFilterState,
1278
+ });
1279
+ }}
1280
+ />
1281
+ );
1282
+ } else if (expandedFilterDrawer === FilterDrawer.Tag) {
1283
+ // Create filter UI
1284
+ filterDrawer = (
1285
+ <TabBox title="Tags">
1286
+ <div>
1287
+ If any tags are selected, logs must contain at least one
1288
+ (but not necessarily all) of the
1289
+ selected tags.
1290
+ </div>
1291
+ <div className="d-flex gap-1 flex-wrap">
1292
+ {
1333
1293
  Object.keys(LogMetadata.Tag ?? {})
1334
1294
  .map((tag) => {
1335
1295
  const description = genHumanReadableName(tag);
@@ -1351,58 +1311,58 @@ const LogReviewer: React.FC<Props> = (props) => {
1351
1311
  );
1352
1312
  })
1353
1313
  }
1354
- </div>
1314
+ </div>
1315
+ </TabBox>
1316
+ );
1317
+ } else if (expandedFilterDrawer === FilterDrawer.Action) {
1318
+ // Create filter UI
1319
+ filterDrawer = (
1320
+ <>
1321
+ {/* Log Type */}
1322
+ <TabBox title="Log Type">
1323
+ <RadioButton
1324
+ id="LogReviewer-type-all"
1325
+ text="All Logs"
1326
+ onSelected={() => {
1327
+ actionErrorFilterState.type = undefined;
1328
+ dispatch({
1329
+ type: ActionType.UpdateActionErrorFilterState,
1330
+ actionErrorFilterState,
1331
+ });
1332
+ }}
1333
+ ariaLabel="show logs of all types"
1334
+ selected={actionErrorFilterState.type === undefined}
1335
+ />
1336
+ <RadioButton
1337
+ id="LogReviewer-type-action-only"
1338
+ text="Action Logs Only"
1339
+ onSelected={() => {
1340
+ actionErrorFilterState.type = LogType.Action;
1341
+ dispatch({
1342
+ type: ActionType.UpdateActionErrorFilterState,
1343
+ actionErrorFilterState,
1344
+ });
1345
+ }}
1346
+ ariaLabel="only show action logs"
1347
+ selected={actionErrorFilterState.type === LogType.Action}
1348
+ />
1349
+ <RadioButton
1350
+ id="LogReviewer-type-error-only"
1351
+ text="Action Error Only"
1352
+ onSelected={() => {
1353
+ actionErrorFilterState.type = LogType.Error;
1354
+ dispatch({
1355
+ type: ActionType.UpdateActionErrorFilterState,
1356
+ actionErrorFilterState,
1357
+ });
1358
+ }}
1359
+ ariaLabel="only show error logs"
1360
+ selected={actionErrorFilterState.type === LogType.Error}
1361
+ noMarginOnRight
1362
+ />
1355
1363
  </TabBox>
1356
- );
1357
- } else if (expandedFilterDrawer === FilterDrawer.Action) {
1358
- // Create filter UI
1359
- filterDrawer = (
1360
- <>
1361
- {/* Log Type */}
1362
- <TabBox title="Log Type">
1363
- <RadioButton
1364
- id="LogReviewer-type-all"
1365
- text="All Logs"
1366
- onSelected={() => {
1367
- actionErrorFilterState.type = undefined;
1368
- dispatch({
1369
- type: ActionType.UpdateActionErrorFilterState,
1370
- actionErrorFilterState,
1371
- });
1372
- }}
1373
- ariaLabel="show logs of all types"
1374
- selected={actionErrorFilterState.type === undefined}
1375
- />
1376
- <RadioButton
1377
- id="LogReviewer-type-action-only"
1378
- text="Action Logs Only"
1379
- onSelected={() => {
1380
- actionErrorFilterState.type = LogType.Action;
1381
- dispatch({
1382
- type: ActionType.UpdateActionErrorFilterState,
1383
- actionErrorFilterState,
1384
- });
1385
- }}
1386
- ariaLabel="only show action logs"
1387
- selected={actionErrorFilterState.type === LogType.Action}
1388
- />
1389
- <RadioButton
1390
- id="LogReviewer-type-error-only"
1391
- text="Action Error Only"
1392
- onSelected={() => {
1393
- actionErrorFilterState.type = LogType.Error;
1394
- dispatch({
1395
- type: ActionType.UpdateActionErrorFilterState,
1396
- actionErrorFilterState,
1397
- });
1398
- }}
1399
- ariaLabel="only show error logs"
1400
- selected={actionErrorFilterState.type === LogType.Error}
1401
- noMarginOnRight
1402
- />
1403
- </TabBox>
1404
- {/* Actions */}
1405
- {
1364
+ {/* Actions */}
1365
+ {
1406
1366
  (
1407
1367
  actionErrorFilterState.type === undefined
1408
1368
  || actionErrorFilterState.type === LogType.Action
@@ -1471,8 +1431,8 @@ const LogReviewer: React.FC<Props> = (props) => {
1471
1431
  </TabBox>
1472
1432
  )
1473
1433
  }
1474
- {/* Errors */}
1475
- {
1434
+ {/* Errors */}
1435
+ {
1476
1436
  (
1477
1437
  actionErrorFilterState.type === undefined
1478
1438
  || actionErrorFilterState.type === LogType.Error
@@ -1525,160 +1485,300 @@ const LogReviewer: React.FC<Props> = (props) => {
1525
1485
  </TabBox>
1526
1486
  )
1527
1487
  }
1528
- </>
1529
- );
1530
- } else if (expandedFilterDrawer === FilterDrawer.Advanced) {
1531
- // Create advanced filter ui
1532
- filterDrawer = (
1533
- <>
1534
- {/* User Info */}
1535
- <TabBox title="User">
1536
- {/* First Name */}
1537
- <div className="input-group mb-2">
1538
- <span className="input-group-text">
1539
- User First Name
1540
- </span>
1541
- <input
1542
- type="text"
1543
- className="form-control"
1544
- aria-label="query for user first name"
1545
- value={advancedFilterState.userFirstName}
1546
- placeholder="e.g. Divardo"
1547
- onChange={(e) => {
1548
- advancedFilterState.userFirstName = e.target.value;
1549
- dispatch({
1550
- type: ActionType.UpdateAdvancedFilterState,
1551
- advancedFilterState,
1552
- });
1553
- }}
1554
- />
1555
- </div>
1556
- {/* Last Name */}
1557
- <div className="input-group mb-2">
1558
- <span className="input-group-text">
1559
- User Last Name
1560
- </span>
1561
- <input
1562
- type="text"
1563
- className="form-control"
1564
- aria-label="query for user last name"
1565
- value={advancedFilterState.userLastName}
1566
- placeholder="e.g. Calicci"
1567
- onChange={(e) => {
1568
- advancedFilterState.userLastName = e.target.value;
1569
- dispatch({
1570
- type: ActionType.UpdateAdvancedFilterState,
1571
- advancedFilterState,
1572
- });
1573
- }}
1574
- />
1575
- </div>
1576
- {/* Email */}
1577
- <div className="input-group mb-2">
1578
- <span className="input-group-text">
1579
- User Email
1580
- </span>
1581
- <input
1582
- type="text"
1583
- className="form-control"
1584
- aria-label="query for user email"
1585
- value={advancedFilterState.userEmail}
1586
- placeholder="e.g. calicci@fas.harvard.edu"
1587
- onChange={(e) => {
1588
- advancedFilterState.userEmail = (
1488
+ </>
1489
+ );
1490
+ } else if (expandedFilterDrawer === FilterDrawer.Advanced) {
1491
+ // Create advanced filter ui
1492
+ filterDrawer = (
1493
+ <>
1494
+ {/* User Info */}
1495
+ <TabBox title="User">
1496
+ {/* First Name */}
1497
+ <div className="input-group mb-2">
1498
+ <span className="input-group-text">
1499
+ User First Name
1500
+ </span>
1501
+ <input
1502
+ type="text"
1503
+ className="form-control"
1504
+ aria-label="query for user first name"
1505
+ value={advancedFilterState.userFirstName}
1506
+ placeholder="e.g. Divardo"
1507
+ onChange={(e) => {
1508
+ advancedFilterState.userFirstName = e.target.value;
1509
+ dispatch({
1510
+ type: ActionType.UpdateAdvancedFilterState,
1511
+ advancedFilterState,
1512
+ });
1513
+ }}
1514
+ />
1515
+ </div>
1516
+ {/* Last Name */}
1517
+ <div className="input-group mb-2">
1518
+ <span className="input-group-text">
1519
+ User Last Name
1520
+ </span>
1521
+ <input
1522
+ type="text"
1523
+ className="form-control"
1524
+ aria-label="query for user last name"
1525
+ value={advancedFilterState.userLastName}
1526
+ placeholder="e.g. Calicci"
1527
+ onChange={(e) => {
1528
+ advancedFilterState.userLastName = e.target.value;
1529
+ dispatch({
1530
+ type: ActionType.UpdateAdvancedFilterState,
1531
+ advancedFilterState,
1532
+ });
1533
+ }}
1534
+ />
1535
+ </div>
1536
+ {/* Email */}
1537
+ <div className="input-group mb-2">
1538
+ <span className="input-group-text">
1539
+ User Email
1540
+ </span>
1541
+ <input
1542
+ type="text"
1543
+ className="form-control"
1544
+ aria-label="query for user email"
1545
+ value={advancedFilterState.userEmail}
1546
+ placeholder="e.g. calicci@fas.harvard.edu"
1547
+ onChange={(e) => {
1548
+ advancedFilterState.userEmail = (
1549
+ (e.target.value)
1550
+ .trim()
1551
+ );
1552
+ dispatch({
1553
+ type: ActionType.UpdateAdvancedFilterState,
1554
+ advancedFilterState,
1555
+ });
1556
+ }}
1557
+ />
1558
+ </div>
1559
+ {/* Canvas Id */}
1560
+ <div className="input-group mb-2">
1561
+ <span className="input-group-text">
1562
+ User Canvas Id
1563
+ </span>
1564
+ <input
1565
+ type="text"
1566
+ className="form-control"
1567
+ aria-label="query for user canvas id"
1568
+ value={advancedFilterState.userId}
1569
+ placeholder="e.g. 104985"
1570
+ onChange={(e) => {
1571
+ const { value } = e.target;
1572
+ // Only update if value contains only numbers
1573
+ if (/^\d+$/.test(value)) {
1574
+ advancedFilterState.userId = (
1589
1575
  (e.target.value)
1590
1576
  .trim()
1591
1577
  );
1592
- dispatch({
1593
- type: ActionType.UpdateAdvancedFilterState,
1594
- advancedFilterState,
1595
- });
1596
- }}
1597
- />
1598
- </div>
1599
- {/* Canvas Id */}
1600
- <div className="input-group mb-2">
1601
- <span className="input-group-text">
1602
- User Canvas Id
1603
- </span>
1604
- <input
1605
- type="text"
1606
- className="form-control"
1607
- aria-label="query for user canvas id"
1608
- value={advancedFilterState.userId}
1609
- placeholder="e.g. 104985"
1610
- onChange={(e) => {
1611
- const { value } = e.target;
1612
- // Only update if value contains only numbers
1613
- if (/^\d+$/.test(value)) {
1614
- advancedFilterState.userId = (
1615
- (e.target.value)
1616
- .trim()
1617
- );
1618
- }
1619
- dispatch({
1620
- type: ActionType.UpdateAdvancedFilterState,
1621
- advancedFilterState,
1622
- });
1623
- }}
1624
- />
1625
- </div>
1626
- {/* Role */}
1627
- <ButtonInputGroup label="Role">
1628
- <CheckboxButton
1629
- text="Students"
1630
- onChanged={(checked) => {
1631
- advancedFilterState.includeLearners = checked;
1632
- dispatch({
1633
- type: ActionType.UpdateAdvancedFilterState,
1634
- advancedFilterState,
1635
- });
1636
- }}
1637
- checked={advancedFilterState.includeLearners}
1638
- ariaLabel="show logs from students"
1639
- />
1640
- <CheckboxButton
1641
- text="Teaching Team Members"
1642
- onChanged={(checked) => {
1643
- advancedFilterState.includeTTMs = checked;
1644
- dispatch({
1645
- type: ActionType.UpdateAdvancedFilterState,
1646
- advancedFilterState,
1647
- });
1648
- }}
1649
- checked={advancedFilterState.includeTTMs}
1650
- ariaLabel="show logs from teaching team members"
1651
- />
1652
- <CheckboxButton
1653
- text="Admins"
1654
- onChanged={(checked) => {
1655
- advancedFilterState.includeAdmins = checked;
1656
- dispatch({
1657
- type: ActionType.UpdateAdvancedFilterState,
1658
- advancedFilterState,
1659
- });
1660
- }}
1661
- checked={advancedFilterState.includeAdmins}
1662
- ariaLabel="show logs from admins"
1663
- />
1664
- </ButtonInputGroup>
1665
- </TabBox>
1578
+ }
1579
+ dispatch({
1580
+ type: ActionType.UpdateAdvancedFilterState,
1581
+ advancedFilterState,
1582
+ });
1583
+ }}
1584
+ />
1585
+ </div>
1586
+ {/* Role */}
1587
+ <ButtonInputGroup label="Role">
1588
+ <CheckboxButton
1589
+ text="Students"
1590
+ onChanged={(checked) => {
1591
+ advancedFilterState.includeLearners = checked;
1592
+ dispatch({
1593
+ type: ActionType.UpdateAdvancedFilterState,
1594
+ advancedFilterState,
1595
+ });
1596
+ }}
1597
+ checked={advancedFilterState.includeLearners}
1598
+ ariaLabel="show logs from students"
1599
+ />
1600
+ <CheckboxButton
1601
+ text="Teaching Team Members"
1602
+ onChanged={(checked) => {
1603
+ advancedFilterState.includeTTMs = checked;
1604
+ dispatch({
1605
+ type: ActionType.UpdateAdvancedFilterState,
1606
+ advancedFilterState,
1607
+ });
1608
+ }}
1609
+ checked={advancedFilterState.includeTTMs}
1610
+ ariaLabel="show logs from teaching team members"
1611
+ />
1612
+ <CheckboxButton
1613
+ text="Admins"
1614
+ onChanged={(checked) => {
1615
+ advancedFilterState.includeAdmins = checked;
1616
+ dispatch({
1617
+ type: ActionType.UpdateAdvancedFilterState,
1618
+ advancedFilterState,
1619
+ });
1620
+ }}
1621
+ checked={advancedFilterState.includeAdmins}
1622
+ ariaLabel="show logs from admins"
1623
+ />
1624
+ </ButtonInputGroup>
1625
+ </TabBox>
1626
+
1627
+ {/* Course Info */}
1628
+ <TabBox title="Course">
1629
+ {/* Name */}
1630
+ <div className="input-group mb-2">
1631
+ <span className="input-group-text">
1632
+ Course Name
1633
+ </span>
1634
+ <input
1635
+ type="text"
1636
+ className="form-control"
1637
+ aria-label="query for course name"
1638
+ value={advancedFilterState.courseName}
1639
+ placeholder="e.g. GLC 200"
1640
+ onChange={(e) => {
1641
+ advancedFilterState.courseName = e.target.value;
1642
+ dispatch({
1643
+ type: ActionType.UpdateAdvancedFilterState,
1644
+ advancedFilterState,
1645
+ });
1646
+ }}
1647
+ />
1648
+ </div>
1649
+ {/* Canvas Id */}
1650
+ <div className="input-group mb-2">
1651
+ <span className="input-group-text">
1652
+ Course Canvas Id
1653
+ </span>
1654
+ <input
1655
+ type="text"
1656
+ className="form-control"
1657
+ aria-label="query for course canvas id"
1658
+ value={advancedFilterState.courseId}
1659
+ placeholder="e.g. 15948"
1660
+ onChange={(e) => {
1661
+ const { value } = e.target;
1662
+ // Only update if value contains only numbers
1663
+ if (/^\d+$/.test(value)) {
1664
+ advancedFilterState.courseId = (
1665
+ (e.target.value)
1666
+ .trim()
1667
+ );
1668
+ }
1669
+ dispatch({
1670
+ type: ActionType.UpdateAdvancedFilterState,
1671
+ advancedFilterState,
1672
+ });
1673
+ }}
1674
+ />
1675
+ </div>
1676
+ </TabBox>
1666
1677
 
1667
- {/* Course Info */}
1668
- <TabBox title="Course">
1669
- {/* Name */}
1678
+ {/* Device Info */}
1679
+ <TabBox title="Device">
1680
+ <ButtonInputGroup label="Device Type">
1681
+ <RadioButton
1682
+ text="All Devices"
1683
+ ariaLabel="show logs from all devices"
1684
+ selected={advancedFilterState.isMobile === undefined}
1685
+ onSelected={() => {
1686
+ advancedFilterState.isMobile = undefined;
1687
+ dispatch({
1688
+ type: ActionType.UpdateAdvancedFilterState,
1689
+ advancedFilterState,
1690
+ });
1691
+ }}
1692
+ />
1693
+ <RadioButton
1694
+ text="Mobile Only"
1695
+ ariaLabel="show logs from mobile devices"
1696
+ selected={advancedFilterState.isMobile === true}
1697
+ onSelected={() => {
1698
+ advancedFilterState.isMobile = true;
1699
+ dispatch({
1700
+ type: ActionType.UpdateAdvancedFilterState,
1701
+ advancedFilterState,
1702
+ });
1703
+ }}
1704
+ />
1705
+ <RadioButton
1706
+ text="Desktop Only"
1707
+ ariaLabel="show logs from desktop devices"
1708
+ selected={advancedFilterState.isMobile === false}
1709
+ onSelected={() => {
1710
+ advancedFilterState.isMobile = false;
1711
+ dispatch({
1712
+ type: ActionType.UpdateAdvancedFilterState,
1713
+ advancedFilterState,
1714
+ });
1715
+ }}
1716
+ noMarginOnRight
1717
+ />
1718
+ </ButtonInputGroup>
1719
+ </TabBox>
1720
+
1721
+ {/* Source */}
1722
+ <TabBox title="Source">
1723
+ <ButtonInputGroup label="Source Type">
1724
+ <RadioButton
1725
+ text="Both"
1726
+ ariaLabel="show logs from all sources"
1727
+ selected={advancedFilterState.source === undefined}
1728
+ onSelected={() => {
1729
+ advancedFilterState.source = undefined;
1730
+ dispatch({
1731
+ type: ActionType.UpdateAdvancedFilterState,
1732
+ advancedFilterState,
1733
+ });
1734
+ }}
1735
+ />
1736
+ <RadioButton
1737
+ text="Client Only"
1738
+ ariaLabel="show logs from client source"
1739
+ selected={advancedFilterState.source === LogSource.Client}
1740
+ onSelected={() => {
1741
+ advancedFilterState.source = LogSource.Client;
1742
+ dispatch({
1743
+ type: ActionType.UpdateAdvancedFilterState,
1744
+ advancedFilterState,
1745
+ });
1746
+ }}
1747
+ />
1748
+ <RadioButton
1749
+ text="Server Only"
1750
+ ariaLabel="show logs from server source"
1751
+ selected={advancedFilterState.source === LogSource.Server}
1752
+ onSelected={() => {
1753
+ advancedFilterState.source = LogSource.Server;
1754
+ dispatch({
1755
+ type: ActionType.UpdateAdvancedFilterState,
1756
+ advancedFilterState,
1757
+ });
1758
+ }}
1759
+ noMarginOnRight
1760
+ />
1761
+ </ButtonInputGroup>
1762
+
1763
+ {/* Server filters */}
1764
+ {advancedFilterState.source !== LogSource.Client && (
1765
+ <div className="mt-2">
1766
+ {/* Route path */}
1670
1767
  <div className="input-group mb-2">
1671
1768
  <span className="input-group-text">
1672
- Course Name
1769
+ Server Route Path
1673
1770
  </span>
1674
1771
  <input
1675
1772
  type="text"
1676
1773
  className="form-control"
1677
- aria-label="query for course name"
1678
- value={advancedFilterState.courseName}
1679
- placeholder="e.g. GLC 200"
1774
+ aria-label="query for server route path"
1775
+ value={advancedFilterState.routePath}
1776
+ placeholder="e.g. /api/ttm/courses/12345"
1680
1777
  onChange={(e) => {
1681
- advancedFilterState.courseName = e.target.value;
1778
+ advancedFilterState.courseName = (
1779
+ (e.target.value)
1780
+ .trim()
1781
+ );
1682
1782
  dispatch({
1683
1783
  type: ActionType.UpdateAdvancedFilterState,
1684
1784
  advancedFilterState,
@@ -1686,26 +1786,23 @@ const LogReviewer: React.FC<Props> = (props) => {
1686
1786
  }}
1687
1787
  />
1688
1788
  </div>
1689
- {/* Canvas Id */}
1789
+
1790
+ {/* Route template */}
1690
1791
  <div className="input-group mb-2">
1691
1792
  <span className="input-group-text">
1692
- Course Canvas Id
1793
+ Server Route Template
1693
1794
  </span>
1694
1795
  <input
1695
1796
  type="text"
1696
1797
  className="form-control"
1697
- aria-label="query for course canvas id"
1698
- value={advancedFilterState.courseId}
1699
- placeholder="e.g. 15948"
1798
+ aria-label="query for server route template"
1799
+ value={advancedFilterState.routeTemplate}
1800
+ placeholder="e.g. /api/ttm/courses/:courseId"
1700
1801
  onChange={(e) => {
1701
- const { value } = e.target;
1702
- // Only update if value contains only numbers
1703
- if (/^\d+$/.test(value)) {
1704
- advancedFilterState.courseId = (
1705
- (e.target.value)
1706
- .trim()
1707
- );
1708
- }
1802
+ advancedFilterState.courseName = (
1803
+ (e.target.value)
1804
+ .trim()
1805
+ );
1709
1806
  dispatch({
1710
1807
  type: ActionType.UpdateAdvancedFilterState,
1711
1808
  advancedFilterState,
@@ -1713,534 +1810,51 @@ const LogReviewer: React.FC<Props> = (props) => {
1713
1810
  }}
1714
1811
  />
1715
1812
  </div>
1716
- </TabBox>
1717
-
1718
- {/* Device Info */}
1719
- <TabBox title="Device">
1720
- <ButtonInputGroup label="Device Type">
1721
- <RadioButton
1722
- text="All Devices"
1723
- ariaLabel="show logs from all devices"
1724
- selected={advancedFilterState.isMobile === undefined}
1725
- onSelected={() => {
1726
- advancedFilterState.isMobile = undefined;
1727
- dispatch({
1728
- type: ActionType.UpdateAdvancedFilterState,
1729
- advancedFilterState,
1730
- });
1731
- }}
1732
- />
1733
- <RadioButton
1734
- text="Mobile Only"
1735
- ariaLabel="show logs from mobile devices"
1736
- selected={advancedFilterState.isMobile === true}
1737
- onSelected={() => {
1738
- advancedFilterState.isMobile = true;
1739
- dispatch({
1740
- type: ActionType.UpdateAdvancedFilterState,
1741
- advancedFilterState,
1742
- });
1743
- }}
1744
- />
1745
- <RadioButton
1746
- text="Desktop Only"
1747
- ariaLabel="show logs from desktop devices"
1748
- selected={advancedFilterState.isMobile === false}
1749
- onSelected={() => {
1750
- advancedFilterState.isMobile = false;
1751
- dispatch({
1752
- type: ActionType.UpdateAdvancedFilterState,
1753
- advancedFilterState,
1754
- });
1755
- }}
1756
- noMarginOnRight
1757
- />
1758
- </ButtonInputGroup>
1759
- </TabBox>
1760
-
1761
- {/* Source */}
1762
- <TabBox title="Source">
1763
- <ButtonInputGroup label="Source Type">
1764
- <RadioButton
1765
- text="Both"
1766
- ariaLabel="show logs from all sources"
1767
- selected={advancedFilterState.source === undefined}
1768
- onSelected={() => {
1769
- advancedFilterState.source = undefined;
1770
- dispatch({
1771
- type: ActionType.UpdateAdvancedFilterState,
1772
- advancedFilterState,
1773
- });
1774
- }}
1775
- />
1776
- <RadioButton
1777
- text="Client Only"
1778
- ariaLabel="show logs from client source"
1779
- selected={advancedFilterState.source === LogSource.Client}
1780
- onSelected={() => {
1781
- advancedFilterState.source = LogSource.Client;
1782
- dispatch({
1783
- type: ActionType.UpdateAdvancedFilterState,
1784
- advancedFilterState,
1785
- });
1786
- }}
1787
- />
1788
- <RadioButton
1789
- text="Server Only"
1790
- ariaLabel="show logs from server source"
1791
- selected={advancedFilterState.source === LogSource.Server}
1792
- onSelected={() => {
1793
- advancedFilterState.source = LogSource.Server;
1794
- dispatch({
1795
- type: ActionType.UpdateAdvancedFilterState,
1796
- advancedFilterState,
1797
- });
1798
- }}
1799
- noMarginOnRight
1800
- />
1801
- </ButtonInputGroup>
1802
-
1803
- {/* Server filters */}
1804
- {advancedFilterState.source !== LogSource.Client && (
1805
- <div className="mt-2">
1806
- {/* Route path */}
1807
- <div className="input-group mb-2">
1808
- <span className="input-group-text">
1809
- Server Route Path
1810
- </span>
1811
- <input
1812
- type="text"
1813
- className="form-control"
1814
- aria-label="query for server route path"
1815
- value={advancedFilterState.routePath}
1816
- placeholder="e.g. /api/ttm/courses/12345"
1817
- onChange={(e) => {
1818
- advancedFilterState.courseName = (
1819
- (e.target.value)
1820
- .trim()
1821
- );
1822
- dispatch({
1823
- type: ActionType.UpdateAdvancedFilterState,
1824
- advancedFilterState,
1825
- });
1826
- }}
1827
- />
1828
- </div>
1829
-
1830
- {/* Route template */}
1831
- <div className="input-group mb-2">
1832
- <span className="input-group-text">
1833
- Server Route Template
1834
- </span>
1835
- <input
1836
- type="text"
1837
- className="form-control"
1838
- aria-label="query for server route template"
1839
- value={advancedFilterState.routeTemplate}
1840
- placeholder="e.g. /api/ttm/courses/:courseId"
1841
- onChange={(e) => {
1842
- advancedFilterState.courseName = (
1843
- (e.target.value)
1844
- .trim()
1845
- );
1846
- dispatch({
1847
- type: ActionType.UpdateAdvancedFilterState,
1848
- advancedFilterState,
1849
- });
1850
- }}
1851
- />
1852
- </div>
1853
- </div>
1854
- )}
1855
- </TabBox>
1856
- </>
1857
- );
1858
- }
1813
+ </div>
1814
+ )}
1815
+ </TabBox>
1816
+ </>
1817
+ );
1859
1818
  }
1819
+ }
1860
1820
 
1861
- // Filters UI
1862
- const filters = (
1863
- <>
1864
- {filterToggles}
1865
- {filterDrawer && (
1866
- <Drawer customBackgroundColor="#eee">
1867
- {filterDrawer}
1868
- </Drawer>
1869
- )}
1870
- </>
1871
- );
1872
-
1873
- // Actually filter the logs
1874
- // > Perform filters
1875
- const logs: Log[] = [];
1876
- Object.keys(logMap).forEach((year) => {
1877
- Object.keys(logMap[year]).forEach((month) => {
1878
- logMap[year][month].forEach((log) => {
1879
- /* ----------- Date Filter ---------- */
1880
-
1881
- // Before start date
1882
- if (
1883
- // Previous year
1884
- log.year < dateFilterState.startDate.year
1885
- // Same year, earlier month
1886
- || (
1887
- (log.year === dateFilterState.startDate.year)
1888
- && (log.month < dateFilterState.startDate.month)
1889
- )
1890
- // Same year, same month, earlier day
1891
- || (
1892
- (log.year === dateFilterState.startDate.year)
1893
- && (log.month === dateFilterState.startDate.month)
1894
- && (log.day < dateFilterState.startDate.day)
1895
- )
1896
- ) {
1897
- return;
1898
- }
1899
-
1900
- // After end date
1901
- if (
1902
- // Later year
1903
- log.year > dateFilterState.endDate.year
1904
- // Same year, later month
1905
- || (
1906
- (log.year === dateFilterState.endDate.year)
1907
- && (log.month > dateFilterState.endDate.month)
1908
- )
1909
- // Same year, same month, later day
1910
- || (
1911
- (log.year === dateFilterState.endDate.year)
1912
- && (log.month === dateFilterState.endDate.month)
1913
- && (log.day > dateFilterState.endDate.day)
1914
- )
1915
- ) {
1916
- return;
1917
- }
1918
-
1919
- /* --------- Context Filter --------- */
1920
-
1921
- // Context doesn't match
1922
- if (
1923
- // Whole context is deselected
1924
- contextFilterState[log.context] === false
1925
- // None of the subcontexts are selected
1926
- || (
1927
- // Has subcontexts
1928
- typeof contextFilterState[log.context] !== 'boolean'
1929
- // None of the subcontexts are selected
1930
- && Object.values(contextFilterState[log.context] ?? {})
1931
- .every((isSelected) => {
1932
- return !isSelected;
1933
- })
1934
- )
1935
- ) {
1936
- return;
1937
- }
1938
-
1939
- // Subcontext doesn't match
1940
- if (
1941
- // Log context is not "uncategorized" (no point in further filters)
1942
- log.context !== LogBuiltInMetadata.Context.Uncategorized
1943
- // Log has a subcontext
1944
- && log.subcontext
1945
- // Context has subcontexts
1946
- && (
1947
- contextFilterState[log.context]
1948
- && contextFilterState[log.context] !== false
1949
- && contextFilterState[log.context] !== true
1950
- )
1951
- // Subcontext is not selected
1952
- && !(contextFilterState as any)[log.context][log.subcontext]
1953
- ) {
1954
- return;
1955
- }
1956
-
1957
- /* -------------- Tags -------------- */
1958
-
1959
- // No tags match
1960
- if (
1961
- // At least one tag is required
1962
- Object.values(tagFilterState)
1963
- .filter((isSelected) => {
1964
- return isSelected;
1965
- })
1966
- .length > 0
1967
- // No tags match
1968
- && log.tags.every((tag) => {
1969
- return !tagFilterState[tag];
1970
- })
1971
- ) {
1972
- return;
1973
- }
1974
-
1975
- /* ------- Actions and Errors ------- */
1976
-
1977
- // Log type doesn't match
1978
- if (
1979
- // Filter won't allow all types
1980
- actionErrorFilterState.type !== undefined
1981
- // Log type doesn't match
1982
- && actionErrorFilterState.type !== log.type
1983
- ) {
1984
- return;
1985
- }
1986
-
1987
- // Filter errors
1988
- if (log.type === LogType.Error) {
1989
- // Message doesn't match
1990
- if (
1991
- // Message exists
1992
- log.errorMessage
1993
- // Message filter exists
1994
- && actionErrorFilterState.errorMessage.trim().length > 0
1995
- // Message doesn't match
1996
- && log.errorMessage.toLowerCase().includes(
1997
- actionErrorFilterState.errorMessage.trim().toLowerCase(),
1998
- )
1999
- ) {
2000
- return;
2001
- }
2002
-
2003
- // Code doesn't match
2004
- if (
2005
- // Code exists
2006
- log.errorCode
2007
- // Code filter exists
2008
- && actionErrorFilterState.errorCode.trim().length > 0
2009
- // Code doesn't match
2010
- && log.errorCode.toUpperCase().includes(
2011
- actionErrorFilterState.errorCode.trim().toUpperCase(),
2012
- )
2013
- ) {
2014
- return;
2015
- }
2016
- }
2017
-
2018
- // Filter actions
2019
- if (log.type === LogType.Action) {
2020
- // Target isn't selected
2021
- if (
2022
- // Target exists
2023
- log.target
2024
- // Target isn't selected
2025
- && !actionErrorFilterState.target[log.target]
2026
- ) {
2027
- return;
2028
- }
2029
-
2030
- // Action
2031
- if (
2032
- // Action exists
2033
- log.action
2034
- // Action isn't selected
2035
- && !actionErrorFilterState.action[log.action]
2036
- ) {
2037
- return;
2038
- }
2039
- }
2040
-
2041
- /* --------- Advanced Filter -------- */
2042
-
2043
- // First name doesn't match
2044
- if (
2045
- // First name exists
2046
- log.userFirstName
2047
- // First name query doesn't match
2048
- && !log.userFirstName.toLowerCase().includes(
2049
- advancedFilterState.userFirstName.toLowerCase().trim(),
2050
- )
2051
- ) {
2052
- return;
2053
- }
2054
-
2055
- // Last name doesn't match
2056
- if (
2057
- // Last name exists
2058
- log.userLastName
2059
- // Last name query doesn't match
2060
- && !log.userLastName.toLowerCase().includes(
2061
- advancedFilterState.userLastName.toLowerCase().trim(),
2062
- )
2063
- ) {
2064
- return;
2065
- }
2066
-
2067
- // Email doesn't match
2068
- if (
2069
- // Email exists
2070
- log.userEmail
2071
- // Email query doesn't match
2072
- && !log.userEmail.toLowerCase().includes(
2073
- advancedFilterState.userEmail.toLowerCase().trim(),
2074
- )
2075
- ) {
2076
- return;
2077
- }
2078
-
2079
- // User id doesn't match
2080
- if (
2081
- // User id exists
2082
- log.userId
2083
- // User id doesn't match
2084
- && !String(log.userId).includes(
2085
- advancedFilterState.userId.trim(),
2086
- )
2087
- ) {
2088
- return;
2089
- }
2090
-
2091
- // Learner not allowed
2092
- if (
2093
- // User is a learner
2094
- log.isLearner
2095
- // Learners aren't included
2096
- && !advancedFilterState.includeLearners
2097
- ) {
2098
- return;
2099
- }
2100
-
2101
- // TTM not allowed
2102
- if (
2103
- // User is a ttm
2104
- log.isTTM
2105
- // TTMs aren't included
2106
- && !advancedFilterState.includeTTMs
2107
- ) {
2108
- return;
2109
- }
2110
-
2111
- // Admin not allowed
2112
- if (
2113
- // User is an admin
2114
- log.isAdmin
2115
- // Admins aren't included
2116
- && !advancedFilterState.includeAdmins
2117
- ) {
2118
- return;
2119
- }
2120
-
2121
- // Course Id doesn't match
2122
- if (
2123
- // Course Id exists
2124
- log.courseId
2125
- // Course Id doesn't match
2126
- && !String(log.courseId).includes(
2127
- advancedFilterState.courseId.trim(),
2128
- )
2129
- ) {
2130
- return;
2131
- }
2132
-
2133
- // Course name doesn't match
2134
- if (
2135
- // Course name exists
2136
- log.courseName
2137
- // Course name doesn't match
2138
- && !String(log.courseName).includes(
2139
- advancedFilterState.courseName.trim(),
2140
- )
2141
- ) {
2142
- return;
2143
- }
2144
-
2145
- // Mobile filter doesn't match
2146
- if (
2147
- // Mobile filter exists
2148
- advancedFilterState.isMobile !== undefined
2149
- // Device info exists
2150
- && log.device
2151
- // Mobile filter doesn't match
2152
- && (advancedFilterState.isMobile !== log.device.isMobile)
2153
- ) {
2154
- return;
2155
- }
2156
-
2157
- // Log source doesn't match
2158
- if (
2159
- // Source filter exists
2160
- advancedFilterState.source !== undefined
2161
- // Source info exists
2162
- && log.source
2163
- // Source filter doesn't match
2164
- && (advancedFilterState.source !== log.source)
2165
- ) {
2166
- return;
2167
- }
2168
-
2169
- // Route path doesn't match (Only for server source)
2170
- if (
2171
- // Source is server
2172
- (log.source === LogSource.Server)
2173
- // Route path is being filtered
2174
- && (advancedFilterState.routePath.trim().length)
2175
- // Route path doesn't match
2176
- && !(log.routePath.includes(advancedFilterState.routePath.trim()))
2177
- ) {
2178
- return;
2179
- }
2180
-
2181
- // Route template doesn't match (Only for server source)
2182
- if (
2183
- // Source is server
2184
- (log.source === LogSource.Server)
2185
- // Route template is being filtered
2186
- && (advancedFilterState.routeTemplate.trim().length)
2187
- // Route template doesn't match
2188
- && !(log.routeTemplate.includes(advancedFilterState.routeTemplate.trim()))
2189
- ) {
2190
- return;
2191
- }
2192
-
2193
- /* -------------- Done -------------- */
2194
-
2195
- // Made it past all filters. Add to the list
2196
- logs.push(log);
2197
- });
2198
- });
2199
- });
2200
-
2201
- /*----------------------------------------*/
2202
- /* ---------------- Data ---------------- */
2203
- /*----------------------------------------*/
1821
+ // Filters UI
1822
+ const filters = (
1823
+ <>
1824
+ {filterToggles}
1825
+ {filterDrawer && (
1826
+ <Drawer customBackgroundColor="#eee">
1827
+ {filterDrawer}
1828
+ </Drawer>
1829
+ )}
1830
+ </>
1831
+ );
2204
1832
 
2205
- // Nothing to show notice
2206
- const noLogsNotice = (
2207
- logs.length === 0
2208
- ? (
1833
+ // Main body
1834
+ body = (
1835
+ <>
1836
+ {filters}
1837
+ <div className="mt-2">
1838
+ <IntelliTable
1839
+ title="Matching Logs:"
1840
+ csvName={`Logs from ${getHumanReadableDate()}`}
1841
+ id="logs"
1842
+ data={logs}
1843
+ columns={columns}
1844
+ />
1845
+ {logs.length === 0 && (
2209
1846
  <div className="alert alert-warning text-center mt-2">
2210
- <h4 className="m-1">
2211
- No Logs to Show
2212
- </h4>
1847
+ <h4 className="m-1">No Logs to Show</h4>
2213
1848
  <div>
2214
1849
  Either your filters are too strict or no matching logs have been
2215
1850
  created yet.
2216
1851
  </div>
2217
1852
  </div>
2218
- )
2219
- : undefined
2220
- );
2221
-
2222
- // Create intelliTable
2223
- const dataTable = (
2224
- <IntelliTable
2225
- title="Matching Logs:"
2226
- csvName={`Logs from ${getHumanReadableDate()}`}
2227
- id="logs"
2228
- data={logs}
2229
- columns={columns}
2230
- />
2231
- );
2232
-
2233
- // Main body
2234
- body = (
2235
- <>
2236
- {filters}
2237
- <div className="mt-2">
2238
- {dataTable}
2239
- {noLogsNotice}
2240
- </div>
2241
- </>
2242
- );
2243
- }
1853
+ )}
1854
+ {paginationControls}
1855
+ </div>
1856
+ </>
1857
+ );
2244
1858
 
2245
1859
  /* ---------- Wrap in Modal --------- */
2246
1860
 
@@ -2252,9 +1866,7 @@ const LogReviewer: React.FC<Props> = (props) => {
2252
1866
  <div className="LogReviewer-inner-container">
2253
1867
  <div className="LogReviewer-header">
2254
1868
  <div className="LogReviewer-header-title">
2255
- <h3 className="text-center m-0">
2256
- Log Review Dashboard
2257
- </h3>
1869
+ <h3 className="text-center m-0">Log Review Dashboard</h3>
2258
1870
  </div>
2259
1871
  <div style={{ width: 0 }}>
2260
1872
  <button
@@ -2263,15 +1875,11 @@ const LogReviewer: React.FC<Props> = (props) => {
2263
1875
  aria-label="close log reviewer panel"
2264
1876
  onClick={onClose}
2265
1877
  >
2266
- <FontAwesomeIcon
2267
- icon={faTimes}
2268
- />
1878
+ <FontAwesomeIcon icon={faTimes} />
2269
1879
  </button>
2270
1880
  </div>
2271
1881
  </div>
2272
- <div className="LogReviewer-contents">
2273
- {body}
2274
- </div>
1882
+ <div className="LogReviewer-contents">{body}</div>
2275
1883
  </div>
2276
1884
  </div>
2277
1885
  );