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

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.
@@ -10,6 +10,8 @@ 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,
@@ -63,13 +65,6 @@ type Props = {
63
65
  onClose: () => void,
64
66
  };
65
67
 
66
- // Map of loaded logs (year => month => Log[])
67
- type LogMap = {
68
- [k: string]: {
69
- [k: string]: Log[]
70
- }
71
- };
72
-
73
68
  // Triple of year/month/day
74
69
  type DateTriple = {
75
70
  // Full year
@@ -532,8 +527,8 @@ type State = {
532
527
  /* -------------- Logs -------------- */
533
528
  // True if currently loading
534
529
  loading: boolean,
535
- // Loaded logs (year => month => Log[])
536
- logMap: LogMap,
530
+ // Loaded logs
531
+ logs: Log[],
537
532
  /* ------------- Filters ------------ */
538
533
  // Current expanded filter drawer
539
534
  expandedFilterDrawer: FilterDrawer | undefined,
@@ -547,6 +542,10 @@ type State = {
547
542
  actionErrorFilterState: ActionErrorFilterState,
548
543
  // State of the advanced filter
549
544
  advancedFilterState: AdvancedFilterState,
545
+ // Current page number
546
+ pageNumber: number,
547
+ // If true, there is another page to load
548
+ hasAnotherPage: boolean,
550
549
  };
551
550
 
552
551
  /* ------------- Actions ------------ */
@@ -555,7 +554,7 @@ type State = {
555
554
  enum ActionType {
556
555
  // Show the loading bar
557
556
  StartLoading = 'start-loading',
558
- // Finish loading one or more months of logs
557
+ // Finish loading logs
559
558
  FinishLoading = 'finish-loading',
560
559
  // Reset filters to initial values
561
560
  ResetFilters = 'reset-filters',
@@ -573,6 +572,12 @@ enum ActionType {
573
572
  UpdateActionErrorFilterState = 'update-action-error-filter-state',
574
573
  // Update the advanced filter state
575
574
  UpdateAdvancedFilterState = 'update-advanced-filter-state',
575
+ // Increment the page number
576
+ IncrementPageNumber = 'increment-page-number',
577
+ // Decrement the page number
578
+ DecrementPageNumber = 'decrement-page-number',
579
+ // Set has another page
580
+ SetHasAnotherPage = 'set-has-another-page',
576
581
  }
577
582
 
578
583
  // Action definitions
@@ -580,8 +585,8 @@ type Action = (
580
585
  | {
581
586
  // Action type
582
587
  type: ActionType.FinishLoading,
583
- // Updated logMap
584
- logMap: LogMap,
588
+ // Updated logs
589
+ logs: Log[],
585
590
  }
586
591
  | {
587
592
  // Action type
@@ -629,6 +634,16 @@ type Action = (
629
634
  // New advanced filter state
630
635
  advancedFilterState: AdvancedFilterState,
631
636
  }
637
+ | {
638
+ type: ActionType.IncrementPageNumber,
639
+ }
640
+ | {
641
+ type: ActionType.DecrementPageNumber,
642
+ }
643
+ | {
644
+ type: ActionType.SetHasAnotherPage,
645
+ hasAnotherPage: boolean,
646
+ }
632
647
  | {
633
648
  // Action type
634
649
  type: (
@@ -656,7 +671,7 @@ const reducer = (state: State, action: Action): State => {
656
671
  return {
657
672
  ...state,
658
673
  loading: false,
659
- logMap: action.logMap,
674
+ logs: action.logs,
660
675
  };
661
676
  }
662
677
  case ActionType.ToggleFilterDrawer: {
@@ -715,6 +730,18 @@ const reducer = (state: State, action: Action): State => {
715
730
  advancedFilterState: action.advancedFilterState,
716
731
  };
717
732
  }
733
+ case ActionType.IncrementPageNumber: {
734
+ return {
735
+ ...state,
736
+ pageNumber: state.pageNumber + 1,
737
+ };
738
+ }
739
+ case ActionType.DecrementPageNumber: {
740
+ return {
741
+ ...state,
742
+ pageNumber: state.pageNumber - 1,
743
+ };
744
+ }
718
745
  default: {
719
746
  return state;
720
747
  }
@@ -850,13 +877,15 @@ const LogReviewer: React.FC<Props> = (props) => {
850
877
  // Initial state
851
878
  const initialState: State = {
852
879
  loading: true,
853
- logMap: {},
880
+ logs: [],
854
881
  expandedFilterDrawer: undefined,
855
882
  dateFilterState: initDateFilterState,
856
883
  contextFilterState: initContextFilterState,
857
884
  tagFilterState: initTagFilterState,
858
885
  actionErrorFilterState: initActionErrorFilterState,
859
886
  advancedFilterState: initAdvancedFilterState,
887
+ pageNumber: 1,
888
+ hasAnotherPage: false,
860
889
  };
861
890
 
862
891
  // Initialize state
@@ -865,13 +894,15 @@ const LogReviewer: React.FC<Props> = (props) => {
865
894
  // Destructure common state
866
895
  const {
867
896
  loading,
868
- logMap,
897
+ logs,
869
898
  expandedFilterDrawer,
870
899
  dateFilterState,
871
900
  contextFilterState,
872
901
  tagFilterState,
873
902
  actionErrorFilterState,
874
903
  advancedFilterState,
904
+ pageNumber,
905
+ hasAnotherPage,
875
906
  } = state;
876
907
 
877
908
  /*------------------------------------------------------------------------*/
@@ -879,106 +910,48 @@ const LogReviewer: React.FC<Props> = (props) => {
879
910
  /*------------------------------------------------------------------------*/
880
911
 
881
912
  /**
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
913
+ * Fetch logs from the server based on current filters
932
914
  */
933
- const handleDateRangeUpdated = async (
934
- newDateFilterState: DateFilterState,
935
- ) => {
936
- // Update state
937
- dispatch({
938
- type: ActionType.UpdateDateFilterState,
939
- dateFilterState: newDateFilterState,
940
- });
915
+ const fetchLogs = async () => {
916
+ dispatch({ type: ActionType.StartLoading });
941
917
 
942
- // Check which year/month combos we need to load
943
- const toLoad = listMonthsToLoad(newDateFilterState);
918
+ try {
919
+ // Prepare filter parameters
920
+ const filters = {
921
+ startDate: dateFilterState.startDate,
922
+ endDate: dateFilterState.endDate,
923
+ contextFilterState,
924
+ tagFilterState,
925
+ actionErrorFilterState,
926
+ advancedFilterState,
927
+ };
944
928
 
945
- // If nothing to load, finished
946
- if (toLoad.length === 0) {
947
- return;
948
- }
929
+ // Send filters to the server
930
+ let fetchedLogs: Log[] = [];
949
931
 
950
- // Start loading
951
- dispatch({
952
- type: ActionType.StartLoading,
953
- });
932
+ const response = await visitServerEndpoint({
933
+ path: `${LOG_REVIEW_ROUTE_PATH_PREFIX}/logs`,
934
+ method: 'GET',
935
+ params: {
936
+ pageNumber,
937
+ filters,
938
+ },
939
+ });
954
940
 
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
- });
941
+ fetchedLogs = fetchedLogs.concat(response.items);
942
+ dispatch({
943
+ type: ActionType.SetHasAnotherPage,
944
+ hasAnotherPage: response.hasAnotherPage,
945
+ });
966
946
 
967
- // Add to map
968
- if (!logMap[year]) {
969
- logMap[year] = {};
970
- }
971
- logMap[year][month] = logs;
972
- }
947
+ // Update logs in state
948
+ dispatch({
949
+ type: ActionType.FinishLoading,
950
+ logs: fetchedLogs,
951
+ });
973
952
  } catch (err) {
974
953
  return showFatalError(err);
975
954
  }
976
-
977
- // Finish loading
978
- dispatch({
979
- type: ActionType.FinishLoading,
980
- logMap,
981
- });
982
955
  };
983
956
 
984
957
  /*------------------------------------------------------------------------*/
@@ -986,21 +959,21 @@ const LogReviewer: React.FC<Props> = (props) => {
986
959
  /*------------------------------------------------------------------------*/
987
960
 
988
961
  /**
989
- * Mount
990
- * @author Gabe Abrams
962
+ * Fetch logs whenever filters change
991
963
  */
992
- useEffect(
993
- () => {
994
- // Perform initial load
995
- handleDateRangeUpdated(dateFilterState);
996
- },
997
- [],
998
- );
964
+ useEffect(() => {
965
+ fetchLogs();
966
+ }, [
967
+ dateFilterState,
968
+ contextFilterState,
969
+ tagFilterState,
970
+ actionErrorFilterState,
971
+ advancedFilterState,
972
+ ]);
999
973
 
1000
974
  /*------------------------------------------------------------------------*/
1001
975
  /* ------------------------------- Render ------------------------------- */
1002
976
  /*------------------------------------------------------------------------*/
1003
-
1004
977
  /*----------------------------------------*/
1005
978
  /* --------------- Main UI -------------- */
1006
979
  /*----------------------------------------*/
@@ -1020,176 +993,218 @@ const LogReviewer: React.FC<Props> = (props) => {
1020
993
 
1021
994
  /* ------------ Review UI ----------- */
1022
995
 
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={() => {
996
+ /*----------------------------------------*/
997
+ /* ------------ Pagination -------------- */
998
+ /*----------------------------------------*/
999
+
1000
+ const paginationControls = logs.length > 0 && (
1001
+ <div className="text-center mt-3">
1002
+ <button
1003
+ type="button"
1004
+ className="btn btn-secondary me-2"
1005
+ disabled={pageNumber <= 1}
1006
+ onClick={() => {
1007
+ dispatch({
1008
+ type: ActionType.DecrementPageNumber,
1009
+ });
1010
+ }}
1011
+ >
1012
+ <FontAwesomeIcon icon={faArrowLeft} className="me-2" />
1013
+ Previous Page
1014
+ </button>
1015
+ <span className="mx-3">
1016
+ Page
1017
+ {' '}
1018
+ {pageNumber}
1019
+ </span>
1020
+ <button
1021
+ type="button"
1022
+ className="btn btn-secondary ms-2"
1023
+ disabled={!hasAnotherPage}
1024
+ onClick={() => {
1025
+ dispatch({
1026
+ type: ActionType.IncrementPageNumber,
1027
+ });
1028
+ }}
1029
+ >
1030
+ Next Page
1031
+ <FontAwesomeIcon icon={faArrowRight} className="ms-2" />
1032
+ </button>
1033
+ </div>
1034
+ );
1035
+
1036
+ /*----------------------------------------*/
1037
+ /* --------------- Filters -------------- */
1038
+ /*----------------------------------------*/
1039
+
1040
+ // Filter toggle
1041
+ const filterToggles = (
1042
+ <div className="LogReviewer-filter-toggles">
1043
+ <h3 className="m-0">
1044
+ Filters:
1045
+ </h3>
1046
+ <div className="LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0">
1047
+ {/* Date */}
1048
+ <button
1049
+ type="button"
1050
+ id="LogReviewer-toggle-date-filter-drawer"
1051
+ className={`btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1052
+ aria-label="toggle date filter drawer"
1053
+ onClick={() => {
1054
+ dispatch({
1055
+ type: ActionType.ToggleFilterDrawer,
1056
+ filterDrawer: FilterDrawer.Date,
1057
+ });
1058
+ }}
1059
+ >
1060
+ <FontAwesomeIcon
1061
+ icon={faCalendar}
1062
+ className="me-2"
1063
+ />
1064
+ Date
1065
+ </button>
1066
+ {/* Context */}
1067
+ <button
1068
+ type="button"
1069
+ id="LogReviewer-toggle-context-filter-drawer"
1070
+ className={`btn btn-${FilterDrawer.Context === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1071
+ aria-label="toggle context filter drawer"
1072
+ onClick={() => {
1073
+ dispatch({
1074
+ type: ActionType.ToggleFilterDrawer,
1075
+ filterDrawer: FilterDrawer.Context,
1076
+ });
1077
+ }}
1078
+ >
1079
+ <FontAwesomeIcon
1080
+ icon={faCircle}
1081
+ className="me-2"
1082
+ />
1083
+ Context
1084
+ </button>
1085
+ {/* Tag */}
1086
+ {/* Skip if no tags are used */}
1087
+ {(LogMetadata.Tag && Object.keys(LogMetadata.Tag).length > 0) && (
1088
+ <button
1089
+ type="button"
1090
+ id="LogReviewer-toggle-tag-filter-drawer"
1091
+ className={`btn btn-${FilterDrawer.Tag === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1092
+ aria-label="toggle tag filter drawer"
1093
+ onClick={() => {
1094
+ dispatch({
1095
+ type: ActionType.ToggleFilterDrawer,
1096
+ filterDrawer: FilterDrawer.Tag,
1097
+ });
1098
+ }}
1099
+ >
1100
+ <FontAwesomeIcon
1101
+ icon={faTag}
1102
+ className="me-2"
1103
+ />
1104
+ Tag
1105
+ </button>
1106
+ )}
1107
+ {/* Action */}
1108
+ <button
1109
+ type="button"
1110
+ id="LogReviewer-toggle-action-filter-drawer"
1111
+ className={`btn btn-${FilterDrawer.Action === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1112
+ aria-label="toggle action and error filter drawer"
1113
+ onClick={() => {
1114
+ dispatch({
1115
+ type: ActionType.ToggleFilterDrawer,
1116
+ filterDrawer: FilterDrawer.Action,
1117
+ });
1118
+ }}
1119
+ >
1120
+ <FontAwesomeIcon
1121
+ icon={faHammer}
1122
+ className="me-2"
1123
+ />
1124
+ Action
1125
+ </button>
1126
+ {/* Advanced */}
1127
+ <button
1128
+ type="button"
1129
+ id="LogReviewer-toggle-advanced-filter-drawer"
1130
+ className={`btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
1131
+ aria-label="toggle advanced filter drawer"
1132
+ onClick={() => {
1133
+ dispatch({
1134
+ type: ActionType.ToggleFilterDrawer,
1135
+ filterDrawer: FilterDrawer.Advanced,
1136
+ });
1137
+ }}
1138
+ >
1139
+ <FontAwesomeIcon
1140
+ icon={faList}
1141
+ className="me-2"
1142
+ />
1143
+ Advanced
1144
+ </button>
1145
+ {/* Reset */}
1146
+ <button
1147
+ type="button"
1148
+ id="LogReviewer-reset-filters-button"
1149
+ className="btn btn-light"
1150
+ aria-label="reset filters"
1151
+ onClick={() => {
1152
+ dispatch({
1153
+ type: ActionType.ResetFilters,
1154
+ initActionErrorFilterState,
1155
+ initAdvancedFilterState,
1156
+ initContextFilterState,
1157
+ initDateFilterState,
1158
+ initTagFilterState,
1159
+ });
1160
+ }}
1161
+ >
1162
+ <FontAwesomeIcon
1163
+ icon={faTimes}
1164
+ />
1165
+ {' '}
1166
+ Reset
1167
+ </button>
1168
+ </div>
1169
+ </div>
1170
+ );
1171
+
1172
+ // Filter drawer
1173
+ let filterDrawer: React.ReactNode;
1174
+ if (expandedFilterDrawer) {
1175
+ if (expandedFilterDrawer === FilterDrawer.Date) {
1176
+ filterDrawer = (
1177
+ <TabBox title="Date">
1178
+ <SimpleDateChooser
1179
+ ariaLabel="filter start date"
1180
+ name="filter-start-date"
1181
+ year={dateFilterState.startDate.year}
1182
+ month={dateFilterState.startDate.month}
1183
+ day={dateFilterState.startDate.day}
1184
+ chooseFromPast
1185
+ numMonthsToShow={36}
1186
+ onChange={(month, day, year) => {
1187
+ dateFilterState.startDate = { month, day, year };
1140
1188
  dispatch({
1141
- type: ActionType.ResetFilters,
1142
- initActionErrorFilterState,
1143
- initAdvancedFilterState,
1144
- initContextFilterState,
1145
- initDateFilterState,
1146
- initTagFilterState,
1189
+ type: ActionType.UpdateDateFilterState,
1190
+ dateFilterState,
1147
1191
  });
1148
1192
  }}
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
1193
+ />
1194
+ {' '}
1195
+ to
1196
+ {' '}
1197
+ <SimpleDateChooser
1198
+ ariaLabel="filter end date"
1199
+ name="filter-end-date"
1200
+ year={dateFilterState.endDate.year}
1201
+ month={dateFilterState.endDate.month}
1202
+ day={dateFilterState.endDate.day}
1203
+ chooseFromPast
1204
+ numMonthsToShow={12}
1205
+ onChange={(month, day, year) => {
1206
+ if (
1207
+ year < dateFilterState.startDate.year
1193
1208
  || (
1194
1209
  year === dateFilterState.startDate.year
1195
1210
  && month < dateFilterState.startDate.month
@@ -1199,137 +1214,140 @@ const LogReviewer: React.FC<Props> = (props) => {
1199
1214
  && month === dateFilterState.startDate.month
1200
1215
  && day < dateFilterState.startDate.day
1201
1216
  )
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);
1217
+ ) {
1218
+ return alert(
1219
+ 'Invalid Start Date',
1220
+ 'The start date cannot be before the end date.',
1221
+ );
1243
1222
  }
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
- );
1223
+ dateFilterState.endDate = { month, day, year };
1224
+ dispatch({
1225
+ type: ActionType.UpdateDateFilterState,
1226
+ dateFilterState,
1227
+ });
1228
+ }}
1229
+ />
1230
+ </TabBox>
1231
+ );
1232
+ } else if (expandedFilterDrawer === FilterDrawer.Context) {
1233
+ // Create item picker items
1234
+ const builtInPickableItem: PickableItem = {
1235
+ id: 'built-in-contexts',
1236
+ name: 'Auto-logged',
1237
+ isGroup: true,
1238
+ children: [],
1239
+ };
1240
+ const pickableItems: PickableItem[] = [];
1241
+ Object.keys(contextMap)
1242
+ .forEach((context) => {
1243
+ const value = contextMap[context];
1244
+ if (typeof value === 'string') {
1245
+ // No subcategories
1264
1246
  const item: PickableItem = {
1265
1247
  id: context,
1266
1248
  name: genHumanReadableName(context),
1267
- isGroup: true,
1268
- children,
1249
+ isGroup: false,
1250
+ checked: !!contextFilterState[context],
1269
1251
  };
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
1252
+
1253
+ // Add built-in items to its own folder
1254
+ const isBuiltIn = context in LogBuiltInMetadata.Context;
1255
+ if (isBuiltIn) {
1256
+ // Add to built-in pickable item
1257
+ builtInPickableItem.children.push(item);
1258
+ } else {
1259
+ // Add to pickable items list
1260
+ pickableItems.push(item);
1261
+ }
1262
+ return;
1263
+ }
1264
+
1265
+ // Has subcategories
1266
+ const children: PickableItem[] = (
1267
+ Object.keys(value)
1268
+ // Remove parent name
1269
+ .filter((subcontext) => {
1270
+ return subcontext !== '_';
1271
+ })
1272
+ // Create child pickable items
1273
+ .map((subcontext) => {
1274
+ return {
1275
+ id: subcontext,
1276
+ name: genHumanReadableName(subcontext),
1277
+ isGroup: false,
1278
+ checked: (contextFilterState[context] as any)[subcontext],
1279
+ };
1280
+ })
1281
+ );
1282
+ const item: PickableItem = {
1283
+ id: context,
1284
+ name: genHumanReadableName(context),
1285
+ isGroup: true,
1286
+ children,
1287
+ };
1288
+ pickableItems.push(item);
1289
+ });
1290
+ // Add built-in contexts to end ofl ist
1291
+ pickableItems.push(builtInPickableItem);
1292
+
1293
+ // Create filter UI
1294
+ filterDrawer = (
1295
+ <ItemPicker
1296
+ title="Context"
1297
+ items={pickableItems}
1298
+ onChanged={(updatedItems) => {
1299
+ // Update our state
1300
+ updatedItems.forEach((pickableItem) => {
1301
+ if (pickableItem.isGroup) {
1302
+ // Has subcontexts
1303
+
1304
+ if (pickableItem.id === 'built-in-contexts') {
1305
+ // Built-in
1306
+
1307
+ // Treat as if these were top-level contexts
1308
+ pickableItem.children.forEach((subcontextItem) => {
1309
+ contextFilterState[subcontextItem.id] = (
1310
+ 'checked' in subcontextItem
1293
1311
  && 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
- }
1312
+ );
1313
+ });
1308
1314
  } else {
1309
- // No subcontexts
1310
- (contextFilterState as any)[pickableItem.id] = (
1311
- pickableItem.checked
1312
- );
1315
+ // Not built-in
1316
+ pickableItem.children.forEach((subcontextItem) => {
1317
+ if (!subcontextItem.isGroup) {
1318
+ (
1319
+ contextFilterState[pickableItem.id] as { [k: string]: boolean }
1320
+ )[subcontextItem.id] = (
1321
+ subcontextItem.checked
1322
+ );
1323
+ }
1324
+ });
1313
1325
  }
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
- {
1326
+ } else {
1327
+ // No subcontexts
1328
+ (contextFilterState as any)[pickableItem.id] = (
1329
+ pickableItem.checked
1330
+ );
1331
+ }
1332
+ });
1333
+ dispatch({
1334
+ type: ActionType.UpdateContextFilterState,
1335
+ contextFilterState,
1336
+ });
1337
+ }}
1338
+ />
1339
+ );
1340
+ } else if (expandedFilterDrawer === FilterDrawer.Tag) {
1341
+ // Create filter UI
1342
+ filterDrawer = (
1343
+ <TabBox title="Tags">
1344
+ <div>
1345
+ If any tags are selected, logs must contain at least one
1346
+ (but not necessarily all) of the
1347
+ selected tags.
1348
+ </div>
1349
+ <div className="d-flex gap-1 flex-wrap">
1350
+ {
1333
1351
  Object.keys(LogMetadata.Tag ?? {})
1334
1352
  .map((tag) => {
1335
1353
  const description = genHumanReadableName(tag);
@@ -1351,58 +1369,58 @@ const LogReviewer: React.FC<Props> = (props) => {
1351
1369
  );
1352
1370
  })
1353
1371
  }
1354
- </div>
1372
+ </div>
1373
+ </TabBox>
1374
+ );
1375
+ } else if (expandedFilterDrawer === FilterDrawer.Action) {
1376
+ // Create filter UI
1377
+ filterDrawer = (
1378
+ <>
1379
+ {/* Log Type */}
1380
+ <TabBox title="Log Type">
1381
+ <RadioButton
1382
+ id="LogReviewer-type-all"
1383
+ text="All Logs"
1384
+ onSelected={() => {
1385
+ actionErrorFilterState.type = undefined;
1386
+ dispatch({
1387
+ type: ActionType.UpdateActionErrorFilterState,
1388
+ actionErrorFilterState,
1389
+ });
1390
+ }}
1391
+ ariaLabel="show logs of all types"
1392
+ selected={actionErrorFilterState.type === undefined}
1393
+ />
1394
+ <RadioButton
1395
+ id="LogReviewer-type-action-only"
1396
+ text="Action Logs Only"
1397
+ onSelected={() => {
1398
+ actionErrorFilterState.type = LogType.Action;
1399
+ dispatch({
1400
+ type: ActionType.UpdateActionErrorFilterState,
1401
+ actionErrorFilterState,
1402
+ });
1403
+ }}
1404
+ ariaLabel="only show action logs"
1405
+ selected={actionErrorFilterState.type === LogType.Action}
1406
+ />
1407
+ <RadioButton
1408
+ id="LogReviewer-type-error-only"
1409
+ text="Action Error Only"
1410
+ onSelected={() => {
1411
+ actionErrorFilterState.type = LogType.Error;
1412
+ dispatch({
1413
+ type: ActionType.UpdateActionErrorFilterState,
1414
+ actionErrorFilterState,
1415
+ });
1416
+ }}
1417
+ ariaLabel="only show error logs"
1418
+ selected={actionErrorFilterState.type === LogType.Error}
1419
+ noMarginOnRight
1420
+ />
1355
1421
  </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
- {
1422
+ {/* Actions */}
1423
+ {
1406
1424
  (
1407
1425
  actionErrorFilterState.type === undefined
1408
1426
  || actionErrorFilterState.type === LogType.Action
@@ -1471,8 +1489,8 @@ const LogReviewer: React.FC<Props> = (props) => {
1471
1489
  </TabBox>
1472
1490
  )
1473
1491
  }
1474
- {/* Errors */}
1475
- {
1492
+ {/* Errors */}
1493
+ {
1476
1494
  (
1477
1495
  actionErrorFilterState.type === undefined
1478
1496
  || actionErrorFilterState.type === LogType.Error
@@ -1525,160 +1543,300 @@ const LogReviewer: React.FC<Props> = (props) => {
1525
1543
  </TabBox>
1526
1544
  )
1527
1545
  }
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 = (
1546
+ </>
1547
+ );
1548
+ } else if (expandedFilterDrawer === FilterDrawer.Advanced) {
1549
+ // Create advanced filter ui
1550
+ filterDrawer = (
1551
+ <>
1552
+ {/* User Info */}
1553
+ <TabBox title="User">
1554
+ {/* First Name */}
1555
+ <div className="input-group mb-2">
1556
+ <span className="input-group-text">
1557
+ User First Name
1558
+ </span>
1559
+ <input
1560
+ type="text"
1561
+ className="form-control"
1562
+ aria-label="query for user first name"
1563
+ value={advancedFilterState.userFirstName}
1564
+ placeholder="e.g. Divardo"
1565
+ onChange={(e) => {
1566
+ advancedFilterState.userFirstName = e.target.value;
1567
+ dispatch({
1568
+ type: ActionType.UpdateAdvancedFilterState,
1569
+ advancedFilterState,
1570
+ });
1571
+ }}
1572
+ />
1573
+ </div>
1574
+ {/* Last Name */}
1575
+ <div className="input-group mb-2">
1576
+ <span className="input-group-text">
1577
+ User Last Name
1578
+ </span>
1579
+ <input
1580
+ type="text"
1581
+ className="form-control"
1582
+ aria-label="query for user last name"
1583
+ value={advancedFilterState.userLastName}
1584
+ placeholder="e.g. Calicci"
1585
+ onChange={(e) => {
1586
+ advancedFilterState.userLastName = e.target.value;
1587
+ dispatch({
1588
+ type: ActionType.UpdateAdvancedFilterState,
1589
+ advancedFilterState,
1590
+ });
1591
+ }}
1592
+ />
1593
+ </div>
1594
+ {/* Email */}
1595
+ <div className="input-group mb-2">
1596
+ <span className="input-group-text">
1597
+ User Email
1598
+ </span>
1599
+ <input
1600
+ type="text"
1601
+ className="form-control"
1602
+ aria-label="query for user email"
1603
+ value={advancedFilterState.userEmail}
1604
+ placeholder="e.g. calicci@fas.harvard.edu"
1605
+ onChange={(e) => {
1606
+ advancedFilterState.userEmail = (
1607
+ (e.target.value)
1608
+ .trim()
1609
+ );
1610
+ dispatch({
1611
+ type: ActionType.UpdateAdvancedFilterState,
1612
+ advancedFilterState,
1613
+ });
1614
+ }}
1615
+ />
1616
+ </div>
1617
+ {/* Canvas Id */}
1618
+ <div className="input-group mb-2">
1619
+ <span className="input-group-text">
1620
+ User Canvas Id
1621
+ </span>
1622
+ <input
1623
+ type="text"
1624
+ className="form-control"
1625
+ aria-label="query for user canvas id"
1626
+ value={advancedFilterState.userId}
1627
+ placeholder="e.g. 104985"
1628
+ onChange={(e) => {
1629
+ const { value } = e.target;
1630
+ // Only update if value contains only numbers
1631
+ if (/^\d+$/.test(value)) {
1632
+ advancedFilterState.userId = (
1589
1633
  (e.target.value)
1590
1634
  .trim()
1591
1635
  );
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>
1636
+ }
1637
+ dispatch({
1638
+ type: ActionType.UpdateAdvancedFilterState,
1639
+ advancedFilterState,
1640
+ });
1641
+ }}
1642
+ />
1643
+ </div>
1644
+ {/* Role */}
1645
+ <ButtonInputGroup label="Role">
1646
+ <CheckboxButton
1647
+ text="Students"
1648
+ onChanged={(checked) => {
1649
+ advancedFilterState.includeLearners = checked;
1650
+ dispatch({
1651
+ type: ActionType.UpdateAdvancedFilterState,
1652
+ advancedFilterState,
1653
+ });
1654
+ }}
1655
+ checked={advancedFilterState.includeLearners}
1656
+ ariaLabel="show logs from students"
1657
+ />
1658
+ <CheckboxButton
1659
+ text="Teaching Team Members"
1660
+ onChanged={(checked) => {
1661
+ advancedFilterState.includeTTMs = checked;
1662
+ dispatch({
1663
+ type: ActionType.UpdateAdvancedFilterState,
1664
+ advancedFilterState,
1665
+ });
1666
+ }}
1667
+ checked={advancedFilterState.includeTTMs}
1668
+ ariaLabel="show logs from teaching team members"
1669
+ />
1670
+ <CheckboxButton
1671
+ text="Admins"
1672
+ onChanged={(checked) => {
1673
+ advancedFilterState.includeAdmins = checked;
1674
+ dispatch({
1675
+ type: ActionType.UpdateAdvancedFilterState,
1676
+ advancedFilterState,
1677
+ });
1678
+ }}
1679
+ checked={advancedFilterState.includeAdmins}
1680
+ ariaLabel="show logs from admins"
1681
+ />
1682
+ </ButtonInputGroup>
1683
+ </TabBox>
1684
+
1685
+ {/* Course Info */}
1686
+ <TabBox title="Course">
1687
+ {/* Name */}
1688
+ <div className="input-group mb-2">
1689
+ <span className="input-group-text">
1690
+ Course Name
1691
+ </span>
1692
+ <input
1693
+ type="text"
1694
+ className="form-control"
1695
+ aria-label="query for course name"
1696
+ value={advancedFilterState.courseName}
1697
+ placeholder="e.g. GLC 200"
1698
+ onChange={(e) => {
1699
+ advancedFilterState.courseName = e.target.value;
1700
+ dispatch({
1701
+ type: ActionType.UpdateAdvancedFilterState,
1702
+ advancedFilterState,
1703
+ });
1704
+ }}
1705
+ />
1706
+ </div>
1707
+ {/* Canvas Id */}
1708
+ <div className="input-group mb-2">
1709
+ <span className="input-group-text">
1710
+ Course Canvas Id
1711
+ </span>
1712
+ <input
1713
+ type="text"
1714
+ className="form-control"
1715
+ aria-label="query for course canvas id"
1716
+ value={advancedFilterState.courseId}
1717
+ placeholder="e.g. 15948"
1718
+ onChange={(e) => {
1719
+ const { value } = e.target;
1720
+ // Only update if value contains only numbers
1721
+ if (/^\d+$/.test(value)) {
1722
+ advancedFilterState.courseId = (
1723
+ (e.target.value)
1724
+ .trim()
1725
+ );
1726
+ }
1727
+ dispatch({
1728
+ type: ActionType.UpdateAdvancedFilterState,
1729
+ advancedFilterState,
1730
+ });
1731
+ }}
1732
+ />
1733
+ </div>
1734
+ </TabBox>
1666
1735
 
1667
- {/* Course Info */}
1668
- <TabBox title="Course">
1669
- {/* Name */}
1736
+ {/* Device Info */}
1737
+ <TabBox title="Device">
1738
+ <ButtonInputGroup label="Device Type">
1739
+ <RadioButton
1740
+ text="All Devices"
1741
+ ariaLabel="show logs from all devices"
1742
+ selected={advancedFilterState.isMobile === undefined}
1743
+ onSelected={() => {
1744
+ advancedFilterState.isMobile = undefined;
1745
+ dispatch({
1746
+ type: ActionType.UpdateAdvancedFilterState,
1747
+ advancedFilterState,
1748
+ });
1749
+ }}
1750
+ />
1751
+ <RadioButton
1752
+ text="Mobile Only"
1753
+ ariaLabel="show logs from mobile devices"
1754
+ selected={advancedFilterState.isMobile === true}
1755
+ onSelected={() => {
1756
+ advancedFilterState.isMobile = true;
1757
+ dispatch({
1758
+ type: ActionType.UpdateAdvancedFilterState,
1759
+ advancedFilterState,
1760
+ });
1761
+ }}
1762
+ />
1763
+ <RadioButton
1764
+ text="Desktop Only"
1765
+ ariaLabel="show logs from desktop devices"
1766
+ selected={advancedFilterState.isMobile === false}
1767
+ onSelected={() => {
1768
+ advancedFilterState.isMobile = false;
1769
+ dispatch({
1770
+ type: ActionType.UpdateAdvancedFilterState,
1771
+ advancedFilterState,
1772
+ });
1773
+ }}
1774
+ noMarginOnRight
1775
+ />
1776
+ </ButtonInputGroup>
1777
+ </TabBox>
1778
+
1779
+ {/* Source */}
1780
+ <TabBox title="Source">
1781
+ <ButtonInputGroup label="Source Type">
1782
+ <RadioButton
1783
+ text="Both"
1784
+ ariaLabel="show logs from all sources"
1785
+ selected={advancedFilterState.source === undefined}
1786
+ onSelected={() => {
1787
+ advancedFilterState.source = undefined;
1788
+ dispatch({
1789
+ type: ActionType.UpdateAdvancedFilterState,
1790
+ advancedFilterState,
1791
+ });
1792
+ }}
1793
+ />
1794
+ <RadioButton
1795
+ text="Client Only"
1796
+ ariaLabel="show logs from client source"
1797
+ selected={advancedFilterState.source === LogSource.Client}
1798
+ onSelected={() => {
1799
+ advancedFilterState.source = LogSource.Client;
1800
+ dispatch({
1801
+ type: ActionType.UpdateAdvancedFilterState,
1802
+ advancedFilterState,
1803
+ });
1804
+ }}
1805
+ />
1806
+ <RadioButton
1807
+ text="Server Only"
1808
+ ariaLabel="show logs from server source"
1809
+ selected={advancedFilterState.source === LogSource.Server}
1810
+ onSelected={() => {
1811
+ advancedFilterState.source = LogSource.Server;
1812
+ dispatch({
1813
+ type: ActionType.UpdateAdvancedFilterState,
1814
+ advancedFilterState,
1815
+ });
1816
+ }}
1817
+ noMarginOnRight
1818
+ />
1819
+ </ButtonInputGroup>
1820
+
1821
+ {/* Server filters */}
1822
+ {advancedFilterState.source !== LogSource.Client && (
1823
+ <div className="mt-2">
1824
+ {/* Route path */}
1670
1825
  <div className="input-group mb-2">
1671
1826
  <span className="input-group-text">
1672
- Course Name
1827
+ Server Route Path
1673
1828
  </span>
1674
1829
  <input
1675
1830
  type="text"
1676
1831
  className="form-control"
1677
- aria-label="query for course name"
1678
- value={advancedFilterState.courseName}
1679
- placeholder="e.g. GLC 200"
1832
+ aria-label="query for server route path"
1833
+ value={advancedFilterState.routePath}
1834
+ placeholder="e.g. /api/ttm/courses/12345"
1680
1835
  onChange={(e) => {
1681
- advancedFilterState.courseName = e.target.value;
1836
+ advancedFilterState.courseName = (
1837
+ (e.target.value)
1838
+ .trim()
1839
+ );
1682
1840
  dispatch({
1683
1841
  type: ActionType.UpdateAdvancedFilterState,
1684
1842
  advancedFilterState,
@@ -1686,26 +1844,23 @@ const LogReviewer: React.FC<Props> = (props) => {
1686
1844
  }}
1687
1845
  />
1688
1846
  </div>
1689
- {/* Canvas Id */}
1847
+
1848
+ {/* Route template */}
1690
1849
  <div className="input-group mb-2">
1691
1850
  <span className="input-group-text">
1692
- Course Canvas Id
1851
+ Server Route Template
1693
1852
  </span>
1694
1853
  <input
1695
1854
  type="text"
1696
1855
  className="form-control"
1697
- aria-label="query for course canvas id"
1698
- value={advancedFilterState.courseId}
1699
- placeholder="e.g. 15948"
1856
+ aria-label="query for server route template"
1857
+ value={advancedFilterState.routeTemplate}
1858
+ placeholder="e.g. /api/ttm/courses/:courseId"
1700
1859
  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
- }
1860
+ advancedFilterState.courseName = (
1861
+ (e.target.value)
1862
+ .trim()
1863
+ );
1709
1864
  dispatch({
1710
1865
  type: ActionType.UpdateAdvancedFilterState,
1711
1866
  advancedFilterState,
@@ -1713,534 +1868,51 @@ const LogReviewer: React.FC<Props> = (props) => {
1713
1868
  }}
1714
1869
  />
1715
1870
  </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
- }
1871
+ </div>
1872
+ )}
1873
+ </TabBox>
1874
+ </>
1875
+ );
1859
1876
  }
1877
+ }
1860
1878
 
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
- /*----------------------------------------*/
1879
+ // Filters UI
1880
+ const filters = (
1881
+ <>
1882
+ {filterToggles}
1883
+ {filterDrawer && (
1884
+ <Drawer customBackgroundColor="#eee">
1885
+ {filterDrawer}
1886
+ </Drawer>
1887
+ )}
1888
+ </>
1889
+ );
2204
1890
 
2205
- // Nothing to show notice
2206
- const noLogsNotice = (
2207
- logs.length === 0
2208
- ? (
1891
+ // Main body
1892
+ body = (
1893
+ <>
1894
+ {filters}
1895
+ <div className="mt-2">
1896
+ <IntelliTable
1897
+ title="Matching Logs:"
1898
+ csvName={`Logs from ${getHumanReadableDate()}`}
1899
+ id="logs"
1900
+ data={logs}
1901
+ columns={columns}
1902
+ />
1903
+ {logs.length === 0 && (
2209
1904
  <div className="alert alert-warning text-center mt-2">
2210
- <h4 className="m-1">
2211
- No Logs to Show
2212
- </h4>
1905
+ <h4 className="m-1">No Logs to Show</h4>
2213
1906
  <div>
2214
1907
  Either your filters are too strict or no matching logs have been
2215
1908
  created yet.
2216
1909
  </div>
2217
1910
  </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
- }
1911
+ )}
1912
+ {paginationControls}
1913
+ </div>
1914
+ </>
1915
+ );
2244
1916
 
2245
1917
  /* ---------- Wrap in Modal --------- */
2246
1918
 
@@ -2252,9 +1924,7 @@ const LogReviewer: React.FC<Props> = (props) => {
2252
1924
  <div className="LogReviewer-inner-container">
2253
1925
  <div className="LogReviewer-header">
2254
1926
  <div className="LogReviewer-header-title">
2255
- <h3 className="text-center m-0">
2256
- Log Review Dashboard
2257
- </h3>
1927
+ <h3 className="text-center m-0">Log Review Dashboard</h3>
2258
1928
  </div>
2259
1929
  <div style={{ width: 0 }}>
2260
1930
  <button
@@ -2263,15 +1933,11 @@ const LogReviewer: React.FC<Props> = (props) => {
2263
1933
  aria-label="close log reviewer panel"
2264
1934
  onClick={onClose}
2265
1935
  >
2266
- <FontAwesomeIcon
2267
- icon={faTimes}
2268
- />
1936
+ <FontAwesomeIcon icon={faTimes} />
2269
1937
  </button>
2270
1938
  </div>
2271
1939
  </div>
2272
- <div className="LogReviewer-contents">
2273
- {body}
2274
- </div>
1940
+ <div className="LogReviewer-contents">{body}</div>
2275
1941
  </div>
2276
1942
  </div>
2277
1943
  );