dce-reactkit 3.2.2-beta.8 → 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 (50) hide show
  1. package/dist/cjs/index.js +743 -440
  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/components/ItemPicker/index.d.ts +1 -0
  7. package/dist/cjs/types/dynamicConstants/DynamicWord.d.ts +13 -0
  8. package/dist/cjs/types/index.d.ts +2 -1
  9. package/dist/cjs/types/types/LogAction.d.ts +16 -16
  10. package/dist/cjs/types/types/LogBuiltInMetadata.d.ts +1 -1
  11. package/dist/cjs/types/types/LogMetadataContextMap.d.ts +11 -0
  12. package/dist/cjs/types/types/LogMetadataTagMap.d.ts +8 -0
  13. package/dist/cjs/types/types/LogMetadataTargetMap.d.ts +8 -0
  14. package/dist/cjs/types/types/LogMetadataType.d.ts +6 -12
  15. package/dist/esm/index.js +744 -442
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/types/components/Drawer.d.ts +1 -0
  18. package/dist/esm/types/components/IntelliTable.d.ts +1 -0
  19. package/dist/esm/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  20. package/dist/esm/types/components/ItemPicker/index.d.ts +1 -0
  21. package/dist/esm/types/dynamicConstants/DynamicWord.d.ts +13 -0
  22. package/dist/esm/types/index.d.ts +2 -1
  23. package/dist/esm/types/types/LogAction.d.ts +16 -16
  24. package/dist/esm/types/types/LogBuiltInMetadata.d.ts +1 -1
  25. package/dist/esm/types/types/LogMetadataContextMap.d.ts +11 -0
  26. package/dist/esm/types/types/LogMetadataTagMap.d.ts +8 -0
  27. package/dist/esm/types/types/LogMetadataTargetMap.d.ts +8 -0
  28. package/dist/esm/types/types/LogMetadataType.d.ts +6 -12
  29. package/dist/index.d.ts +64 -30
  30. package/package.json +1 -1
  31. package/src/components/ButtonInputGroup.tsx +1 -1
  32. package/src/components/CSVDownloadButton.tsx +2 -2
  33. package/src/components/Drawer.tsx +9 -1
  34. package/src/components/IntelliTable.tsx +178 -23
  35. package/src/components/ItemPicker/NestableItemList.tsx +32 -14
  36. package/src/components/ItemPicker/index.tsx +4 -0
  37. package/src/components/LogReviewer.tsx +588 -418
  38. package/src/components/SimpleDateChooser.tsx +30 -25
  39. package/src/dynamicConstants/DynamicWord.tsx +55 -0
  40. package/src/helpers/genCSV.ts +22 -4
  41. package/src/helpers/genRouteHandler.ts +2 -2
  42. package/src/helpers/logClientEvent.tsx +1 -1
  43. package/src/index.ts +5 -0
  44. package/src/server/initServer.ts +10 -3
  45. package/src/types/LogAction.ts +16 -16
  46. package/src/types/LogBuiltInMetadata.ts +5 -5
  47. package/src/types/LogMetadataContextMap.ts +15 -0
  48. package/src/types/LogMetadataTagMap.ts +9 -0
  49. package/src/types/LogMetadataTargetMap.ts +9 -0
  50. 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,14 +247,214 @@ 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 {
258
+ border: 0 !important;
259
+ background-color: transparent !important;
260
+ padding-top: 0 !important;
261
+ padding-bottom: 0 !important;
262
+ margin: 0 !important;
250
263
  color: #000 !important;
251
264
  }
