dce-reactkit 3.2.2-beta.9 → 3.2.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 (47) hide show
  1. package/dist/cjs/index.js +731 -435
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/Drawer.d.ts +1 -0
  4. package/dist/cjs/types/components/IntelliTable.d.ts +1 -0
  5. package/dist/cjs/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  6. package/dist/cjs/types/dynamicConstants/DynamicWord.d.ts +13 -0
  7. package/dist/cjs/types/index.d.ts +2 -1
  8. package/dist/cjs/types/types/LogAction.d.ts +16 -16
  9. package/dist/cjs/types/types/LogBuiltInMetadata.d.ts +1 -1
  10. package/dist/cjs/types/types/LogMetadataContextMap.d.ts +11 -0
  11. package/dist/cjs/types/types/LogMetadataTagMap.d.ts +8 -0
  12. package/dist/cjs/types/types/LogMetadataTargetMap.d.ts +8 -0
  13. package/dist/cjs/types/types/LogMetadataType.d.ts +6 -12
  14. package/dist/esm/index.js +732 -437
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/types/components/Drawer.d.ts +1 -0
  17. package/dist/esm/types/components/IntelliTable.d.ts +1 -0
  18. package/dist/esm/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  19. package/dist/esm/types/dynamicConstants/DynamicWord.d.ts +13 -0
  20. package/dist/esm/types/index.d.ts +2 -1
  21. package/dist/esm/types/types/LogAction.d.ts +16 -16
  22. package/dist/esm/types/types/LogBuiltInMetadata.d.ts +1 -1
  23. package/dist/esm/types/types/LogMetadataContextMap.d.ts +11 -0
  24. package/dist/esm/types/types/LogMetadataTagMap.d.ts +8 -0
  25. package/dist/esm/types/types/LogMetadataTargetMap.d.ts +8 -0
  26. package/dist/esm/types/types/LogMetadataType.d.ts +6 -12
  27. package/dist/index.d.ts +63 -30
  28. package/package.json +1 -1
  29. package/src/components/ButtonInputGroup.tsx +1 -1
  30. package/src/components/CSVDownloadButton.tsx +2 -2
  31. package/src/components/Drawer.tsx +9 -1
  32. package/src/components/IntelliTable.tsx +178 -23
  33. package/src/components/ItemPicker/NestableItemList.tsx +32 -14
  34. package/src/components/LogReviewer.tsx +584 -424
  35. package/src/components/SimpleDateChooser.tsx +30 -25
  36. package/src/dynamicConstants/DynamicWord.tsx +55 -0
  37. package/src/helpers/genCSV.ts +22 -4
  38. package/src/helpers/genRouteHandler.ts +2 -2
  39. package/src/helpers/logClientEvent.tsx +1 -1
  40. package/src/index.ts +5 -0
  41. package/src/server/initServer.ts +10 -3
  42. package/src/types/LogAction.ts +16 -16
  43. package/src/types/LogBuiltInMetadata.ts +5 -5
  44. package/src/types/LogMetadataContextMap.ts +15 -0
  45. package/src/types/LogMetadataTagMap.ts +9 -0
  46. package/src/types/LogMetadataTargetMap.ts +9 -0
  47. package/src/types/LogMetadataType.ts +8 -15