252
265
  `;
253
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
+
254
458
  /*------------------------------------------------------------------------*/
255
459
  /* Static Functions */
256
460
  /*------------------------------------------------------------------------*/
@@ -494,6 +698,36 @@ const LogReviewer: React.FC<Props> = (props) => {
494
698
  onClose,
495
699
  } = props;
496
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
+
497
731
  /* -------------- State ------------- */
498
732
 
499
733
  // Create initial date filter state
@@ -515,25 +749,30 @@ const LogReviewer: React.FC<Props> = (props) => {
515
749
 
516
750
  // Create initial context filter state
517
751
  const initContextFilterState: ContextFilterState = {};
518
- Object.keys(LogMetadata.Context ?? {}).forEach((context) => {
519
- const contextValue = (LogMetadata.Context ?? {})[context];
752
+ Object.keys(contextMap).forEach((context) => {
753
+ const contextValue = contextMap[context];
520
754
  if (typeof contextValue === 'string') {
521
- // Case: no subcontexts
755
+ // Case: no subcontexts, init as checked
522
756
  initContextFilterState[contextValue] = true;
523
757
  } else {
524
758
  // Case: subcontexts exist
525
- initContextFilterState[contextValue._] = {};
526
- Object.values((LogMetadata.Context ?? {})[context]).forEach((subcontext) => {
527
- const subcontextValue = contextValue[subcontext];
528
- (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;
529
768
  });
530
769
  }
531
770
  });
532
771
 
533
772
  // Create initial tag filter state
534
773
  const initTagFilterState: TagFilterState = {};
535
- Object.values(LogMetadata.Tag ?? {}).forEach((tagValue) => {
536
- initTagFilterState[tagValue] = true;
774
+ Object.keys(LogMetadata.Tag ?? {}).forEach((tag) => {
775
+ initTagFilterState[tag] = false;
537
776
  });
538
777
 
539
778
  // Create advanced filter state
@@ -561,7 +800,7 @@ const LogReviewer: React.FC<Props> = (props) => {
561
800
  target: {},
562
801
  action: {},
563
802
  };
564
- Object.values(LogMetadata.Target ?? {}).forEach((target) => {
803
+ Object.values(targetMap).forEach((target) => {
565
804
  initActionErrorFilterState.target[target] = true;
566
805
  });
567
806
  Object.values(LogAction).forEach((action) => {
@@ -617,18 +856,23 @@ const LogReviewer: React.FC<Props> = (props) => {
617
856
  let month = newDateFilterState.startDate.month;
618
857
  while (
619
858
  // Earlier year
620
- (year <= newDateFilterState.endDate.year)
859
+ (year < newDateFilterState.endDate.year)
621
860
  // Current year but included month
622
861
  || (
623
862
  year === newDateFilterState.endDate.year
624
863
  && month <= newDateFilterState.endDate.month
625
864
  )
626
865
  ) {
627
- // Add to list
628
- toLoad.push({
629
- year,
630
- month,
631
- });
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
+ }
632
876
 
633
877
  // Increment
634
878
  month += 1;
@@ -744,16 +988,16 @@ const LogReviewer: React.FC<Props> = (props) => {
744
988
 
745
989
  // Filter toggle
746
990
  const filterToggles = (
747
- <div className="LogReviewer-filter-toggles mb-2">
991
+ <div className="LogReviewer-filter-toggles">
748
992
  <h3 className="m-0">
749
993
  Filters:
750
994
  </h3>
751
- <div className="LogReviewer-filter-toggle-buttons">
995
+ <div className="LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0">
752
996
  {/* Date */}
753
997
  <button
754
998
  type="button"
755
999
  id="LogReviewer-toggle-date-filter-drawer"
756
- className={`btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'secondary'} me-2`}
1000
+ className={`btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
757
1001
  aria-label="toggle date filter drawer"