@@ -21,7 +21,8 @@ import {
21
21
  // Import shared helpers
22
22
  import visitServerEndpoint from '../helpers/visitServerEndpoint';
23
23
  import getTimeInfoInET from '../helpers/getTimeInfoInET';
24
- import { showFatalError } from './AppWrapper';
24
+ import { alert, showFatalError } from './AppWrapper';
25
+ import getHumanReadableDate from '../helpers/getHumanReadableDate';
25
26
 
26
27
  // Import shared constants
27
28
  import LOG_REVIEW_ROUTE_PATH_PREFIX from '../constants/LOG_REVIEW_ROUTE_PATH_PREFIX';
@@ -34,6 +35,9 @@ import LogType from '../types/LogType';
34
35
  import LogAction from '../types/LogAction';
35
36
  import ParamType from '../types/ParamType';
36
37
  import IntelliTableColumn from '../types/IntelliTableColumn';
38
+ import LogBuiltInMetadata from '../types/LogBuiltInMetadata';
39
+ import LogMetadataContextMap from '../types/LogMetadataContextMap';
40
+ import LogMetadataTargetMap from '../types/LogMetadataTargetMap';
37
41
 
38
42
  // Import shared components
39
43
  import SimpleDateChooser from './SimpleDateChooser';
@@ -121,7 +125,7 @@ type ContextFilterState = {
121
125
 
122
126
  // Tag filter state
123
127
  type TagFilterState = {
124
- [k: string]: boolean
128
+ [k: string]: boolean // tag => true if in the list of tags to show
125
129
  };
126
130
 
127
131
  // Action filter state (only relevant for action logs)
@@ -243,8 +247,12 @@ const style = `
243
247
  background-color: transparent !important;
244
248
  padding-top: 0 !important;
245
249
  padding-bottom: 0 !important;
250
+ padding-right: 1em !important;
246
251
  margin: 0 !important;
247
- color: #333 !important;
252
+ color: #444 !important;
253
+
254
+ right: 0 !important;
255
+ position: absolute !important;
248
256
  }
249
257
  .LogReviewer-header-close-button:hover {
250
258
  border: 0 !important;
@@ -256,6 +264,197 @@ const style = `
256
264
  }
257
265
  `;
258
266
 
267
+ /*------------------------------------------------------------------------*/
268
+ /* Constants */
269
+ /*------------------------------------------------------------------------*/
270
+
271
+ const columns: IntelliTableColumn[] = [
272
+ {
273
+ title: 'First Name',
274
+ param: 'userFirstName',
275
+ type: ParamType.String,
276
+ },
277
+ {
278
+ title: 'Last Name',
279
+ param: 'userLastName',
280
+ type: ParamType.String,
281
+ },
282
+ {
283
+ title: 'Email',
284
+ param: 'userEmail',
285
+ type: ParamType.String,
286
+ },
287
+ {
288
+ title: 'Canvas Id',
289
+ param: 'userId',
290
+ type: ParamType.Int,
291
+ },
292
+ {
293
+ title: 'Student?',
294
+ param: 'isLearner',
295
+ type: ParamType.Boolean,
296
+ },
297
+ {
298
+ title: 'Teaching Staff?',
299
+ param: 'isTTM',
300
+ type: ParamType.Boolean,
301
+ startsHidden: true,
302
+ },
303
+ {
304
+ title: 'Admin?',
305
+ param: 'isAdmin',
306
+ type: ParamType.Boolean,
307
+ startsHidden: true,
308
+ },
309
+ {
310
+ title: 'Course Canvas Id',
311
+ param: 'courseId',
312
+ type: ParamType.Int,
313
+ startsHidden: true,
314
+ },
315
+ {
316
+ title: 'Course Name',
317
+ param: 'courseName',
318
+ type: ParamType.String,
319
+ },
320
+ {
321
+ title: 'Browser Name',
322
+ param: 'browser.name',
323
+ type: ParamType.String,
324
+ startsHidden: true,
325
+ },
326
+ {
327
+ title: 'Browser Version',
328
+ param: 'browser.version',
329
+ type: ParamType.String,
330
+ startsHidden: true,
331
+ },
332
+ {
333
+ title: 'OS',
334
+ param: 'device.os',
335
+ type: ParamType.String,
336
+ startsHidden: true,
337
+ },
338
+ {
339
+ title: 'Mobile?',
340
+ param: 'device.isMobile',
341
+ type: ParamType.Boolean,
342
+ startsHidden: true,
343
+ },
344
+ {
345
+ title: 'Year',
346
+ param: 'year',
347
+ type: ParamType.Int,
348
+ },
349
+ {
350
+ title: 'Month',
351
+ param: 'month',
352
+ type: ParamType.Int,
353
+ },
354
+ {
355
+ title: 'Day',
356
+ param: 'day',
357
+ type: ParamType.Int,
358
+ },
359
+ {
360
+ title: 'Hour',
361
+ param: 'hour',
362
+ type: ParamType.Int,
363
+ },
364
+ {
365
+ title: 'Minute',
366
+ param: 'minute',
367
+ type: ParamType.Int,
368
+ startsHidden: true,
369
+ },
370
+ {
371
+ title: 'Timestamp',
372
+ param: 'timestamp',
373
+ type: ParamType.Int,
374
+ startsHidden: true,
375
+ },
376
+ {
377
+ title: 'Context',
378
+ param: 'context',
379
+ type: ParamType.String,
380
+ },
381
+ {
382
+ title: 'Subcontext',
383
+ param: 'subcontext',
384
+ type: ParamType.String,
385
+ },
386
+ {
387
+ title: 'Tags',
388
+ param: 'tags',
389
+ type: ParamType.JSON,
390
+ startsHidden: true,
391
+ },
392
+ {
393
+ title: 'Log Level',
394
+ param: 'level',
395
+ type: ParamType.String,
396
+ startsHidden: true,
397
+ },
398
+ {
399
+ title: 'Metadata',
400
+ param: 'metadata',
401
+ type: ParamType.JSON,
402
+ startsHidden: true,
403
+ },
404
+ {
405
+ title: 'Source',
406
+ param: 'source',
407
+ type: ParamType.String,
408
+ },
409
+ {
410
+ title: 'Server Route Path',
411
+ param: 'routePath',
412
+ type: ParamType.String,
413
+ startsHidden: true,
414
+ },
415
+ {
416
+ title: 'Server Route Template',
417
+ param: 'routeTemplate',
418
+ type: ParamType.String,
419
+ startsHidden: true,
420
+ },
421
+ {
422
+ title: 'Type',
423
+ param: 'type',
424
+ type: ParamType.String,
425
+ },
426
+ {
427
+ title: 'Error Message',
428
+ param: 'errorMessage',
429
+ type: ParamType.String,
430
+ startsHidden: true,
431
+ },
432
+ {
433
+ title: 'Error Code',
434
+ param: 'errorCode',
435
+ type: ParamType.String,
436
+ startsHidden: true,
437
+ },
438
+ {
439
+ title: 'Error Stack',
440
+ param: 'errorStack',
441
+ type: ParamType.String,
442
+ startsHidden: true,
443
+ },
444
+ {
445
+ title: 'Action Target',
446
+ param: 'target',
447
+ type: ParamType.String,
448
+ startsHidden: true,
449
+ },
450
+ {
451
+ title: 'Action Type',
452
+ param: 'action',
453
+ type: ParamType.String,
454
+ startsHidden: true,
455
+ },
456
+ ];
457
+
259
458
  /*------------------------------------------------------------------------*/
260
459
  /* Static Functions */
261
460
  /*------------------------------------------------------------------------*/
@@ -499,6 +698,36 @@ const LogReviewer: React.FC<Props> = (props) => {
499
698
  onClose,
500
699
  } = props;
501
700
 
701
+ // Create complete map of contexts
702
+ const contextMap: LogMetadataContextMap = {};
703
+ [
704
+ (LogMetadata.Context ?? {}),
705
+ LogBuiltInMetadata.Context,
706
+ ].forEach((partialContextMap) => {
707
+ Object.keys(partialContextMap).forEach((context) => {
708
+ // Add context
709
+ contextMap[context] = partialContextMap[context];
710
+
711
+ // If context has children, add an "uncategorized" subcontext
712
+ if (typeof contextMap[context] !== 'string') {
713
+ (contextMap[context] as any)[LogBuiltInMetadata.Context.Uncategorized] = (
714
+ LogBuiltInMetadata.Context.Uncategorized
715
+ );
716
+ }
717
+ });
718
+ });
719
+
720
+ // Create complete map of targets
721
+ const targetMap: LogMetadataTargetMap = {};
722
+ [
723
+ (LogMetadata.Target ?? {}),
724
+ LogBuiltInMetadata.Target,
725
+ ].forEach((partialTargetMap) => {
726
+ Object.keys(partialTargetMap).forEach((target) => {
727
+ targetMap[target] = partialTargetMap[target];
728
+ });
729
+ });
730
+
502
731
  /* -------------- State ------------- */
503
732
 
504
733
  // Create initial date filter state
@@ -520,25 +749,30 @@ const LogReviewer: React.FC<Props> = (props) => {
520
749
 
521
750
  // Create initial context filter state
522
751
  const initContextFilterState: ContextFilterState = {};
523
- Object.keys(LogMetadata.Context ?? {}).forEach((context) => {
524
- const contextValue = (LogMetadata.Context ?? {})[context];
752
+ Object.keys(contextMap).forEach((context) => {
753
+ const contextValue = contextMap[context];
525
754
  if (typeof contextValue === 'string') {
526
- // Case: no subcontexts
755
+ // Case: no subcontexts, init as checked
527
756
  initContextFilterState[contextValue] = true;
528
757
  } else {
529
758
  // Case: subcontexts exist
530
- initContextFilterState[contextValue._] = {};
531
- Object.values((LogMetadata.Context ?? {})[context]).forEach((subcontext) => {
532
- const subcontextValue = contextValue[subcontext];
533
- (initContextFilterState[contextValue._] as { [k: string]: boolean })[subcontextValue] = true;
759
+ initContextFilterState[context] = {};
760
+ Object.values(contextMap[context]).forEach((subcontext) => {
761
+ // Skip self ("_")
762
+ if (subcontext === '_') {
763
+ return;
764
+ }
765
+
766
+ // Initialize as checked
767
+ (initContextFilterState[context] as any)[subcontext] = true;
534
768
  });
535
769
  }
536
770
  });
537
771
 
538
772
  // Create initial tag filter state
539
773
  const initTagFilterState: TagFilterState = {};
540
- Object.values(LogMetadata.Tag ?? {}).forEach((tagValue) => {
541
- initTagFilterState[tagValue] = true;
774
+ Object.keys(LogMetadata.Tag ?? {}).forEach((tag) => {
775
+ initTagFilterState[tag] = false;
542
776
  });
543
777
 
544
778
  // Create advanced filter state
@@ -566,7 +800,7 @@ const LogReviewer: React.FC<Props> = (props) => {
566
800
  target: {},
567
801
  action: {},
568
802
  };
569
- Object.values(LogMetadata.Target ?? {}).forEach((target) => {
803
+ Object.values(targetMap).forEach((target) => {
570
804
  initActionErrorFilterState.target[target] = true;
571
805
  });
572
806
  Object.values(LogAction).forEach((action) => {
@@ -622,18 +856,23 @@ const LogReviewer: React.FC<Props> = (props) => {
622
856
  let month = newDateFilterState.startDate.month;
623
857
  while (
624
858
  // Earlier year
625
- (year <= newDateFilterState.endDate.year)
859
+ (year < newDateFilterState.endDate.year)
626
860
  // Current year but included month
627
861
  || (
628
862
  year === newDateFilterState.endDate.year
629
863
  && month <= newDateFilterState.endDate.month
630
864
  )
631
865
  ) {
632
- // Add to list
633
- toLoad.push({
634
- year,
635
- month,
636
- });
866
+ // Add to list if not already loaded
867
+ if (
868
+ !logMap[year]
869
+ || !logMap[year][month]
870
+ ) {
871
+ toLoad.push({
872
+ year,
873
+ month,
874
+ });
875
+ }
637
876
 
638
877
  // Increment
639
878
  month += 1;
@@ -749,7 +988,7 @@ const LogReviewer: React.FC<Props> = (props) => {
749
988
 
750
989
  // Filter toggle
751
990
  const filterToggles = (
752
- <div className="LogReviewer-filter-toggles mb-2">
991
+ <div className="LogReviewer-filter-toggles">
753
992
  <h3 className="m-0">
754
993
  Filters:
755
994
  </h3>
@@ -837,7 +1076,7 @@ const LogReviewer: React.FC<Props> = (props) => {
837
1076
  <button
838
1077
  type="button"
839
1078
  id="LogReviewer-toggle-advanced-filter-drawer"
840
- className={`btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'}`}
1079
+ className={`btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
841
1080
  aria-label="toggle advanced filter drawer"
842
1081
  onClick={() => {
843
1082
  dispatch({
@@ -852,6 +1091,29 @@ const LogReviewer: React.FC<Props> = (props) => {
852
1091
  />
853
1092
  Advanced
854
1093
  </button>
1094
+ {/* Reset */}
1095
+ <button
1096
+ type="button"
1097
+ id="LogReviewer-reset-filters-button"
1098
+ className="btn btn-light"
1099
+ aria-label="reset filters"
1100
+ onClick={() => {
1101
+ dispatch({
1102
+ type: ActionType.ResetFilters,
1103
+ initActionErrorFilterState,
1104
+ initAdvancedFilterState,
1105
+ initContextFilterState,
1106
+ initDateFilterState,
1107
+ initTagFilterState,
1108
+ });
1109
+ }}
1110
+ >
1111
+ <FontAwesomeIcon
1112
+ icon={faTimes}
1113
+ />
1114
+ {' '}
1115
+ Reset
1116
+ </button>
855
1117
  </div>
856
1118
  </div>
857
1119
  );
@@ -868,14 +1130,11 @@ const LogReviewer: React.FC<Props> = (props) => {
868
1130
  year={dateFilterState.startDate.year}
869
1131
  month={dateFilterState.startDate.month}
870
1132
  day={dateFilterState.startDate.day}
1133
+ chooseFromPast
1134
+ numMonthsToShow={36}
871
1135
  onChange={(month, day, year) => {
872
- dispatch({
873
- type: ActionType.UpdateDateFilterState,
874
- dateFilterState: {
875
- ...dateFilterState,
876
- startDate: { month, day, year },
877
- },
878
- });
1136
+ dateFilterState.startDate = { month, day, year };
1137
+ handleDateRangeUpdated(dateFilterState);
879
1138
  }}
880
1139
  />
881
1140
  {' '}
@@ -887,59 +1146,92 @@ const LogReviewer: React.FC<Props> = (props) => {
887
1146
  year={dateFilterState.endDate.year}
888
1147
  month={dateFilterState.endDate.month}
889
1148
  day={dateFilterState.endDate.day}
1149
+ chooseFromPast
1150
+ numMonthsToShow={12}
890
1151
  onChange={(month, day, year) => {
891
- dispatch({
892
- type: ActionType.UpdateDateFilterState,
893
- dateFilterState: {
894
- ...dateFilterState,
895
- endDate: { month, day, year },
896
- },
897
- });
1152
+ if (
1153
+ year < dateFilterState.startDate.year
1154
+ || (
1155
+ year === dateFilterState.startDate.year
1156
+ && month < dateFilterState.startDate.month
1157
+ )
1158
+ || (
1159
+ year === dateFilterState.startDate.year
1160
+ && month === dateFilterState.startDate.month
1161
+ && day < dateFilterState.startDate.day
1162
+ )
1163
+ ) {
1164
+ return alert(
1165
+ 'Invalid Start Date',
1166
+ 'The start date cannot be before the end date.',
1167
+ );
1168
+ }
1169
+ dateFilterState.endDate = { month, day, year };
1170
+ handleDateRangeUpdated(dateFilterState);
898
1171
  }}
899
1172
  />
900
1173
  </TabBox>
901
1174
  );
902
1175
  } else if (expandedFilterDrawer === FilterDrawer.Context) {
903
1176
  // Create item picker items
904
- const pickableItems: PickableItem[] = (
905
- Object.keys(LogMetadata.Context ?? {})
906
- .map((context) => {
907
- const value = (LogMetadata.Context ?? {})[context];
908
- if (typeof value === 'string') {
909
- // No subcategories
910
- const item: PickableItem = {
911
- id: context,
912
- name: genHumanReadableName(context),
913
- isGroup: false,
914
- checked: !!contextFilterState[context],
915
- };
916
- return item;
917
- }
918
-
919
- // Has subcategories
920
- const children: PickableItem[] = (
921
- Object.keys(value)
922
- .filter((subcontext) => {
923
- return subcontext !== '_'
924
- })
925
- .map((subcontext) => {
926
- return {
927
- id: `${context}-${subcontext}`,
928
- name: genHumanReadableName(subcontext),
929
- isGroup: false,
930
- checked: !!value[subcontext],
931
- };
932
- })
933
- );
1177
+ const builtInPickableItem: PickableItem = {
1178
+ id: 'built-in-contexts',
1179
+ name: 'Auto-logged',
1180
+ isGroup: true,
1181
+ children: [],
1182
+ };
1183
+ const pickableItems: PickableItem[] = [];
1184
+ Object.keys(contextMap)
1185
+ .forEach((context) => {
1186
+ const value = contextMap[context];
1187
+ if (typeof value === 'string') {
1188
+ // No subcategories
934
1189
  const item: PickableItem = {
935
1190
  id: context,
936
- name: context,
937
- isGroup: true,
938
- children,
1191
+ name: genHumanReadableName(context),
1192
+ isGroup: false,
1193
+ checked: !!contextFilterState[context],
939
1194
  };
940
- return item;
941
- })
942
- );
1195
+
1196
+ // Add built-in items to its own folder
1197
+ const isBuiltIn = context in LogBuiltInMetadata.Context;
1198
+ if (isBuiltIn) {
1199
+ // Add to built-in pickable item
1200
+ builtInPickableItem.children.push(item);
1201
+ } else {
1202
+ // Add to pickable items list
1203
+ pickableItems.push(item);
1204
+ }
1205
+ return;
1206
+ }
1207
+
1208
+ // Has subcategories
1209
+ const children: PickableItem[] = (
1210
+ Object.keys(value)
1211
+ // Remove parent name
1212
+ .filter((subcontext) => {
1213
+ return subcontext !== '_';
1214
+ })
1215
+ // Create child pickable items
1216
+ .map((subcontext) => {
1217
+ return {
1218
+ id: subcontext,
1219
+ name: genHumanReadableName(subcontext),
1220
+ isGroup: false,
1221
+ checked: (contextFilterState[context] as any)[subcontext],
1222
+ };
1223
+ })
1224
+ );
1225
+ const item: PickableItem = {
1226
+ id: context,
1227
+ name: genHumanReadableName(context),
1228
+ isGroup: true,
1229
+ children,
1230
+ };
1231
+ pickableItems.push(item);
1232
+ });
1233
+ // Add built-in contexts to end ofl ist
1234
+ pickableItems.push(builtInPickableItem);
943
1235
 
944
1236
  // Create filter UI
945
1237
  filterDrawer = (
@@ -951,11 +1243,29 @@ const LogReviewer: React.FC<Props> = (props) => {
951
1243
  updatedItems.forEach((pickableItem) => {
952
1244
  if (pickableItem.isGroup) {
953
1245
  // Has subcontexts
954
- pickableItem.children.forEach((subcontextItem) => {
955
- (contextFilterState as any)[pickableItem.id][subcontextItem.id] = (
956
- (subcontextItem as any).checked
957
- );
958
- });
1246
+
1247
+ if (pickableItem.id === 'built-in-contexts') {
1248
+ // Built-in
1249
+
1250
+ // Treat as if these were top-level contexts
1251
+ pickableItem.children.forEach((subcontextItem) => {
1252
+ contextFilterState[subcontextItem.id] = (
1253
+ 'checked' in subcontextItem
1254
+ && subcontextItem.checked
1255
+ );
1256
+ });
1257
+ } else {
1258
+ // Not built-in
1259
+ pickableItem.children.forEach((subcontextItem) => {
1260
+ if (!subcontextItem.isGroup) {
1261
+ (
1262
+ contextFilterState[pickableItem.id] as { [k: string]: boolean }
1263
+ )[subcontextItem.id] = (
1264
+ subcontextItem.checked
1265
+ );
1266
+ }
1267
+ });
1268
+ }
959
1269
  } else {
960
1270
  // No subcontexts
961
1271
  (contextFilterState as any)[pickableItem.id] = (
@@ -974,23 +1284,35 @@ const LogReviewer: React.FC<Props> = (props) => {
974
1284
  // Create filter UI
975
1285
  filterDrawer = (
976
1286
  <TabBox title="Tags">
977
- {
978
- Object.keys(tagFilterState)
979
- .map((tag, i) => {
980
- const description = genHumanReadableName(tag);
981
- return (
982
- <CheckboxButton
983
- id={`LogReviewer-tag-${tag}-checkbox`}
984
- text={description}
985
- ariaLabel={`include logs tagged with "${description}" in results`}
986
- noMarginOnRight={i === Object.keys(tagFilterState).length - 1}
987
- onChanged={(checked) => {
988
- tagFilterState[tag] = checked;
989
- }}
990
- />
991
- );
992
- })
993
- }
1287
+ <div>
1288
+ If any tags are selected, logs must contain at least one
1289
+ (but not necessarily all) of the
1290
+ selected tags.
1291
+ </div>
1292
+ <div className="d-flex gap-1 flex-wrap">
1293
+ {
1294
+ Object.keys(LogMetadata.Tag ?? {})
1295
+ .map((tag) => {
1296
+ const description = genHumanReadableName(tag);
1297
+ return (
1298
+ <CheckboxButton
1299
+ key={tag}
1300
+ id={`LogReviewer-tag-${tag}-checkbox`}
1301
+ text={description}
1302
+ ariaLabel={`require that logs be tagged with "${description}" or any other selected tag`}
1303
+ checked={tagFilterState[tag]}
1304
+ onChanged={(checked) => {
1305
+ tagFilterState[tag] = checked;
1306
+ dispatch({
1307
+ type: ActionType.UpdateTagFilterState,
1308
+ tagFilterState,
1309
+ });
1310
+ }}
1311
+ />
1312
+ );
1313
+ })
1314
+ }
1315
+ </div>
994
1316
  </TabBox>
995
1317
  );
996
1318
  } else if (expandedFilterDrawer === FilterDrawer.Action) {
@@ -1052,58 +1374,60 @@ const LogReviewer: React.FC<Props> = (props) => {
1052
1374
  label="Action"
1053
1375
  className="mb-2"
1054
1376
  >
1055
- {
1056
- Object.keys(LogAction)
1057
- .map((action, i) => {
1058
- const description = genHumanReadableName(action);
1059
- return (
1060
- <CheckboxButton
1061
- id={`LogReviewer-action-${action}-checkbox`}
1062
- text={description}
1063
- ariaLabel={`include logs with action type "${description}" in results`}
1064
- noMarginOnRight={i === Object.keys(LogAction).length - 1}
1065
- onChanged={(checked) => {
1066
- actionErrorFilterState.action[action] = checked;
1067
- dispatch({
1068
- type: ActionType.UpdateActionErrorFilterState,
1069
- actionErrorFilterState,
1070
- });
1071
- }}
1072
- />
1073
- );
1074
- })
1075
- }
1377
+ <div className="d-flex gap-1 flex-wrap">
1378
+ {
1379
+ Object.keys(LogAction)
1380
+ .map((action) => {
1381
+ const description = genHumanReadableName(action);
1382
+ return (
1383
+ <CheckboxButton
1384
+ key={action}
1385
+ id={`LogReviewer-action-${action}-checkbox`}
1386
+ text={description}
1387
+ ariaLabel={`include logs with action type "${description}" in results`}
1388
+ noMarginOnRight
1389
+ checked={actionErrorFilterState.action[action]}
1390
+ onChanged={(checked) => {
1391
+ actionErrorFilterState.action[action] = checked;
1392
+ dispatch({
1393
+ type: ActionType.UpdateActionErrorFilterState,
1394
+ actionErrorFilterState,
1395
+ });
1396
+ }}
1397
+ />
1398
+ );
1399
+ })
1400
+ }
1401
+ </div>
1076
1402
  </ButtonInputGroup>
1077
1403
  {/* Target */}
1078
1404
  <ButtonInputGroup label="Target">
1079
- {/* Nothing here */}
1080
- {(Object.keys(LogMetadata.Target ?? {})) && (
1081
- <div>
1082
- This app does not have any targets yet.
1083
- </div>
1084
- )}
1085
1405
  {/* List of targets */}
1086
- {
1087
- Object.keys(LogMetadata.Target ?? {})
1088
- .map((target, i) => {
1089
- const description = genHumanReadableName(target);
1090
- return (
1091
- <CheckboxButton
1092
- id={`LogReviewer-target-${target}-checkbox`}
1093
- text={description}
1094
- ariaLabel={`include logs with target "${description}" in results`}
1095
- onChanged={(checked) => {
1096
- actionErrorFilterState.target[target] = checked;
1097
- dispatch({
1098
- type: ActionType.UpdateActionErrorFilterState,
1099
- actionErrorFilterState,
1100
- });
1101
- }}
1102
- noMarginOnRight={i === Object.keys(LogMetadata.Target ?? {}).length - 1}
1103
- />
1104
- );
1105
- })
1106
- }
1406
+ <div className="d-flex gap-1 flex-wrap">
1407
+ {
1408
+ Object.keys(targetMap)
1409
+ .map((target) => {
1410
+ const description = genHumanReadableName(target);
1411
+ return (
1412
+ <CheckboxButton
1413
+ key={target}
1414
+ id={`LogReviewer-target-${target}-checkbox`}
1415
+ text={description}
1416
+ ariaLabel={`include logs with target "${description}" in results`}
1417
+ checked={actionErrorFilterState.target[target]}
1418
+ noMarginOnRight
1419
+ onChanged={(checked) => {
1420
+ actionErrorFilterState.target[target] = checked;
1421
+ dispatch({
1422
+ type: ActionType.UpdateActionErrorFilterState,
1423
+ actionErrorFilterState,
1424
+ });
1425
+ }}
1426
+ />
1427
+ );
1428
+ })
1429
+ }
1430
+ </div>
1107
1431
  </ButtonInputGroup>
1108
1432
  </TabBox>
1109
1433
  )
@@ -1125,6 +1449,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1125
1449
  className="form-control"
1126
1450
  aria-label="query for error message"
1127
1451
  value={actionErrorFilterState.errorMessage}
1452
+ placeholder="e.g. undefined is not a function"
1128
1453
  onChange={(e) => {
1129
1454
  actionErrorFilterState.errorMessage = e.target.value;
1130
1455
  dispatch({
@@ -1143,7 +1468,8 @@ const LogReviewer: React.FC<Props> = (props) => {
1143
1468
  type="text"
1144
1469
  className="form-control"
1145
1470
  aria-label="query for error code"
1146
- value={actionErrorFilterState.errorMessage}
1471
+ value={actionErrorFilterState.errorCode}
1472
+ placeholder="e.g. GC22"
1147
1473
  onChange={(e) => {
1148
1474
  actionErrorFilterState.errorCode = (
1149
1475
  (e.target.value)
@@ -1167,7 +1493,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1167
1493
  filterDrawer = (
1168
1494
  <>
1169
1495
  {/* User Info */}
1170
- <TabBox title="User Info">
1496
+ <TabBox title="User">
1171
1497
  {/* First Name */}
1172
1498
  <div className="input-group mb-2">
1173
1499
  <span className="input-group-text">
@@ -1178,6 +1504,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1178
1504
  className="form-control"
1179
1505
  aria-label="query for user first name"
1180
1506
  value={advancedFilterState.userFirstName}
1507
+ placeholder="e.g. Divardo"
1181
1508
  onChange={(e) => {
1182
1509
  advancedFilterState.userFirstName = e.target.value;
1183
1510
  dispatch({
@@ -1197,6 +1524,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1197
1524
  className="form-control"
1198
1525
  aria-label="query for user last name"
1199
1526
  value={advancedFilterState.userLastName}
1527
+ placeholder="e.g. Calicci"
1200
1528
  onChange={(e) => {
1201
1529
  advancedFilterState.userLastName = e.target.value;
1202
1530
  dispatch({
@@ -1216,6 +1544,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1216
1544
  className="form-control"
1217
1545
  aria-label="query for user email"
1218
1546
  value={advancedFilterState.userEmail}
1547
+ placeholder="e.g. calicci@fas.harvard.edu"
1219
1548
  onChange={(e) => {
1220
1549
  advancedFilterState.userEmail = (
1221
1550
  (e.target.value)
@@ -1238,6 +1567,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1238
1567
  className="form-control"
1239
1568
  aria-label="query for user canvas id"
1240
1569
  value={advancedFilterState.userId}
1570
+ placeholder="e.g. 104985"
1241
1571
  onChange={(e) => {
1242
1572
  const { value } = e.target;
1243
1573
  // Only update if value contains only numbers
@@ -1296,7 +1626,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1296
1626
  </TabBox>
1297
1627
 
1298
1628
  {/* Course Info */}
1299
- <TabBox title="Course Info">
1629
+ <TabBox title="Course">
1300
1630
  {/* Name */}
1301
1631
  <div className="input-group mb-2">
1302
1632
  <span className="input-group-text">
@@ -1307,6 +1637,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1307
1637
  className="form-control"
1308
1638
  aria-label="query for course name"
1309
1639
  value={advancedFilterState.courseName}
1640
+ placeholder="e.g. GLC 200"
1310
1641
  onChange={(e) => {
1311
1642
  advancedFilterState.courseName = e.target.value;
1312
1643
  dispatch({
@@ -1326,6 +1657,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1326
1657
  className="form-control"
1327
1658
  aria-label="query for course canvas id"
1328
1659
  value={advancedFilterState.courseId}
1660
+ placeholder="e.g. 15948"
1329
1661
  onChange={(e) => {
1330
1662
  const { value } = e.target;
1331
1663
  // Only update if value contains only numbers
@@ -1345,7 +1677,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1345
1677
  </TabBox>
1346
1678
 
1347
1679
  {/* Device Info */}
1348
- <TabBox title="Device Info">
1680
+ <TabBox title="Device">
1349
1681
  <ButtonInputGroup label="Device Type">
1350
1682
  <RadioButton
1351
1683
  text="All Devices"
@@ -1429,53 +1761,58 @@ const LogReviewer: React.FC<Props> = (props) => {
1429
1761
  />
1430
1762
  </ButtonInputGroup>
1431
1763
 
1432
- {/* Route path */}
1433
- <div className="input-group mb-2">
1434
- <span className="input-group-text">
1435
- Server Route Path
1436
- </span>
1437
- <input
1438
- type="text"
1439
- className="form-control"
1440
- aria-label="query for server route path"
1441
- placeholder="e.g. /api/ttm/courses/12345"
1442
- value={advancedFilterState.routePath}
1443
- onChange={(e) => {
1444
- advancedFilterState.courseName = (
1445
- (e.target.value)
1446
- .trim()
1447
- );
1448
- dispatch({
1449
- type: ActionType.UpdateAdvancedFilterState,
1450
- advancedFilterState,
1451
- });
1452
- }}
1453
- />
1454
- </div>
1764
+ {/* Server filters */}
1765
+ {advancedFilterState.source !== LogSource.Client && (
1766
+ <div className="mt-2">
1767
+ {/* Route path */}
1768
+ <div className="input-group mb-2">
1769
+ <span className="input-group-text">
1770
+ Server Route Path
1771
+ </span>
1772
+ <input
1773
+ type="text"
1774
+ className="form-control"
1775
+ aria-label="query for server route path"
1776
+ value={advancedFilterState.routePath}
1777
+ placeholder="e.g. /api/ttm/courses/12345"
1778
+ onChange={(e) => {
1779
+ advancedFilterState.courseName = (
1780
+ (e.target.value)
1781
+ .trim()
1782
+ );
1783
+ dispatch({
1784
+ type: ActionType.UpdateAdvancedFilterState,
1785
+ advancedFilterState,
1786
+ });
1787
+ }}
1788
+ />
1789
+ </div>
1455
1790
 
1456
- {/* Route template */}
1457
- <div className="input-group mb-2">
1458
- <span className="input-group-text">
1459
- Server Route Template
1460
- </span>
1461
- <input
1462
- type="text"
1463
- className="form-control"
1464
- aria-label="query for server route template"
1465
- value={advancedFilterState.routeTemplate}
1466
- placeholder="e.g. /api/ttm/courses/:courseId"
1467
- onChange={(e) => {
1468
- advancedFilterState.courseName = (
1469
- (e.target.value)
1470
- .trim()
1471
- );
1472
- dispatch({
1473
- type: ActionType.UpdateAdvancedFilterState,
1474
- advancedFilterState,
1475
- });
1476
- }}
1477
- />
1478
- </div>
1791
+ {/* Route template */}
1792
+ <div className="input-group mb-2">
1793
+ <span className="input-group-text">
1794
+ Server Route Template
1795
+ </span>
1796
+ <input
1797
+ type="text"
1798
+ className="form-control"
1799
+ aria-label="query for server route template"
1800
+ value={advancedFilterState.routeTemplate}
1801
+ placeholder="e.g. /api/ttm/courses/:courseId"
1802
+ onChange={(e) => {
1803
+ advancedFilterState.courseName = (
1804
+ (e.target.value)
1805
+ .trim()
1806
+ );
1807
+ dispatch({
1808
+ type: ActionType.UpdateAdvancedFilterState,
1809
+ advancedFilterState,
1810
+ });
1811
+ }}
1812
+ />
1813
+ </div>
1814
+ </div>
1815
+ )}
1479
1816
  </TabBox>
1480
1817
  </>
1481
1818
  );
@@ -1487,7 +1824,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1487
1824
  <>
1488
1825
  {filterToggles}
1489
1826
  {filterDrawer && (
1490
- <Drawer>
1827
+ <Drawer customBackgroundColor="#eee">
1491
1828
  {filterDrawer}
1492
1829
  </Drawer>
1493
1830
  )}
@@ -1548,7 +1885,10 @@ const LogReviewer: React.FC<Props> = (props) => {
1548
1885
  contextFilterState[log.context] === false
1549
1886
  // None of the subcontexts are selected
1550
1887
  || (
1551
- Object.values(contextFilterState[log.context] ?? {})
1888
+ // Has subcontexts
1889
+ typeof contextFilterState[log.context] !== 'boolean'
1890
+ // None of the subcontexts are selected
1891
+ && Object.values(contextFilterState[log.context] ?? {})
1552
1892
  .every((isSelected) => {
1553
1893
  return !isSelected;
1554
1894
  })
@@ -1559,8 +1899,10 @@ const LogReviewer: React.FC<Props> = (props) => {
1559
1899
 
1560
1900
  // Subcontext doesn't match
1561
1901
  if (
1902
+ // Log context is not "uncategorized" (no point in further filters)
1903
+ log.context !== LogBuiltInMetadata.Context.Uncategorized
1562
1904
  // Log has a subcontext
1563
- log.subcontext
1905
+ && log.subcontext
1564
1906
  // Context has subcontexts
1565
1907
  && (
1566
1908
  contextFilterState[log.context]
@@ -1577,14 +1919,16 @@ const LogReviewer: React.FC<Props> = (props) => {
1577
1919
 
1578
1920
  // No tags match
1579
1921
  if (
1580
- // Log has at least one tag
1581
- log.tags.length > 0
1582
- // No tags match
1583
- && (
1584
- log.tags.every((tag) => {
1585
- return !tagFilterState[tag];
1922
+ // At least one tag is required
1923
+ Object.values(tagFilterState)
1924
+ .filter((isSelected) => {
1925
+ return isSelected;
1586
1926
  })
1587
- )
1927
+ .length > 0
1928
+ // No tags match
1929
+ && log.tags.every((tag) => {
1930
+ return !tagFilterState[tag];
1931
+ })
1588
1932
  ) {
1589
1933
  return;
1590
1934
  }
@@ -1766,7 +2110,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1766
2110
  // Device info exists
1767
2111
  && log.device
1768
2112
  // Mobile filter doesn't match
1769
- && (advancedFilterState.isMobile === log.device.isMobile)
2113
+ && (advancedFilterState.isMobile !== log.device.isMobile)
1770
2114
  ) {
1771
2115
  return;
1772
2116
  }
@@ -1819,228 +2163,42 @@ const LogReviewer: React.FC<Props> = (props) => {
1819
2163
  /* Data */
1820
2164
  /*----------------------------------------*/
1821
2165
 
1822
- // Create data table
1823
- const columns: IntelliTableColumn[] = [
1824
- {
1825
- title: 'First Name',
1826
- param: 'userFirstName',
1827
- type: ParamType.String,
1828
- },
1829
- {
1830
- title: 'Last Name',
1831
- param: 'userLastName',
1832
- type: ParamType.String,
1833
- },
1834
- {
1835
- title: 'Email',
1836
- param: 'userEmail',
1837
- type: ParamType.String,
1838
- },
1839
- {
1840
- title: 'Canvas Id',
1841
- param: 'userId',
1842
- type: ParamType.Int,
1843
- },
1844
- {
1845
- title: 'Student',
1846
- param: 'isLearner',
1847
- type: ParamType.Boolean,
1848
- },
1849
- {
1850
- title: 'Teaching Staff',
1851
- param: 'isTTM',
1852
- type: ParamType.Boolean,
1853
- startsHidden: true,
1854
- },
1855
- {
1856
- title: 'Admin',
1857
- param: 'isAdmin',
1858
- type: ParamType.Boolean,
1859
- startsHidden: true,
1860
- },
1861
- {
1862
- title: 'Course Canvas Id',
1863
- param: 'courseId',
1864
- type: ParamType.Int,
1865
- startsHidden: true,
1866
- },
1867
- {
1868
- title: 'Course Name',
1869
- param: 'courseName',
1870
- type: ParamType.String,
1871
- },
1872
- {
1873
- title: 'Browser Name',
1874
- param: 'browser.name',
1875
- type: ParamType.String,
1876
- startsHidden: true,
1877
- },
1878
- {
1879
- title: 'Browser Version',
1880
- param: 'browser.version',
1881
- type: ParamType.String,
1882
- startsHidden: true,
1883
- },
1884
- {
1885
- title: 'OS',
1886
- param: 'device.os',
1887
- type: ParamType.String,
1888
- startsHidden: true,
1889
- },
1890
- {
1891
- title: 'Mobile',
1892
- param: 'device.isMobile',
1893
- type: ParamType.Boolean,
1894
- startsHidden: true,
1895
- },
1896
- {
1897
- title: 'Year',
1898
- param: 'year',
1899
- type: ParamType.Int,
1900
- },
1901
- {
1902
- title: 'Month',
1903
- param: 'month',
1904
- type: ParamType.Int,
1905
- },
1906
- {
1907
- title: 'Day',
1908
- param: 'day',
1909
- type: ParamType.Int,
1910
- },
1911
- {
1912
- title: 'Hour',
1913
- param: 'hour',
1914
- type: ParamType.Int,
1915
- },
1916
- {
1917
- title: 'Minute',
1918
- param: 'minute',
1919
- type: ParamType.Int,
1920
- startsHidden: true,
1921
- },
1922
- {
1923
- title: 'Timestamp',
1924
- param: 'timestamp',
1925
- type: ParamType.Int,
1926
- startsHidden: true,
1927
- },
1928
- {
1929
- title: 'Context',
1930
- param: 'context',
1931
- type: ParamType.String,
1932
- },
1933
- {
1934
- title: 'Subcontext',
1935
- param: 'subcontext',
1936
- type: ParamType.String,
1937
- },
1938
- {
1939
- title: 'Tags',
1940
- param: 'tags',
1941
- type: ParamType.JSON,
1942
- startsHidden: true,
1943
- },
1944
- {
1945
- title: 'Log Level',
1946
- param: 'level',
1947
- type: ParamType.String,
1948
- startsHidden: true,
1949
- },
1950
- {
1951
- title: 'Metadata',
1952
- param: 'metadata',
1953
- type: ParamType.JSON,
1954
- startsHidden: true,
1955
- },
1956
- {
1957
- title: 'Source',
1958
- param: 'source',
1959
- type: ParamType.String,
1960
- },
1961
- {
1962
- title: 'Server Route Path',
1963
- param: 'routePath',
1964
- type: ParamType.String,
1965
- startsHidden: true,
1966
- },
1967
- {
1968
- title: 'Server Route Template',
1969
- param: 'routeTemplate',
1970
- type: ParamType.String,
1971
- startsHidden: true,
1972
- },
1973
- {
1974
- title: 'Type',
1975
- param: 'type',
1976
- type: ParamType.String,
1977
- },
1978
- {
1979
- title: 'Error Message',
1980
- param: 'errorMessage',
1981
- type: ParamType.String,
1982
- startsHidden: true,
1983
- },
1984
- {
1985
- title: 'Error Code',
1986
- param: 'errorCode',
1987
- type: ParamType.String,
1988
- startsHidden: true,
1989
- },
1990
- {
1991
- title: 'Error Stack',
1992
- param: 'errorStack',
1993
- type: ParamType.String,
1994
- startsHidden: true,
1995
- },
1996
- {
1997
- title: 'Action Target',
1998
- param: 'target',
1999
- type: ParamType.String,
2000
- startsHidden: true,
2001
- },
2002
- {
2003
- title: 'Action Type',
2004
- param: 'action',
2005
- type: ParamType.String,
2006
- startsHidden: true,
2007
- },
2008
- ];
2009
-
2010
- // Create intelliTable
2011
- const dataTable = (
2166
+ // Nothing to show notice
2167
+ const noLogsNotice = (
2012
2168
  logs.length === 0
2013
2169
  ? (
2014
- <>
2015
- <h3 className="m-0">
2016
- Matching Logs:
2017
- </h3>
2018
- <div className="alert alert-warning text-center">
2019
- <h4 className="m-1">
2020
- No Logs to Show
2021
- </h4>
2022
- <div>
2023
- Either your filters are too strict or no matching logs have been
2024
- created yet.
2025
- </div>
2170
+ <div className="alert alert-warning text-center mt-2">
2171
+ <h4 className="m-1">
2172
+ No Logs to Show
2173
+ </h4>
2174
+ <div>
2175
+ Either your filters are too strict or no matching logs have been
2176
+ created yet.
2026
2177
  </div>
2027
- </>
2028
- )
2029
- : (
2030
- <IntelliTable
2031
- title="Matching Logs:"
2032
- id="logs"
2033
- data={logs}
2034
- columns={columns}
2035
- />
2178
+ </div>
2036
2179
  )
2180
+ : undefined
2181
+ );
2182
+
2183
+ // Create intelliTable
2184
+ const dataTable = (
2185
+ <IntelliTable
2186
+ title="Matching Logs:"
2187
+ csvName={`Logs from ${getHumanReadableDate()}`}
2188
+ id="logs"
2189
+ data={logs}
2190
+ columns={columns}
2191
+ />
2037
2192
  );
2038
2193
 
2039
2194
  // Main body
2040
2195
  body = (
2041
2196
  <>
2042
2197
  {filters}
2043
- {dataTable}
2198
+ <div className="mt-2">
2199
+ {dataTable}
2200
+ {noLogsNotice}
2201
+ </div>
2044
2202
  </>
2045
2203
  );
2046
2204
  }
@@ -2059,16 +2217,18 @@ const LogReviewer: React.FC<Props> = (props) => {
2059
2217
  Log Review Dashboard
2060
2218
  </h3>
2061
2219
  </div>
2062
- <button
2063
- type="button"
2064
- className="LogReviewer-header-close-button btn btn-dark btn-lg"
2065
- aria-label="close log reviewer panel"
2066
- onClick={onClose}
2067
- >
2068
- <FontAwesomeIcon
2069
- icon={faTimes}
2070
- />
2071
- </button>
2220
+ <div style={{ width: 0 }}>
2221
+ <button
2222
+ type="button"
2223
+ className="LogReviewer-header-close-button btn btn-dark btn-lg pe-0"
2224
+ aria-label="close log reviewer panel"
2225
+ onClick={onClose}
2226
+ >
2227
+ <FontAwesomeIcon
2228
+ icon={faTimes}
2229
+ />
2230
+ </button>
2231
+ </div>
2072
2232
  </div>
2073
2233
  <div className="LogReviewer-contents">
2074
2234
  {body}