758
1002
  onClick={() => {
759
1003
  dispatch({
@@ -772,7 +1016,7 @@ const LogReviewer: React.FC<Props> = (props) => {
772
1016
  <button
773
1017
  type="button"
774
1018
  id="LogReviewer-toggle-context-filter-drawer"
775
- className={`btn btn-${FilterDrawer.Context === expandedFilterDrawer ? 'warning' : 'secondary'} me-2`}
1019
+ className={`btn btn-${FilterDrawer.Context === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
776
1020
  aria-label="toggle context filter drawer"
777
1021
  onClick={() => {
778
1022
  dispatch({
@@ -793,7 +1037,7 @@ const LogReviewer: React.FC<Props> = (props) => {
793
1037
  <button
794
1038
  type="button"
795
1039
  id="LogReviewer-toggle-tag-filter-drawer"
796
- className={`btn btn-${FilterDrawer.Tag === expandedFilterDrawer ? 'warning' : 'secondary'} me-2`}
1040
+ className={`btn btn-${FilterDrawer.Tag === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
797
1041
  aria-label="toggle tag filter drawer"
798
1042
  onClick={() => {
799
1043
  dispatch({
@@ -813,7 +1057,7 @@ const LogReviewer: React.FC<Props> = (props) => {
813
1057
  <button
814
1058
  type="button"
815
1059
  id="LogReviewer-toggle-action-filter-drawer"
816
- className={`btn btn-${FilterDrawer.Action === expandedFilterDrawer ? 'warning' : 'secondary'} me-2`}
1060
+ className={`btn btn-${FilterDrawer.Action === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
817
1061
  aria-label="toggle action and error filter drawer"
818
1062
  onClick={() => {
819
1063
  dispatch({
@@ -832,7 +1076,7 @@ const LogReviewer: React.FC<Props> = (props) => {
832
1076
  <button
833
1077
  type="button"
834
1078
  id="LogReviewer-toggle-advanced-filter-drawer"
835
- className={`btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'secondary'}`}
1079
+ className={`btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
836
1080
  aria-label="toggle advanced filter drawer"
837
1081
  onClick={() => {
838
1082
  dispatch({
@@ -847,6 +1091,29 @@ const LogReviewer: React.FC<Props> = (props) => {
847
1091
  />
848
1092
  Advanced
849
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>
850
1117
  </div>
851
1118
  </div>
852
1119
  );
@@ -863,14 +1130,11 @@ const LogReviewer: React.FC<Props> = (props) => {
863
1130
  year={dateFilterState.startDate.year}
864
1131
  month={dateFilterState.startDate.month}
865
1132
  day={dateFilterState.startDate.day}
1133
+ chooseFromPast
1134
+ numMonthsToShow={36}
866
1135
  onChange={(month, day, year) => {
867
- dispatch({
868
- type: ActionType.UpdateDateFilterState,
869
- dateFilterState: {
870
- ...dateFilterState,
871
- startDate: { month, day, year },
872
- },
873
- });
1136
+ dateFilterState.startDate = { month, day, year };
1137
+ handleDateRangeUpdated(dateFilterState);
874
1138
  }}
875
1139
  />
876
1140
  {' '}
@@ -882,59 +1146,92 @@ const LogReviewer: React.FC<Props> = (props) => {
882
1146
  year={dateFilterState.endDate.year}
883
1147
  month={dateFilterState.endDate.month}
884
1148
  day={dateFilterState.endDate.day}
1149
+ chooseFromPast
1150
+ numMonthsToShow={12}
885
1151
  onChange={(month, day, year) => {
886
- dispatch({
887
- type: ActionType.UpdateDateFilterState,
888
- dateFilterState: {
889
- ...dateFilterState,
890
- endDate: { month, day, year },
891
- },
892
- });
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);
893
1171
  }}
894
1172
  />
895
1173
  </TabBox>
896
1174
  );
897
1175
  } else if (expandedFilterDrawer === FilterDrawer.Context) {
898
1176
  // Create item picker items
899
- const pickableItems: PickableItem[] = (
900
- Object.keys(LogMetadata.Context ?? {})
901
- .map((context) => {
902
- const value = (LogMetadata.Context ?? {})[context];
903
- if (typeof value === 'string') {
904
- // No subcategories
905
- const item: PickableItem = {
906
- id: context,
907
- name: genHumanReadableName(context),
908
- isGroup: false,
909
- checked: !!contextFilterState[context],
910
- };
911
- return item;
912
- }
913
-
914
- // Has subcategories
915
- const children: PickableItem[] = (
916
- Object.keys(value)
917
- .filter((subcontext) => {
918
- return subcontext !== '_'
919
- })
920
- .map((subcontext) => {
921
- return {
922
- id: `${context}-${subcontext}`,
923
- name: genHumanReadableName(subcontext),
924
- isGroup: false,
925
- checked: !!value[subcontext],
926
- };
927
- })
928
- );
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
929
1189
  const item: PickableItem = {
930
1190
  id: context,
931
- name: context,
932
- isGroup: true,
933
- children,
1191
+ name: genHumanReadableName(context),
1192
+ isGroup: false,
1193
+ checked: !!contextFilterState[context],
934
1194
  };
935
- return item;
936
- })
937
- );
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);
938
1235
 
939
1236
  // Create filter UI
940
1237
  filterDrawer = (
@@ -946,11 +1243,29 @@ const LogReviewer: React.FC<Props> = (props) => {
946
1243
  updatedItems.forEach((pickableItem) => {
947
1244
  if (pickableItem.isGroup) {
948
1245
  // Has subcontexts
949
- pickableItem.children.forEach((subcontextItem) => {
950
- (contextFilterState as any)[pickableItem.id][subcontextItem.id] = (
951
- (subcontextItem as any).checked
952
- );
953
- });
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
+ }
954
1269
  } else {
955
1270
  // No subcontexts
956
1271
  (contextFilterState as any)[pickableItem.id] = (
@@ -969,23 +1284,35 @@ const LogReviewer: React.FC<Props> = (props) => {
969
1284
  // Create filter UI
970
1285
  filterDrawer = (
971
1286
  <TabBox title="Tags">
972
- {
973
- Object.keys(tagFilterState)
974
- .map((tag, i) => {
975
- const description = genHumanReadableName(tag);
976
- return (
977
- <CheckboxButton
978
- id={`LogReviewer-tag-${tag}-checkbox`}
979
- text={description}
980
- ariaLabel={`include logs tagged with "${description}" in results`}
981
- noMarginOnRight={i === Object.keys(tagFilterState).length - 1}
982
- onChanged={(checked) => {
983
- tagFilterState[tag] = checked;
984
- }}
985
- />
986
- );
987
- })
988
- }
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>
989
1316
  </TabBox>
990
1317
  );
991
1318
  } else if (expandedFilterDrawer === FilterDrawer.Action) {
@@ -1047,58 +1374,60 @@ const LogReviewer: React.FC<Props> = (props) => {
1047
1374
  label="Action"
1048
1375
  className="mb-2"
1049
1376
  >
1050
- {
1051
- Object.keys(LogAction)
1052
- .map((action, i) => {
1053
- const description = genHumanReadableName(action);
1054
- return (
1055
- <CheckboxButton
1056
- id={`LogReviewer-action-${action}-checkbox`}
1057
- text={description}
1058
- ariaLabel={`include logs with action type "${description}" in results`}
1059
- noMarginOnRight={i === Object.keys(LogAction).length - 1}
1060
- onChanged={(checked) => {
1061
- actionErrorFilterState.action[action] = checked;
1062
- dispatch({
1063
- type: ActionType.UpdateActionErrorFilterState,
1064
- actionErrorFilterState,
1065
- });
1066
- }}
1067
- />
1068
- );
1069
- })
1070
- }
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>
1071
1402
  </ButtonInputGroup>
1072
1403
  {/* Target */}
1073
1404
  <ButtonInputGroup label="Target">
1074
- {/* Nothing here */}
1075
- {(Object.keys(LogMetadata.Target ?? {})) && (
1076
- <div>
1077
- This app does not have any targets yet.
1078
- </div>
1079
- )}
1080
1405
  {/* List of targets */}
1081
- {
1082
- Object.keys(LogMetadata.Target ?? {})
1083
- .map((target, i) => {
1084
- const description = genHumanReadableName(target);
1085
- return (
1086
- <CheckboxButton
1087
- id={`LogReviewer-target-${target}-checkbox`}
1088
- text={description}
1089
- ariaLabel={`include logs with target "${description}" in results`}
1090
- onChanged={(checked) => {
1091
- actionErrorFilterState.target[target] = checked;
1092
- dispatch({
1093
- type: ActionType.UpdateActionErrorFilterState,
1094
- actionErrorFilterState,
1095
- });
1096
- }}
1097
- noMarginOnRight={i === Object.keys(LogMetadata.Target ?? {}).length - 1}
1098
- />
1099
- );
1100
- })
1101
- }
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>
1102
1431
  </ButtonInputGroup>
1103
1432
  </TabBox>
1104
1433
  )
@@ -1120,6 +1449,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1120
1449
  className="form-control"
1121
1450
  aria-label="query for error message"
1122
1451
  value={actionErrorFilterState.errorMessage}
1452
+ placeholder="e.g. undefined is not a function"
1123
1453
  onChange={(e) => {
1124
1454
  actionErrorFilterState.errorMessage = e.target.value;
1125
1455
  dispatch({
@@ -1138,7 +1468,8 @@ const LogReviewer: React.FC<Props> = (props) => {
1138
1468
  type="text"
1139
1469
  className="form-control"
1140
1470
  aria-label="query for error code"
1141
- value={actionErrorFilterState.errorMessage}
1471
+ value={actionErrorFilterState.errorCode}
1472
+ placeholder="e.g. GC22"
1142
1473
  onChange={(e) => {
1143
1474
  actionErrorFilterState.errorCode = (
1144
1475
  (e.target.value)
@@ -1162,7 +1493,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1162
1493
  filterDrawer = (
1163
1494
  <>
1164
1495
  {/* User Info */}
1165
- <TabBox title="User Info">
1496
+ <TabBox title="User">
1166
1497
  {/* First Name */}
1167
1498
  <div className="input-group mb-2">
1168
1499
  <span className="input-group-text">
@@ -1173,6 +1504,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1173
1504
  className="form-control"
1174
1505
  aria-label="query for user first name"
1175
1506
  value={advancedFilterState.userFirstName}
1507
+ placeholder="e.g. Divardo"
1176
1508
  onChange={(e) => {
1177
1509
  advancedFilterState.userFirstName = e.target.value;
1178
1510
  dispatch({
@@ -1192,6 +1524,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1192
1524
  className="form-control"
1193
1525
  aria-label="query for user last name"
1194
1526
  value={advancedFilterState.userLastName}
1527
+ placeholder="e.g. Calicci"
1195
1528
  onChange={(e) => {
1196
1529
  advancedFilterState.userLastName = e.target.value;
1197
1530
  dispatch({
@@ -1211,6 +1544,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1211
1544
  className="form-control"
1212
1545
  aria-label="query for user email"
1213
1546
  value={advancedFilterState.userEmail}
1547
+ placeholder="e.g. calicci@fas.harvard.edu"
1214
1548
  onChange={(e) => {
1215
1549
  advancedFilterState.userEmail = (
1216
1550
  (e.target.value)
@@ -1233,6 +1567,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1233
1567
  className="form-control"
1234
1568
  aria-label="query for user canvas id"
1235
1569
  value={advancedFilterState.userId}
1570
+ placeholder="e.g. 104985"
1236
1571
  onChange={(e) => {
1237
1572
  const { value } = e.target;
1238
1573
  // Only update if value contains only numbers
@@ -1291,7 +1626,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1291
1626
  </TabBox>
1292
1627
 
1293
1628
  {/* Course Info */}
1294
- <TabBox title="Course Info">
1629
+ <TabBox title="Course">
1295
1630
  {/* Name */}
1296
1631
  <div className="input-group mb-2">
1297
1632
  <span className="input-group-text">
@@ -1302,6 +1637,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1302
1637
  className="form-control"
1303
1638
  aria-label="query for course name"
1304
1639
  value={advancedFilterState.courseName}
1640
+ placeholder="e.g. GLC 200"
1305
1641
  onChange={(e) => {
1306
1642
  advancedFilterState.courseName = e.target.value;
1307
1643
  dispatch({
@@ -1321,6 +1657,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1321
1657
  className="form-control"
1322
1658
  aria-label="query for course canvas id"
1323
1659
  value={advancedFilterState.courseId}
1660
+ placeholder="e.g. 15948"
1324
1661
  onChange={(e) => {
1325
1662
  const { value } = e.target;
1326
1663
  // Only update if value contains only numbers
@@ -1340,7 +1677,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1340
1677
  </TabBox>
1341
1678
 
1342
1679
  {/* Device Info */}
1343
- <TabBox title="Device Info">
1680
+ <TabBox title="Device">
1344
1681
  <ButtonInputGroup label="Device Type">
1345
1682
  <RadioButton
1346
1683
  text="All Devices"
@@ -1424,53 +1761,58 @@ const LogReviewer: React.FC<Props> = (props) => {
1424
1761
  />
1425
1762
  </ButtonInputGroup>
1426
1763
 
1427
- {/* Route path */}
1428
- <div className="input-group mb-2">
1429
- <span className="input-group-text">
1430
- Server Route Path
1431
- </span>
1432
- <input
1433
- type="text"
1434
- className="form-control"
1435
- aria-label="query for server route path"
1436
- placeholder="e.g. /api/ttm/courses/12345"
1437
- value={advancedFilterState.routePath}
1438
- onChange={(e) => {
1439
- advancedFilterState.courseName = (
1440
- (e.target.value)
1441
- .trim()
1442
- );
1443
- dispatch({
1444
- type: ActionType.UpdateAdvancedFilterState,
1445
- advancedFilterState,
1446
- });
1447
- }}
1448
- />
1449
- </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>
1450
1790
 
1451
- {/* Route template */}
1452
- <div className="input-group mb-2">
1453
- <span className="input-group-text">
1454
- Server Route Template
1455
- </span>
1456
- <input
1457
- type="text"
1458
- className="form-control"
1459
- aria-label="query for server route template"
1460
- value={advancedFilterState.routeTemplate}
1461
- placeholder="e.g. /api/ttm/courses/:courseId"
1462
- onChange={(e) => {
1463
- advancedFilterState.courseName = (
1464
- (e.target.value)
1465
- .trim()
1466
- );
1467
- dispatch({
1468
- type: ActionType.UpdateAdvancedFilterState,
1469
- advancedFilterState,
1470
- });
1471
- }}
1472
- />
1473
- </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
+ )}
1474
1816
  </TabBox>
1475
1817
  </>
1476
1818
  );
@@ -1482,7 +1824,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1482
1824
  <>
1483
1825
  {filterToggles}
1484
1826
  {filterDrawer && (
1485
- <Drawer>
1827
+ <Drawer customBackgroundColor="#eee">
1486
1828
  {filterDrawer}
1487
1829
  </Drawer>
1488
1830
  )}
@@ -1543,7 +1885,10 @@ const LogReviewer: React.FC<Props> = (props) => {
1543
1885
  contextFilterState[log.context] === false
1544
1886
  // None of the subcontexts are selected
1545
1887
  || (
1546
- 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] ?? {})
1547
1892
  .every((isSelected) => {
1548
1893
  return !isSelected;
1549
1894
  })
@@ -1554,8 +1899,10 @@ const LogReviewer: React.FC<Props> = (props) => {
1554
1899
 
1555
1900
  // Subcontext doesn't match
1556
1901
  if (
1902
+ // Log context is not "uncategorized" (no point in further filters)
1903
+ log.context !== LogBuiltInMetadata.Context.Uncategorized
1557
1904
  // Log has a subcontext
1558
- log.subcontext
1905
+ && log.subcontext
1559
1906
  // Context has subcontexts
1560
1907
  && (
1561
1908
  contextFilterState[log.context]
@@ -1572,14 +1919,16 @@ const LogReviewer: React.FC<Props> = (props) => {
1572
1919
 
1573
1920
  // No tags match
1574
1921
  if (
1575
- // Log has at least one tag
1576
- log.tags.length > 0
1577
- // No tags match
1578
- && (
1579
- log.tags.every((tag) => {
1580
- return !tagFilterState[tag];
1922
+ // At least one tag is required
1923
+ Object.values(tagFilterState)
1924
+ .filter((isSelected) => {
1925
+ return isSelected;
1581
1926
  })
1582
- )
1927
+ .length > 0
1928
+ // No tags match
1929
+ && log.tags.every((tag) => {
1930
+ return !tagFilterState[tag];
1931
+ })
1583
1932
  ) {
1584
1933
  return;
1585
1934
  }
@@ -1761,7 +2110,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1761
2110
  // Device info exists
1762
2111
  && log.device
1763
2112
  // Mobile filter doesn't match
1764
- && (advancedFilterState.isMobile === log.device.isMobile)
2113
+ && (advancedFilterState.isMobile !== log.device.isMobile)
1765
2114
  ) {
1766
2115
  return;
1767
2116
  }
@@ -1814,199 +2163,11 @@ const LogReviewer: React.FC<Props> = (props) => {
1814
2163
  /* Data */
1815
2164
  /*----------------------------------------*/
1816
2165
 
1817
- // Create data table
1818
- const columns: IntelliTableColumn[] = [
1819
- {
1820
- title: 'First Name',
1821
- param: 'userFirstName',
1822
- type: ParamType.String,
1823
- },
1824
- {
1825
- title: 'Last Name',
1826
- param: 'userLastName',
1827
- type: ParamType.String,
1828
- },
1829
- {
1830
- title: 'Email',
1831
- param: 'userEmail',
1832
- type: ParamType.String,
1833
- },
1834
- {
1835
- title: 'Canvas Id',
1836
- param: 'userId',
1837
- type: ParamType.Int,
1838
- },
1839
- {
1840
- title: 'Student',
1841
- param: 'isLearner',
1842
- type: ParamType.Boolean,
1843
- },
1844
- {
1845
- title: 'Teaching Staff',
1846
- param: 'isTTM',
1847
- type: ParamType.Boolean,
1848
- startsHidden: true,
1849
- },
1850
- {
1851
- title: 'Admin',
1852
- param: 'isAdmin',
1853
- type: ParamType.Boolean,
1854
- startsHidden: true,
1855
- },
1856
- {
1857
- title: 'Course Canvas Id',
1858
- param: 'courseId',
1859
- type: ParamType.Int,
1860
- startsHidden: true,
1861
- },
1862
- {
1863
- title: 'Course Name',
1864
- param: 'courseName',
1865
- type: ParamType.String,
1866
- },
1867
- {
1868
- title: 'Browser Name',
1869
- param: 'browser.name',
1870
- type: ParamType.String,
1871
- startsHidden: true,
1872
- },
1873
- {
1874
- title: 'Browser Version',
1875
- param: 'browser.version',
1876
- type: ParamType.String,
1877
- startsHidden: true,
1878
- },
1879
- {
1880
- title: 'OS',
1881
- param: 'device.os',
1882
- type: ParamType.String,
1883
- startsHidden: true,
1884
- },
1885
- {
1886
- title: 'Mobile',
1887
- param: 'device.isMobile',
1888
- type: ParamType.Boolean,
1889
- startsHidden: true,
1890
- },
1891
- {
1892
- title: 'Year',
1893
- param: 'year',
1894
- type: ParamType.Int,
1895
- },
1896
- {
1897
- title: 'Month',
1898
- param: 'month',
1899
- type: ParamType.Int,
1900
- },
1901
- {
1902
- title: 'Day',
1903
- param: 'day',
1904
- type: ParamType.Int,
1905
- },
1906
- {
1907
- title: 'Hour',
1908
- param: 'hour',
1909
- type: ParamType.Int,
1910
- },
1911
- {
1912
- title: 'Minute',
1913
- param: 'minute',
1914
- type: ParamType.Int,
1915
- startsHidden: true,
1916
- },
1917
- {
1918
- title: 'Timestamp',
1919
- param: 'timestamp',
1920
- type: ParamType.Int,
1921
- startsHidden: true,
1922
- },
1923
- {
1924
- title: 'Context',
1925
- param: 'context',
1926
- type: ParamType.String,
1927
- },
1928
- {
1929
- title: 'Subcontext',
1930
- param: 'subcontext',
1931
- type: ParamType.String,
1932
- },
1933
- {
1934
- title: 'Tags',
1935
- param: 'tags',
1936
- type: ParamType.JSON,
1937
- startsHidden: true,
1938
- },
1939
- {
1940
- title: 'Log Level',
1941
- param: 'level',
1942
- type: ParamType.String,
1943
- startsHidden: true,
1944
- },
1945
- {
1946
- title: 'Metadata',
1947
- param: 'metadata',
1948
- type: ParamType.JSON,
1949
- startsHidden: true,
1950
- },
1951
- {
1952
- title: 'Source',
1953
- param: 'source',
1954
- type: ParamType.String,
1955
- },
1956
- {
1957
- title: 'Server Route Path',
1958
- param: 'routePath',
1959
- type: ParamType.String,
1960
- startsHidden: true,
1961
- },
1962
- {
1963
- title: 'Server Route Template',
1964
- param: 'routeTemplate',
1965
- type: ParamType.String,
1966
- startsHidden: true,
1967
- },
1968
- {
1969
- title: 'Type',
1970
- param: 'type',
1971
- type: ParamType.String,
1972
- },
1973
- {
1974
- title: 'Error Message',
1975
- param: 'errorMessage',
1976
- type: ParamType.String,
1977
- startsHidden: true,
1978
- },
1979
- {
1980
- title: 'Error Code',
1981
- param: 'errorCode',
1982
- type: ParamType.String,
1983
- startsHidden: true,
1984
- },
1985
- {
1986
- title: 'Error Stack',
1987
- param: 'errorStack',
1988
- type: ParamType.String,
1989
- startsHidden: true,
1990
- },
1991
- {
1992
- title: 'Action Target',
1993
- param: 'target',
1994
- type: ParamType.String,
1995
- startsHidden: true,
1996
- },
1997
- {
1998
- title: 'Action Type',
1999
- param: 'action',
2000
- type: ParamType.String,
2001
- startsHidden: true,
2002
- },
2003
- ];
2004
-
2005
- // Create intelliTable
2006
- const dataTable = (
2166
+ // Nothing to show notice
2167
+ const noLogsNotice = (
2007
2168
  logs.length === 0
2008
2169
  ? (
2009
- <div className="alert alert-warning">
2170
+ <div className="alert alert-warning text-center mt-2">
2010
2171
  <h4 className="m-1">
2011
2172
  No Logs to Show
2012
2173
  </h4>
@@ -2016,21 +2177,28 @@ const LogReviewer: React.FC<Props> = (props) => {
2016
2177
  </div>
2017
2178
  </div>
2018
2179
  )
2019
- : (
2020
- <IntelliTable
2021
- title="Matching Logs"
2022
- id="logs"
2023
- data={logs}
2024
- columns={columns}
2025
- />
2026
- )
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
+ />
2027
2192
  );
2028
2193
 
2029
2194
  // Main body
2030
2195
  body = (
2031
2196
  <>
2032
2197
  {filters}
2033
- {dataTable}
2198
+ <div className="mt-2">
2199
+ {dataTable}
2200
+ {noLogsNotice}
2201
+ </div>
2034
2202
  </>
2035
2203
  );
2036
2204
  }
@@ -2045,20 +2213,22 @@ const LogReviewer: React.FC<Props> = (props) => {
2045
2213
  <div className="LogReviewer-inner-container">
2046
2214
  <div className="LogReviewer-header">
2047
2215
  <div className="LogReviewer-header-title">
2048
- <h3 className="m-0">
2216
+ <h3 className="text-center m-0">
2049
2217
  Log Review Dashboard
2050
2218
  </h3>
2051
2219
  </div>
2052
- <button
2053
- type="button"
2054
- className="LogReviewer-header-close-button btn btn-dark btn-lg"
2055
- aria-label="close log reviewer panel"
2056
- onClick={onClose}
2057
- >
2058
- <FontAwesomeIcon
2059
- icon={faTimes}
2060
- />
2061
- </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>
2062
2232
  </div>
2063
2233
  <div className="LogReviewer-contents">
2064
2234
  {body}