dce-reactkit 3.2.1 → 3.2.2-beta.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.
Files changed (55) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/dist/cjs/index.js +2106 -416
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/types/components/CSVDownloadButton.d.ts +19 -0
  5. package/dist/cjs/types/components/IntelliTable.d.ts +17 -0
  6. package/dist/cjs/types/components/LogReviewer.d.ts +13 -0
  7. package/dist/cjs/types/components/SimpleDateChooser.d.ts +1 -0
  8. package/dist/cjs/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
  9. package/dist/cjs/types/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
  10. package/dist/cjs/types/helpers/canReviewLogs.d.ts +7 -0
  11. package/dist/cjs/types/helpers/genCSV.d.ts +14 -0
  12. package/dist/cjs/types/helpers/getMonthName.d.ts +14 -0
  13. package/dist/cjs/types/index.d.ts +8 -1
  14. package/dist/cjs/types/server/initServer.d.ts +8 -0
  15. package/dist/cjs/types/types/IntelliTableColumn.d.ts +12 -0
  16. package/dist/cjs/types/types/LogBuiltInMetadata.d.ts +1 -0
  17. package/dist/cjs/types/types/LogMetadataType.d.ts +19 -0
  18. package/dist/cjs/types/types/ReactKitErrorCode.d.ts +2 -1
  19. package/dist/esm/index.js +2103 -418
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/types/components/CSVDownloadButton.d.ts +19 -0
  22. package/dist/esm/types/components/IntelliTable.d.ts +17 -0
  23. package/dist/esm/types/components/LogReviewer.d.ts +13 -0
  24. package/dist/esm/types/components/SimpleDateChooser.d.ts +1 -0
  25. package/dist/esm/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
  26. package/dist/esm/types/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
  27. package/dist/esm/types/helpers/canReviewLogs.d.ts +7 -0
  28. package/dist/esm/types/helpers/genCSV.d.ts +14 -0
  29. package/dist/esm/types/helpers/getMonthName.d.ts +14 -0
  30. package/dist/esm/types/index.d.ts +8 -1
  31. package/dist/esm/types/server/initServer.d.ts +8 -0
  32. package/dist/esm/types/types/IntelliTableColumn.d.ts +12 -0
  33. package/dist/esm/types/types/LogBuiltInMetadata.d.ts +1 -0
  34. package/dist/esm/types/types/LogMetadataType.d.ts +19 -0
  35. package/dist/esm/types/types/ReactKitErrorCode.d.ts +2 -1
  36. package/dist/index.d.ts +163 -47
  37. package/package.json +1 -1
  38. package/sandbox.js +78 -17
  39. package/src/components/AppWrapper.tsx +15 -0
  40. package/src/components/CSVDownloadButton.tsx +102 -0
  41. package/src/components/IntelliTable.tsx +610 -0
  42. package/src/components/LogReviewer.tsx +2038 -0
  43. package/src/components/SimpleDateChooser.tsx +26 -27
  44. package/src/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.ts +9 -0
  45. package/src/constants/LOG_REVIEW_STATUS_ROUTE.ts +10 -0
  46. package/src/helpers/canReviewLogs.ts +33 -0
  47. package/src/helpers/genCSV.ts +59 -0
  48. package/src/helpers/getHumanReadableDate.ts +6 -17
  49. package/src/helpers/getMonthName.ts +29 -0
  50. package/src/index.ts +14 -0
  51. package/src/server/initServer.ts +105 -3
  52. package/src/types/IntelliTableColumn.ts +24 -0
  53. package/src/types/LogBuiltInMetadata.ts +1 -0
  54. package/src/types/LogMetadataType.ts +23 -0
  55. package/src/types/ReactKitErrorCode.tsx +2 -1
package/dist/cjs/index.js CHANGED
@@ -36,7 +36,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
36
36
  });
37
37
  }
38
38
 
39
- // Highest error code = DRK10
39
+ // Highest error code = DRK11
40
40
  /**
41
41
  * List of error codes built into the react kit
42
42
  * @author Gabe Abrams
@@ -53,6 +53,7 @@ var ReactKitErrorCode;
53
53
  ReactKitErrorCode["NoCACCLGetLaunchInfoFunction"] = "DRK8";
54
54
  ReactKitErrorCode["NotTTM"] = "DRK9";
55
55
  ReactKitErrorCode["NotAdmin"] = "DRK10";
56
+ ReactKitErrorCode["NotAllowedToReviewLogs"] = "DRK11";
56
57
  })(ReactKitErrorCode || (ReactKitErrorCode = {}));
57
58
  var ReactKitErrorCode$1 = ReactKitErrorCode;
58
59
 
@@ -279,7 +280,7 @@ const ModalButtonTypeToLabelAndVariant = {
279
280
  /*------------------------------------------------------------------------*/
280
281
  /* Style */
281
282
  /*------------------------------------------------------------------------*/
282
- const style$6 = `
283
+ const style$7 = `
283
284
  .Modal-backdrop {
284
285
  position: fixed;
285
286
  top: 0;
@@ -453,7 +454,7 @@ const Modal = (props) => {
453
454
  left: 0,
454
455
  right: 0,
455
456
  } },
456
- React__default["default"].createElement("style", null, style$6),
457
+ React__default["default"].createElement("style", null, style$7),
457
458
  React__default["default"].createElement("div", { className: `Modal-backdrop ${backdropAnimationClass}`, style: {
458
459
  zIndex: 5000000003,
459
460
  }, onClick: () => __awaiter(void 0, void 0, void 0, function* () {
@@ -499,6 +500,184 @@ class ErrorWithCode extends Error {
499
500
  }
500
501
  }
501
502
 
503
+ /**
504
+ * Path that all routes start with
505
+ * @author Gabe Abrams
506
+ */
507
+ const ROUTE_PATH_PREFIX = '/dce-reactkit';
508
+
509
+ /**
510
+ * Path of the route for storing client-side logs
511
+ * @author Gabe Abrams
512
+ */
513
+ const LOG_ROUTE_PATH = `${ROUTE_PATH_PREFIX}/log`;
514
+
515
+ /**
516
+ * Built-in metadata for logs
517
+ * @author Gabe Abrams
518
+ */
519
+ const LogBuiltInMetadata = {
520
+ // Contexts
521
+ Context: {
522
+ Uncategorized: 'n/a',
523
+ ServerRenderedErrorPage: '_server-rendered-error-page',
524
+ ServerEndpointError: '_server-endpoint-error',
525
+ ClientFatalError: '_client-fatal-error',
526
+ },
527
+ // Targets
528
+ Target: {
529
+ NoSpecificTarget: 'n/a',
530
+ },
531
+ };
532
+
533
+ // Keep track of whether or not session expiry has already been handled
534
+ let sessionAlreadyExpired = false;
535
+ /*------------------------------------------------------------------------*/
536
+ /* Stub Logic */
537
+ /*------------------------------------------------------------------------*/
538
+ // Stored stub responses
539
+ const stubResponses = {};
540
+ /**
541
+ * Add a stub response
542
+ * @author Gabe Abrams
543
+ * @param opts object containing all arguments
544
+ * @param [opts.method=GET] http request method
545
+ * @param opts.path pathname of the request
546
+ * @param [opts.body] body of the response if successful
547
+ * @param [opts.errorMessage] error message if not successful
548
+ * @param [opts.errorCode] error code if not successful
549
+ */
550
+ const _setStubResponse = (opts) => {
551
+ var _a, _b, _c;
552
+ const { path, body, } = opts;
553
+ const method = ((_a = opts.method) !== null && _a !== void 0 ? _a : 'GET').toUpperCase();
554
+ const errorMessage = ((_b = opts.errorMessage) !== null && _b !== void 0 ? _b : 'An unknown error has occurred.');
555
+ const errorCode = ((_c = opts.errorCode) !== null && _c !== void 0 ? _c : ReactKitErrorCode$1.NoCode);
556
+ // Store to stub responses
557
+ if (!stubResponses[method]) {
558
+ stubResponses[method] = {};
559
+ }
560
+ stubResponses[method][path] = ((opts.errorMessage || opts.errorCode)
561
+ ? {
562
+ success: false,
563
+ errorMessage,
564
+ errorCode,
565
+ }
566
+ : {
567
+ success: true,
568
+ body: body !== null && body !== void 0 ? body : undefined,
569
+ });
570
+ };
571
+ /*------------------------------------------------------------------------*/
572
+ /* Main */
573
+ /*------------------------------------------------------------------------*/
574
+ /**
575
+ * Visit an endpoint on the server [for client only]
576
+ * @author Gabe Abrams
577
+ * @param opts object containing all arguments
578
+ * @param opts.path - the path of the server endpoint
579
+ * @param [opts.method=GET] - the method of the endpoint
580
+ * @param [opts.params] - query/body parameters to include
581
+ * @returns response from server
582
+ */
583
+ const visitServerEndpoint = (opts) => __awaiter(void 0, void 0, void 0, function* () {
584
+ var _a, _b, _c;
585
+ const method = ((_a = opts.method) !== null && _a !== void 0 ? _a : 'GET');
586
+ // Handle stubs
587
+ const stubResponse = (_b = stubResponses[method]) === null || _b === void 0 ? void 0 : _b[opts.path];
588
+ if (stubResponse) {
589
+ // Remove from list
590
+ try {
591
+ stubResponses[method][opts.path] = undefined;
592
+ }
593
+ catch (err) {
594
+ // Ignore
595
+ }
596
+ // Success
597
+ if (stubResponse.success) {
598
+ return stubResponse.body;
599
+ }
600
+ // Error
601
+ throw new ErrorWithCode(stubResponse.errorMessage, stubResponse.errorCode);
602
+ }
603
+ // Send the request
604
+ const response = yield cacclSendRequest({
605
+ path: opts.path,
606
+ method: (_c = opts.method) !== null && _c !== void 0 ? _c : 'GET',
607
+ params: opts.params,
608
+ });
609
+ // Check for failure
610
+ if (!response || !response.body) {
611
+ throw new ErrorWithCode('We didn\'t get a response from the server. Please check your internet connection.', ReactKitErrorCode$1.NoResponse);
612
+ }
613
+ if (!response.body.success) {
614
+ // Session expired
615
+ if (response.body.code === ReactKitErrorCode$1.SessionExpired) {
616
+ // Skip notice if session was already expired
617
+ if (sessionAlreadyExpired) {
618
+ // Never return (browser is already reloading)
619
+ yield new Promise(() => {
620
+ // Promise that never returns
621
+ });
622
+ }
623
+ sessionAlreadyExpired = true;
624
+ // Show session expiration message
625
+ {
626
+ // Fallback to alert
627
+ // eslint-disable-next-line no-alert
628
+ alert('Your session has expired. Please start over.');
629
+ }
630
+ // Never return (don't continue execution)
631
+ yield new Promise(() => {
632
+ // Promise that never returns
633
+ });
634
+ }
635
+ // Other errors
636
+ throw new ErrorWithCode((response.body.message
637
+ || 'An unknown error occurred. Please contact an admin.'), (response.body.code
638
+ || ReactKitErrorCode$1.NoCode));
639
+ }
640
+ // Success! Extract the body
641
+ const { body } = response.body;
642
+ // Return
643
+ return body;
644
+ });
645
+
646
+ /**
647
+ * Log a user action on the client (cannot be used on the server)
648
+ * @author Gabe Abrams
649
+ */
650
+ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* () {
651
+ var _a, _b, _c, _d, _e, _f;
652
+ return visitServerEndpoint({
653
+ path: LOG_ROUTE_PATH,
654
+ method: 'POST',
655
+ params: {
656
+ context: (typeof opts.context === 'string'
657
+ ? opts.context
658
+ : ((_b = ((_a = opts.context) !== null && _a !== void 0 ? _a : {})._) !== null && _b !== void 0 ? _b : LogBuiltInMetadata.Context.Uncategorized)),
659
+ subcontext: ((_c = opts.subcontext) !== null && _c !== void 0 ? _c : LogBuiltInMetadata.Context.Uncategorized),
660
+ tags: JSON.stringify((_d = opts.tags) !== null && _d !== void 0 ? _d : []),
661
+ metadata: JSON.stringify((_e = opts.metadata) !== null && _e !== void 0 ? _e : {}),
662
+ errorMessage: (opts.error
663
+ ? opts.error.message
664
+ : undefined),
665
+ errorCode: (opts.error
666
+ ? opts.error.code
667
+ : undefined),
668
+ errorStack: (opts.error
669
+ ? opts.error.stack
670
+ : undefined),
671
+ target: (opts.action
672
+ ? ((_f = opts.target) !== null && _f !== void 0 ? _f : LogBuiltInMetadata.Target.NoSpecificTarget)
673
+ : undefined),
674
+ action: (opts.action
675
+ ? opts.action
676
+ : undefined),
677
+ },
678
+ });
679
+ });
680
+
502
681
  /**
503
682
  * A wrapper for the entire React app that adds global functionality like
504
683
  * handling for fatal error messages, adds bootstrap support
@@ -618,6 +797,18 @@ const showFatalError = (error, errorTitle = 'An Error Occurred') => {
618
797
  const code = (typeof error === 'string'
619
798
  ? ReactKitErrorCode$1.NoCode
620
799
  : String((_b = error.code) !== null && _b !== void 0 ? _b : ReactKitErrorCode$1.NoCode));
800
+ // Add log
801
+ logClientEvent({
802
+ context: LogBuiltInMetadata.Context.ClientFatalError,
803
+ error: {
804
+ message,
805
+ code,
806
+ stack: (error !== null && error !== void 0 ? error : {}).stack,
807
+ },
808
+ metadata: {
809
+ errorTitle,
810
+ },
811
+ });
621
812
  // Handle case where app hasn't loaded
622
813
  if (!setFatalErrorMessage || !setFatalErrorCode) {
623
814
  alert$1(errorTitle, `${message} (code: ${code}). Please contact support.`);
@@ -727,7 +918,7 @@ const AppWrapper = (props) => {
727
918
  /*------------------------------------------------------------------------*/
728
919
  /* Style */
729
920
  /*------------------------------------------------------------------------*/
730
- const style$5 = `
921
+ const style$6 = `
731
922
  /* Container fades in */
732
923
  .LoadingSpinner-container {
733
924
  animation-name: LoadingSpinner-container-fade-in;
@@ -805,7 +996,7 @@ const LoadingSpinner = () => {
805
996
  /*------------------------------------------------------------------------*/
806
997
  // Add all four blips to a container
807
998
  return (React__default["default"].createElement("div", { className: "text-center LoadingSpinner LoadingSpinner-container" },
808
- React__default["default"].createElement("style", null, style$5),
999
+ React__default["default"].createElement("style", null, style$6),
809
1000
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-1 me-1" }),
810
1001
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-2 me-1" }),
811
1002
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-3 me-1" }),
@@ -819,7 +1010,7 @@ const LoadingSpinner = () => {
819
1010
  /*------------------------------------------------------------------------*/
820
1011
  /* Style */
821
1012
  /*------------------------------------------------------------------------*/
822
- const style$4 = `
1013
+ const style$5 = `
823
1014
  /* Tab Box */
824
1015
  .TabBox-box {
825
1016
  /* Light Border */
@@ -899,7 +1090,7 @@ const TabBox = (props) => {
899
1090
  /*----------------------------------------*/
900
1091
  // Full UI
901
1092
  return (React__default["default"].createElement("div", { className: `TabBox-container ${noBottomMargin ? '' : 'mb-2'}` },
902
- React__default["default"].createElement("style", null, style$4),
1093
+ React__default["default"].createElement("style", null, style$5),
903
1094
  React__default["default"].createElement("div", { className: "TabBox-title-container" },
904
1095
  React__default["default"].createElement("div", { className: "TabBox-title" }, title)),
905
1096
  React__default["default"].createElement("div", { className: `TabBox-box ps-2 pt-2 pe-2 ${noBottomPadding ? '' : 'pb-2'}` },
@@ -1006,6 +1197,34 @@ const ButtonInputGroup = (props) => {
1006
1197
  } }, children))));
1007
1198
  };
1008
1199
 
1200
+ const monthNames = [
1201
+ { short: 'Jan', full: 'January' },
1202
+ { short: 'Feb', full: 'February' },
1203
+ { short: 'Mar', full: 'March' },
1204
+ { short: 'Apr', full: 'April' },
1205
+ { short: 'May', full: 'May' },
1206
+ { short: 'Jun', full: 'June' },
1207
+ { short: 'Jul', full: 'July' },
1208
+ { short: 'Aug', full: 'August' },
1209
+ { short: 'Sep', full: 'September' },
1210
+ { short: 'Oct', full: 'October' },
1211
+ { short: 'Nov', full: 'November' },
1212
+ { short: 'Dec', full: 'December' },
1213
+ ];
1214
+ /**
1215
+ * Get the name of a month given the month number (1 = January, etc.)
1216
+ * If an invalid number is provided, we will treat it like January
1217
+ * @author Gabe Abrams
1218
+ * @param month the number of the month
1219
+ * @returns object containing multiple month name formats:
1220
+ * { short, full } where short will look like "Jan" and full will look like
1221
+ * "January"
1222
+ */
1223
+ const getMonthName = (month) => {
1224
+ var _a;
1225
+ return ((_a = monthNames[month - 1]) !== null && _a !== void 0 ? _a : monthNames[0]);
1226
+ };
1227
+
1009
1228
  const ORDINALS = ['th', 'st', 'nd', 'rd'];
1010
1229
  /**
1011
1230
  * Get a number's ordinal
@@ -1075,29 +1294,23 @@ const getTimeInfoInET = (dateOrTimestamp) => {
1075
1294
  };
1076
1295
  };
1077
1296
 
1297
+ /**
1298
+ * Force a number to stay within specific bounds
1299
+ * @author Gabe Abrams
1300
+ * @param num the number to move into the bounds
1301
+ * @param min the minimum number in the bound
1302
+ * @param max the maximum number in the bound
1303
+ * @returns bounded number
1304
+ */
1305
+ const forceNumIntoBounds = (num, min, max) => {
1306
+ return Math.max(min, Math.min(max, num));
1307
+ };
1308
+
1078
1309
  /**
1079
1310
  * A very simple, lightweight date chooser
1080
1311
  * @author Gabe Abrams
1081
1312
  */
1082
1313
  /*------------------------------------------------------------------------*/
1083
- /* Constants */
1084
- /*------------------------------------------------------------------------*/
1085
- // Constants
1086
- const MONTH_NAMES = [
1087
- 'January',
1088
- 'February',
1089
- 'March',
1090
- 'April',
1091
- 'May',
1092
- 'June',
1093
- 'July',
1094
- 'August',
1095
- 'September',
1096
- 'October',
1097
- 'November',
1098
- 'December',
1099
- ];
1100
- /*------------------------------------------------------------------------*/
1101
1314
  /* Component */
1102
1315
  /*------------------------------------------------------------------------*/
1103
1316
  const SimpleDateChooser = (props) => {
@@ -1106,8 +1319,8 @@ const SimpleDateChooser = (props) => {
1106
1319
  /*------------------------------------------------------------------------*/
1107
1320
  var _a;
1108
1321
  /* -------------- Props ------------- */
1109
- const { ariaLabel, name, month, day, year, onChange, } = props;
1110
- const numMonthsToShow = Math.min((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6, 12);
1322
+ const { ariaLabel, name, month, day, year, onChange, chooseFromPast, } = props;
1323
+ let numMonthsToShow = forceNumIntoBounds(((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6), 1, 12);
1111
1324
  /*------------------------------------------------------------------------*/
1112
1325
  /* Render */
1113
1326
  /*------------------------------------------------------------------------*/
@@ -1117,16 +1330,25 @@ const SimpleDateChooser = (props) => {
1117
1330
  // Determine the set of choices allowed
1118
1331
  const today = getTimeInfoInET();
1119
1332
  const choices = [];
1333
+ let startYear = today.year;
1334
+ let startMonth = today.month;
1335
+ if (chooseFromPast) {
1336
+ startMonth -= Math.max(0, numMonthsToShow - 1);
1337
+ if (startMonth <= 0) {
1338
+ startMonth += 12;
1339
+ startYear -= 1;
1340
+ }
1341
+ }
1120
1342
  for (let i = 0; i < numMonthsToShow; i++) {
1121
1343
  // Get month and year info
1122
- const unmoddedMonth = (today.month + i);
1344
+ const unmoddedMonth = (startMonth + i);
1123
1345
  const month = (unmoddedMonth > 12
1124
1346
  ? unmoddedMonth - 12
1125
1347
  : unmoddedMonth);
1126
- const monthName = MONTH_NAMES[month - 1];
1348
+ const monthName = getMonthName(month).full;
1127
1349
  const year = (unmoddedMonth > 12
1128
- ? today.year + 1
1129
- : today.year);
1350
+ ? startYear + 1
1351
+ : startYear);
1130
1352
  // Figure out which days are allowed
1131
1353
  const days = [];
1132
1354
  const numDaysInMonth = (new Date(year, month, 0)).getDate();
@@ -1182,7 +1404,7 @@ const SimpleDateChooser = (props) => {
1182
1404
  /*------------------------------------------------------------------------*/
1183
1405
  /* Style */
1184
1406
  /*------------------------------------------------------------------------*/
1185
- const style$3 = `
1407
+ const style$4 = `
1186
1408
  .Drawer-container {
1187
1409
  margin-left: 1rem;
1188
1410
  margin-right: 1rem;
@@ -1214,7 +1436,7 @@ const Drawer = (props) => {
1214
1436
  /* Main UI */
1215
1437
  /*----------------------------------------*/
1216
1438
  return (React__default["default"].createElement("div", { className: "Drawer-container" },
1217
- React__default["default"].createElement("style", null, style$3),
1439
+ React__default["default"].createElement("style", null, style$4),
1218
1440
  children));
1219
1441
  };
1220
1442
 
@@ -1225,7 +1447,7 @@ const Drawer = (props) => {
1225
1447
  /*------------------------------------------------------------------------*/
1226
1448
  /* Style */
1227
1449
  /*------------------------------------------------------------------------*/
1228
- const style$2 = `
1450
+ const style$3 = `
1229
1451
  .PopSuccessMark-outer-container {
1230
1452
  position: relative;
1231
1453
  display: inline-block;
@@ -1330,7 +1552,7 @@ const PopSuccessMark = (props) => {
1330
1552
  width: `${sizeRem}rem`,
1331
1553
  height: `${sizeRem}rem`,
1332
1554
  }, "aria-label": "checkmark indicating success" },
1333
- React__default["default"].createElement("style", null, style$2),
1555
+ React__default["default"].createElement("style", null, style$3),
1334
1556
  React__default["default"].createElement("div", { className: `PopSuccessMark-check-stroke-1 bg-${checkVariant}`, style: {
1335
1557
  borderRadius: `${sizeRem / 5}rem`,
1336
1558
  } }),
@@ -1346,7 +1568,7 @@ const PopSuccessMark = (props) => {
1346
1568
  /*------------------------------------------------------------------------*/
1347
1569
  /* Style */
1348
1570
  /*------------------------------------------------------------------------*/
1349
- const style$1 = `
1571
+ const style$2 = `
1350
1572
  .PopFailureMark-outer-container {
1351
1573
  position: relative;
1352
1574
  display: inline-block;
@@ -1450,7 +1672,7 @@ const PopFailureMark = (props) => {
1450
1672
  width: `${sizeRem}rem`,
1451
1673
  height: `${sizeRem}rem`,
1452
1674
  }, "aria-label": "mark indicating failure" },
1453
- React__default["default"].createElement("style", null, style$1),
1675
+ React__default["default"].createElement("style", null, style$2),
1454
1676
  React__default["default"].createElement("div", { className: `PopFailureMark-x-stroke-1 bg-${xVariant}`, style: {
1455
1677
  borderRadius: `${sizeRem / 5}rem`,
1456
1678
  } }),
@@ -1466,7 +1688,7 @@ const PopFailureMark = (props) => {
1466
1688
  /*------------------------------------------------------------------------*/
1467
1689
  /* Style */
1468
1690
  /*------------------------------------------------------------------------*/
1469
- const style = `
1691
+ const style$1 = `
1470
1692
  .PopPendingMark-outer-container {
1471
1693
  position: relative;
1472
1694
  display: inline-block;
@@ -1539,7 +1761,7 @@ const PopPendingMark = (props) => {
1539
1761
  width: `${sizeRem}rem`,
1540
1762
  height: `${sizeRem}rem`,
1541
1763
  }, "aria-label": "mark indicating that the item is pending" },
1542
- React__default["default"].createElement("style", null, style),
1764
+ React__default["default"].createElement("style", null, style$1),
1543
1765
  React__default["default"].createElement("div", null,
1544
1766
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faHourglass, className: `PopPendingMark-hourglass text-${hourglassVariant}`, style: {
1545
1767
  fontSize: `${sizeRem * 0.6}rem`,
@@ -1552,27 +1774,27 @@ const PopPendingMark = (props) => {
1552
1774
  */
1553
1775
  /* ------------- Actions ------------ */
1554
1776
  // Types of actions
1555
- var ActionType$1;
1777
+ var ActionType$3;
1556
1778
  (function (ActionType) {
1557
1779
  // Indicate that the text was recently copied
1558
1780
  ActionType["IndicateRecentlyCopied"] = "indicate-recently-copied";
1559
1781
  // Clear the status
1560
1782
  ActionType["ClearRecentlyCopiedStatus"] = "clear-recently-copied-status";
1561
- })(ActionType$1 || (ActionType$1 = {}));
1783
+ })(ActionType$3 || (ActionType$3 = {}));
1562
1784
  /**
1563
1785
  * Reducer that executes actions
1564
1786
  * @author Gabe Abrams
1565
1787
  * @param state current state
1566
1788
  * @param action action to execute
1567
1789
  */
1568
- const reducer$1 = (state, action) => {
1790
+ const reducer$3 = (state, action) => {
1569
1791
  switch (action.type) {
1570
- case ActionType$1.IndicateRecentlyCopied: {
1792
+ case ActionType$3.IndicateRecentlyCopied: {
1571
1793
  return {
1572
1794
  recentlyCopied: true,
1573
1795
  };
1574
1796
  }
1575
- case ActionType$1.ClearRecentlyCopiedStatus: {
1797
+ case ActionType$3.ClearRecentlyCopiedStatus: {
1576
1798
  return {
1577
1799
  recentlyCopied: false,
1578
1800
  };
@@ -1598,7 +1820,7 @@ const CopiableBox = (props) => {
1598
1820
  recentlyCopied: false,
1599
1821
  };
1600
1822
  // Initialize state
1601
- const [state, dispatch] = React.useReducer(reducer$1, initialState);
1823
+ const [state, dispatch] = React.useReducer(reducer$3, initialState);
1602
1824
  // Destructure common state
1603
1825
  const { recentlyCopied, } = state;
1604
1826
  /*------------------------------------------------------------------------*/
@@ -1618,13 +1840,13 @@ const CopiableBox = (props) => {
1618
1840
  }
1619
1841
  // Show copied notice
1620
1842
  dispatch({
1621
- type: ActionType$1.IndicateRecentlyCopied,
1843
+ type: ActionType$3.IndicateRecentlyCopied,
1622
1844
  });
1623
1845
  // Wait a moment
1624
1846
  yield waitMs(4000);
1625
1847
  // Hide copied notice
1626
1848
  dispatch({
1627
- type: ActionType$1.ClearRecentlyCopiedStatus,
1849
+ type: ActionType$3.ClearRecentlyCopiedStatus,
1628
1850
  });
1629
1851
  });
1630
1852
  /*------------------------------------------------------------------------*/
@@ -1679,20 +1901,20 @@ const CopiableBox = (props) => {
1679
1901
  */
1680
1902
  /* ------------- Actions ------------ */
1681
1903
  // Types of actions
1682
- var ActionType;
1904
+ var ActionType$2;
1683
1905
  (function (ActionType) {
1684
1906
  // Toggle whether the children are being shown
1685
1907
  ActionType["ToggleItems"] = "toggle-items";
1686
- })(ActionType || (ActionType = {}));
1908
+ })(ActionType$2 || (ActionType$2 = {}));
1687
1909
  /**
1688
1910
  * Reducer that executes actions
1689
1911
  * @author Yuen Ler Chow
1690
1912
  * @param state current state
1691
1913
  * @param action action to execute
1692
1914
  */
1693
- const reducer = (state, action) => {
1915
+ const reducer$2 = (state, action) => {
1694
1916
  switch (action.type) {
1695
- case ActionType.ToggleItems: {
1917
+ case ActionType$2.ToggleItems: {
1696
1918
  return { isShowingItems: !state.isShowingItems };
1697
1919
  }
1698
1920
  default: {
@@ -1716,7 +1938,7 @@ const NestableItemList = (props) => {
1716
1938
  isShowingItems: false,
1717
1939
  };
1718
1940
  // Initialize state
1719
- const [state, dispatch] = React.useReducer(reducer, initialState);
1941
+ const [state, dispatch] = React.useReducer(reducer$2, initialState);
1720
1942
  // Destructure common state
1721
1943
  const { isShowingItems, } = state;
1722
1944
  /*------------------------------------------------------------------------*/
@@ -1806,7 +2028,7 @@ const NestableItemList = (props) => {
1806
2028
  backgroundColor: 'transparent',
1807
2029
  }, type: "button", onClick: () => {
1808
2030
  dispatch({
1809
- type: ActionType.ToggleItems,
2031
+ type: ActionType$2.ToggleItems,
1810
2032
  });
1811
2033
  }, "aria-label": `${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}` },
1812
2034
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: isShowingItems ? freeSolidSvgIcons.faChevronDown : freeSolidSvgIcons.faChevronRight })))),
@@ -1851,311 +2073,1827 @@ const ItemPicker = (props) => {
1851
2073
  };
1852
2074
 
1853
2075
  /**
1854
- * One minute in ms
2076
+ * Path of the route for storing client-side logs
1855
2077
  * @author Gabe Abrams
1856
2078
  */
1857
- const MINUTE_IN_MS = 60000;
2079
+ const LOG_REVIEW_ROUTE_PATH_PREFIX = `/admin${ROUTE_PATH_PREFIX}/logs`;
1858
2080
 
1859
2081
  /**
1860
- * One hour in ms
2082
+ * Source of a log event
1861
2083
  * @author Gabe Abrams
1862
2084
  */
1863
- const HOUR_IN_MS = 3600000;
2085
+ var LogSource;
2086
+ (function (LogSource) {
2087
+ // Client
2088
+ LogSource["Client"] = "client";
2089
+ // Server
2090
+ LogSource["Server"] = "server";
2091
+ })(LogSource || (LogSource = {}));
2092
+ var LogSource$1 = LogSource;
1864
2093
 
1865
2094
  /**
1866
- * One day in ms
2095
+ * Type of a log event
1867
2096
  * @author Gabe Abrams
1868
2097
  */
1869
- const DAY_IN_MS = 86400000;
1870
-
1871
- /**
1872
- * Shorten text so it fits into a certain number of chars
1873
- * @author Gabe Abrams
1874
- * @param text the text to abbreviate
1875
- * @param maxChars the maximum number of chars to include
1876
- * @returns abbreviated text with length no greater than maxChars
1877
- * (including ellipses if applicable)
1878
- */
1879
- const abbreviate = (text, maxChars) => {
1880
- // Check if already short enough
1881
- if (text.trim().length < maxChars) {
1882
- return text.trim();
1883
- }
1884
- // Abbreviate
1885
- const shortenedText = (text
1886
- .trim()
1887
- .substring(0, maxChars - 3)
1888
- .trim());
1889
- return `${shortenedText}...`;
1890
- };
1891
-
1892
- /**
1893
- * Sum the numbers in an array
1894
- * @author Gabe Abrams
1895
- * @param nums the numbers to sum
1896
- * @returns the sum of the numbers
1897
- */
1898
- const sum = (nums) => {
1899
- return nums.reduce((a, b) => {
1900
- return (a + b);
1901
- }, 0);
1902
- };
2098
+ var LogType;
2099
+ (function (LogType) {
2100
+ // User action
2101
+ LogType["Action"] = "action";
2102
+ // Error
2103
+ LogType["Error"] = "error";
2104
+ })(LogType || (LogType = {}));
2105
+ var LogType$1 = LogType;
1903
2106
 
1904
2107
  /**
1905
- * Get the average of a set of numbers
2108
+ * Types of actions
1906
2109
  * @author Gabe Abrams
1907
- * @param nums the numbers to average
1908
- * @returns average value or 0 if no numbers
1909
2110
  */
1910
- const avg = (nums) => {
1911
- // Handle empty array case
1912
- if (nums.length === 0) {
1913
- return 0;
1914
- }
1915
- // Get the total value
1916
- const total = sum(nums);
1917
- // Get average
1918
- return (total / nums.length);
1919
- };
2111
+ var LogAction;
2112
+ (function (LogAction) {
2113
+ // Target was opened by the user (it was not on screen, but now it is)
2114
+ LogAction["Open"] = "open";
2115
+ // Target was closed by the user (it was on screen, but now it is not)
2116
+ LogAction["Close"] = "close";
2117
+ // Target was cancelled by the user (it was on closed without saving)
2118
+ LogAction["Cancel"] = "cancel";
2119
+ // Target was expanded by the user (it always remains on screen, but size was changed)
2120
+ LogAction["Expand"] = "expand";
2121
+ // Target was collapsed by the user (it always remains on screen, but size was changed)
2122
+ LogAction["Collapse"] = "collapse";
2123
+ // Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
2124
+ LogAction["View"] = "view";
2125
+ // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2126
+ LogAction["Interrupt"] = "interrupt";
2127
+ // Target was created by the user (it did not exist before)
2128
+ LogAction["Create"] = "create";
2129
+ // Target was edited by the user (it existed and was changed)
2130
+ LogAction["Edit"] = "edit";
2131
+ // Target was deleted by the user (it existed and now it doesn't)
2132
+ LogAction["Delete"] = "delete";
2133
+ // Target was added by the user (it already existed and was added to another place)
2134
+ LogAction["Add"] = "add";
2135
+ // Target was removed by the user (it was removed from something but still exists)
2136
+ LogAction["Remove"] = "remove";
2137
+ // Target was activated by the user (click, check, tap, keypress, etc.)
2138
+ LogAction["Activate"] = "activate";
2139
+ // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2140
+ LogAction["Deactivate"] = "deactivate";
2141
+ // User showed interest in a target (hover, peek, etc.)
2142
+ LogAction["Peek"] = "peek";
2143
+ // Unknown action
2144
+ LogAction["Unknown"] = "unknown";
2145
+ })(LogAction || (LogAction = {}));
2146
+ var LogAction$1 = LogAction;
1920
2147
 
1921
2148
  /**
1922
- * Round a number (ceiling) to a certain number of decimals
2149
+ * Server-side API param types
1923
2150
  * @author Gabe Abrams
1924
- * @param num the number to round
1925
- * @param numDecimals the number of decimals to round to
1926
- * @returns rounded number
1927
2151
  */
1928
- const ceilToNumDecimals = (num, numDecimals) => {
1929
- const rounder = 10 ** numDecimals;
1930
- return (Math.ceil(num * rounder) / rounder);
1931
- };
2152
+ var ParamType;
2153
+ (function (ParamType) {
2154
+ ParamType["Boolean"] = "boolean";
2155
+ ParamType["BooleanOptional"] = "boolean-optional";
2156
+ ParamType["Float"] = "float";
2157
+ ParamType["FloatOptional"] = "float-optional";
2158
+ ParamType["Int"] = "int";
2159
+ ParamType["IntOptional"] = "int-optional";
2160
+ ParamType["JSON"] = "json";
2161
+ ParamType["JSONOptional"] = "json-optional";
2162
+ ParamType["String"] = "string";
2163
+ ParamType["StringOptional"] = "string-optional";
2164
+ })(ParamType || (ParamType = {}));
2165
+ var ParamType$1 = ParamType;
1932
2166
 
1933
2167
  /**
1934
- * Round a number (floor) to a certain number of decimals
2168
+ * Round a number to a certain number of decimals
1935
2169
  * @author Gabe Abrams
1936
2170
  * @param num the number to round
1937
2171
  * @param numDecimals the number of decimals to round to
1938
2172
  * @returns rounded number
1939
2173
  */
1940
- const floorToNumDecimals = (num, numDecimals) => {
2174
+ const roundToNumDecimals = (num, numDecimals) => {
1941
2175
  const rounder = 10 ** numDecimals;
1942
- return (Math.floor(num * rounder) / rounder);
2176
+ return (Math.round(num * rounder) / rounder);
1943
2177
  };
1944
2178
 
1945
2179
  /**
1946
- * Force a number to stay within specific bounds
2180
+ * Escape a CSV cell if needed
1947
2181
  * @author Gabe Abrams
1948
- * @param num the number to move into the bounds
1949
- * @param min the minimum number in the bound
1950
- * @param max the maximum number in the bound
1951
- * @returns bounded number
2182
+ * @param text the cell contents
2183
+ * @returns escaped cell text
1952
2184
  */
1953
- const forceNumIntoBounds = (num, min, max) => {
1954
- return Math.max(min, Math.min(max, num));
2185
+ const escapeCellText = (text) => {
2186
+ if (!text.includes(',')) {
2187
+ // No need to escape
2188
+ return text;
2189
+ }
2190
+ // Perform escape
2191
+ return `"${text.replace(/"/g, '""')}`;
1955
2192
  };
1956
-
1957
2193
  /**
1958
- * Pad a number's decimal with zeros on the right
1959
- * (e.g. 5.2 becomes 5.20 with 2 digit padding)
2194
+ * Generate a CSV file
1960
2195
  * @author Gabe Abrams
1961
- * @param num the number to pad
1962
- * @param numDigits the minimum number of digits after the decimal
1963
- * @returns padded number
2196
+ * @param data list of row data in the form of json objects
2197
+ * @param columns list of columns to include in the csv
2198
+ * @returns multiline csv string
1964
2199
  */
1965
- const padDecimalZeros = (num, numDigits) => {
1966
- // Skip if nothing to do
1967
- if (numDigits < 1) {
1968
- return String(num);
1969
- }
1970
- // Convert to string
1971
- let out = String(num);
1972
- // Add a decimal point if there isn't one
1973
- if (!out.includes('.')) {
1974
- out += '.';
1975
- }
1976
- // Add zeros
1977
- while (out.split('.')[1].length < numDigits) {
1978
- out = `${out}0`;
1979
- }
2200
+ const genCSV = (data, columns) => {
2201
+ let csv = '';
2202
+ // Add header
2203
+ csv += (columns
2204
+ .map((column) => {
2205
+ return escapeCellText(column.title);
2206
+ })
2207
+ .join(','));
2208
+ // Add each row
2209
+ data.forEach((datum) => {
2210
+ csv += (columns
2211
+ .map((column) => {
2212
+ return escapeCellText(datum[column.param]);
2213
+ })
2214
+ .join(','));
2215
+ });
1980
2216
  // Return
1981
- return out;
2217
+ return csv;
1982
2218
  };
1983
2219
 
1984
2220
  /**
1985
- * Pad a number with zeros on the left (e.g. 5 becomes 05 with 2 digit padding)
2221
+ * Button for downloading a csv file
1986
2222
  * @author Gabe Abrams
1987
- * @param num the number to pad
1988
- * @param numDigits the minimum number of digits before the decimal
1989
- * @returns padded number
1990
2223
  */
1991
- const padZerosLeft = (num, numDigits) => {
1992
- // Convert to string
1993
- let out = String(num);
1994
- // Add zeros
1995
- while (out.split('.')[0].length < numDigits) {
1996
- out = `0${out}`;
1997
- }
1998
- // Return
1999
- return out;
2224
+ /*------------------------------------------------------------------------*/
2225
+ /* Component */
2226
+ /*------------------------------------------------------------------------*/
2227
+ const CSVDownloadButton = (props) => {
2228
+ /*------------------------------------------------------------------------*/
2229
+ /* Setup */
2230
+ /*------------------------------------------------------------------------*/
2231
+ /* -------------- Props ------------- */
2232
+ // Destructure all props
2233
+ const { filename, csv, id, className, ariaLabel, style, onClick, children, } = props;
2234
+ /*------------------------------------------------------------------------*/
2235
+ /* Render */
2236
+ /*------------------------------------------------------------------------*/
2237
+ /*----------------------------------------*/
2238
+ /* Main UI */
2239
+ /*----------------------------------------*/
2240
+ // Render the button
2241
+ return (React__default["default"].createElement("a", { id: id, download: filename, href: `data:application/octet-stream,${csv}`, className: `CSVDownloadButton-button ${className !== null && className !== void 0 ? className : 'btn btn-secondary'}`, "aria-label": (ariaLabel
2242
+ ? `Click to download ${filename}`
2243
+ : ariaLabel), style: style, onClick: onClick },
2244
+ !children && (React__default["default"].createElement(React__default["default"].Fragment, null,
2245
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCloudDownloadAlt, className: "mr-2" }),
2246
+ "Download CSV")),
2247
+ children));
2000
2248
  };
2001
2249
 
2002
2250
  /**
2003
- * Round a number to a certain number of decimals
2251
+ * Intelligent table
2004
2252
  * @author Gabe Abrams
2005
- * @param num the number to round
2006
- * @param numDecimals the number of decimals to round to
2007
- * @returns rounded number
2008
2253
  */
2009
- const roundToNumDecimals = (num, numDecimals) => {
2010
- const rounder = 10 ** numDecimals;
2011
- return (Math.round(num * rounder) / rounder);
2012
- };
2013
-
2014
- // Keep track of whether or not session expiry has already been handled
2015
- let sessionAlreadyExpired = false;
2016
- /*------------------------------------------------------------------------*/
2017
- /* Stub Logic */
2018
- /*------------------------------------------------------------------------*/
2019
- // Stored stub responses
2020
- const stubResponses = {};
2254
+ // Sort types
2255
+ var SortType;
2256
+ (function (SortType) {
2257
+ // Ascending
2258
+ SortType["Ascending"] = "ascending";
2259
+ // Descending
2260
+ SortType["Descending"] = "descending";
2261
+ })(SortType || (SortType = {}));
2262
+ /* ------------- Actions ------------ */
2263
+ // Types of actions
2264
+ var ActionType$1;
2265
+ (function (ActionType) {
2266
+ // Toggle sort column param
2267
+ ActionType["ToggleSortColumn"] = "toggle-sort-column";
2268
+ // Toggle the visibility of a column
2269
+ ActionType["ToggleColumnVisibility"] = "toggle-column-visibility";
2270
+ // Toggle the column visibility customization modal
2271
+ ActionType["ToggleColVisCusModalVisibility"] = "toggle-col-vis-cus-modal-visibility";
2272
+ })(ActionType$1 || (ActionType$1 = {}));
2021
2273
  /**
2022
- * Add a stub response
2274
+ * Reducer that executes actions
2023
2275
  * @author Gabe Abrams
2024
- * @param opts object containing all arguments
2025
- * @param [opts.method=GET] http request method
2026
- * @param opts.path pathname of the request
2027
- * @param [opts.body] body of the response if successful
2028
- * @param [opts.errorMessage] error message if not successful
2029
- * @param [opts.errorCode] error code if not successful
2276
+ * @param state current state
2277
+ * @param action action to execute
2030
2278
  */
2031
- const _setStubResponse = (opts) => {
2032
- var _a, _b, _c;
2033
- const { path, body, } = opts;
2034
- const method = ((_a = opts.method) !== null && _a !== void 0 ? _a : 'GET').toUpperCase();
2035
- const errorMessage = ((_b = opts.errorMessage) !== null && _b !== void 0 ? _b : 'An unknown error has occurred.');
2036
- const errorCode = ((_c = opts.errorCode) !== null && _c !== void 0 ? _c : ReactKitErrorCode$1.NoCode);
2037
- // Store to stub responses
2038
- if (!stubResponses[method]) {
2039
- stubResponses[method] = {};
2040
- }
2041
- stubResponses[method][path] = ((opts.errorMessage || opts.errorCode)
2042
- ? {
2043
- success: false,
2044
- errorMessage,
2045
- errorCode,
2279
+ const reducer$1 = (state, action) => {
2280
+ switch (action.type) {
2281
+ case ActionType$1.ToggleSortColumn: {
2282
+ if (action.param !== state.sortColumnParam) {
2283
+ // Different column param
2284
+ return Object.assign(Object.assign({}, state), { sortColumnParam: action.param, sortType: SortType.Ascending });
2285
+ }
2286
+ if (state.sortType === SortType.Ascending) {
2287
+ // Switch to descending
2288
+ return Object.assign(Object.assign({}, state), { sortType: SortType.Descending });
2289
+ }
2290
+ // Stop sorting by column
2291
+ return Object.assign(Object.assign({}, state), { sortColumnParam: undefined, sortType: SortType.Ascending });
2046
2292
  }
2047
- : {
2048
- success: true,
2049
- body: body !== null && body !== void 0 ? body : undefined,
2050
- });
2293
+ case ActionType$1.ToggleColumnVisibility: {
2294
+ const { columnVisibilityMap } = state;
2295
+ columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
2296
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2297
+ }
2298
+ case ActionType$1.ToggleColVisCusModalVisibility: {
2299
+ return Object.assign(Object.assign({}, state), { columnVisibilityCustomizationModalVisible: !state.columnVisibilityCustomizationModalVisible });
2300
+ }
2301
+ default: {
2302
+ return state;
2303
+ }
2304
+ }
2051
2305
  };
2052
2306
  /*------------------------------------------------------------------------*/
2053
- /* Main */
2307
+ /* Component */
2054
2308
  /*------------------------------------------------------------------------*/
2055
- /**
2056
- * Visit an endpoint on the server [for client only]
2057
- * @author Gabe Abrams
2058
- * @param opts object containing all arguments
2059
- * @param opts.path - the path of the server endpoint
2060
- * @param [opts.method=GET] - the method of the endpoint
2061
- * @param [opts.params] - query/body parameters to include
2062
- * @returns response from server
2063
- */
2064
- const visitServerEndpoint = (opts) => __awaiter(void 0, void 0, void 0, function* () {
2065
- var _a, _b, _c;
2066
- const method = ((_a = opts.method) !== null && _a !== void 0 ? _a : 'GET');
2067
- // Handle stubs
2068
- const stubResponse = (_b = stubResponses[method]) === null || _b === void 0 ? void 0 : _b[opts.path];
2069
- if (stubResponse) {
2070
- // Remove from list
2071
- try {
2072
- stubResponses[method][opts.path] = undefined;
2309
+ const IntelliTable = (props) => {
2310
+ /*------------------------------------------------------------------------*/
2311
+ /* Setup */
2312
+ /*------------------------------------------------------------------------*/
2313
+ var _a;
2314
+ /* -------------- Props ------------- */
2315
+ // Destructure all props
2316
+ const { title, id, data, columns, } = props;
2317
+ /* -------------- State ------------- */
2318
+ // Initial state
2319
+ const initColumnVisibilityMap = {};
2320
+ columns.forEach((column) => {
2321
+ initColumnVisibilityMap[column.param] = !column.startsHidden;
2322
+ });
2323
+ const initialState = {
2324
+ sortColumnParam: undefined,
2325
+ sortType: SortType.Descending,
2326
+ columnVisibilityMap: initColumnVisibilityMap,
2327
+ columnVisibilityCustomizationModalVisible: false,
2328
+ };
2329
+ // Initialize state
2330
+ const [state, dispatch] = React.useReducer(reducer$1, initialState);
2331
+ // Destructure common state
2332
+ const { sortColumnParam, sortType, columnVisibilityMap, columnVisibilityCustomizationModalVisible, } = state;
2333
+ /* ------- Col Vis Customization Modal ------ */
2334
+ if (columnVisibilityCustomizationModalVisible) {
2335
+ // Create modal
2336
+ (React__default["default"].createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2337
+ dispatch({
2338
+ type: ActionType$1.ToggleColVisCusModalVisibility,
2339
+ });
2340
+ } }, columns.map((column) => {
2341
+ return (React__default["default"].createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, text: column.title, onChanged: () => {
2342
+ dispatch({
2343
+ type: ActionType$1.ToggleColumnVisibility,
2344
+ param: column.param,
2345
+ });
2346
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column` }));
2347
+ })));
2348
+ }
2349
+ /*----------------------------------------*/
2350
+ /* Main UI */
2351
+ /*----------------------------------------*/
2352
+ // Table header
2353
+ const headerCells = (columns
2354
+ .filter((column) => {
2355
+ return columnVisibilityMap[column.param];
2356
+ })
2357
+ .map((column) => {
2358
+ // Custom info based on current sort type
2359
+ let sortButtonAriaLabel;
2360
+ let sortIcon = freeSolidSvgIcons.faSort;
2361
+ if (!sortColumnParam) {
2362
+ // Not being sorted yet
2363
+ sortButtonAriaLabel = `sort ascending by ${column.title}`;
2364
+ sortIcon = freeSolidSvgIcons.faSort;
2073
2365
  }
2074
- catch (err) {
2075
- // Ignore
2366
+ else if (column.param === sortColumnParam) {
2367
+ // Already sorted by this column
2368
+ if (sortType === SortType.Ascending) {
2369
+ // Sorted ascending
2370
+ sortButtonAriaLabel = `sort descending by ${column.title}`;
2371
+ sortIcon = freeSolidSvgIcons.faSortDown;
2372
+ }
2373
+ else {
2374
+ // Sorted descending
2375
+ sortButtonAriaLabel = `stop sorting by ${column.title}`;
2376
+ sortIcon = freeSolidSvgIcons.faSortUp;
2377
+ }
2076
2378
  }
2077
- // Success
2078
- if (stubResponse.success) {
2079
- return stubResponse.body;
2379
+ else {
2380
+ // Sorted by a different column
2381
+ sortButtonAriaLabel = `sort ascending by ${column.title}`;
2382
+ sortIcon = freeSolidSvgIcons.faSort;
2080
2383
  }
2081
- // Error
2082
- throw new ErrorWithCode(stubResponse.errorMessage, stubResponse.errorCode);
2384
+ // Create the cell UI
2385
+ return (React__default["default"].createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}` },
2386
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-center flex-row h-100" },
2387
+ React__default["default"].createElement("h4", { className: "m-0" }, column.title),
2388
+ React__default["default"].createElement("div", null,
2389
+ React__default["default"].createElement("button", { type: "button", className: "btn btn-light", "aria-label": sortButtonAriaLabel, onClick: () => {
2390
+ dispatch({
2391
+ type: ActionType$1.ToggleSortColumn,
2392
+ param: column.param,
2393
+ });
2394
+ } },
2395
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: sortIcon }))))));
2396
+ }));
2397
+ const tableHeader = (React__default["default"].createElement("thead", null,
2398
+ React__default["default"].createElement("tr", null, headerCells)));
2399
+ // Sort data
2400
+ let sortedData = [...data];
2401
+ const paramType = (_a = columns.find((column) => {
2402
+ return (column.param === sortColumnParam);
2403
+ })) === null || _a === void 0 ? void 0 : _a.type;
2404
+ const descending = (sortType === SortType.Descending);
2405
+ if (sortColumnParam) {
2406
+ sortedData.sort((a, b) => {
2407
+ const aVal = a[sortColumnParam];
2408
+ const bVal = b[sortColumnParam];
2409
+ // Sort differently based on the data type
2410
+ // > Boolean
2411
+ if (paramType === ParamType$1.Boolean) {
2412
+ if (aVal && !bVal) {
2413
+ return (descending ? -1 : 1);
2414
+ }
2415
+ if (!aVal && bVal) {
2416
+ return (descending ? 1 : -1);
2417
+ }
2418
+ return 0;
2419
+ }
2420
+ // > Number
2421
+ if (paramType === ParamType$1.Int
2422
+ || paramType === ParamType$1.Float) {
2423
+ return (descending
2424
+ ? (bVal - aVal)
2425
+ : (aVal - bVal));
2426
+ }
2427
+ // > String
2428
+ if (paramType === ParamType$1.String) {
2429
+ if (aVal < bVal) {
2430
+ return (descending ? -1 : 1);
2431
+ }
2432
+ if (aVal > bVal) {
2433
+ return (descending ? 1 : -1);
2434
+ }
2435
+ return 0;
2436
+ }
2437
+ // > JSON
2438
+ if (paramType === ParamType$1.JSON) {
2439
+ const aSize = (Array.isArray(aVal)
2440
+ ? aVal.length
2441
+ : Object.keys(aVal).length);
2442
+ const bSize = (Array.isArray(bVal)
2443
+ ? bVal.length
2444
+ : Object.keys(bVal).length);
2445
+ return (descending
2446
+ ? (bSize - aSize)
2447
+ : (aSize - bSize));
2448
+ }
2449
+ // No sort
2450
+ return 0;
2451
+ });
2083
2452
  }
2084
- // Send the request
2085
- const response = yield cacclSendRequest({
2086
- path: opts.path,
2087
- method: (_c = opts.method) !== null && _c !== void 0 ? _c : 'GET',
2088
- params: opts.params,
2453
+ // Table body
2454
+ const rows = sortedData.map((datum) => {
2455
+ // Build cells
2456
+ const cells = (columns
2457
+ .filter((column) => {
2458
+ return columnVisibilityMap[column.param];
2459
+ })
2460
+ .map((column) => {
2461
+ // Get value
2462
+ let value = datum;
2463
+ const paramParts = column.param.split('.');
2464
+ paramParts.forEach((paramPart) => {
2465
+ value = (value !== null && value !== void 0 ? value : {})[paramPart];
2466
+ });
2467
+ let fullValue;
2468
+ let visibleValue;
2469
+ let title = '';
2470
+ if (column.type === ParamType$1.Boolean) {
2471
+ fullValue = !!(value);
2472
+ const noValue = (value === undefined
2473
+ || value === null);
2474
+ visibleValue = (noValue
2475
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCheckCircle }))
2476
+ : (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faXmarkCircle })));
2477
+ title = (fullValue ? 'True' : 'False');
2478
+ }
2479
+ else if (column.type === ParamType$1.Int) {
2480
+ fullValue = Number.parseInt(value, 10);
2481
+ const noValue = Number.isNaN(fullValue);
2482
+ visibleValue = (noValue
2483
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2484
+ : fullValue);
2485
+ title = String(fullValue);
2486
+ }
2487
+ else if (column.type === ParamType$1.Float) {
2488
+ fullValue = Number.parseFloat(value);
2489
+ const noValue = Number.isNaN(fullValue);
2490
+ visibleValue = (noValue
2491
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2492
+ : roundToNumDecimals(fullValue, 2));
2493
+ title = String(fullValue);
2494
+ }
2495
+ else if (column.type === ParamType$1.String) {
2496
+ fullValue = String(value).trim();
2497
+ const noValue = (value.trim().length) === 0;
2498
+ visibleValue = (noValue
2499
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2500
+ : fullValue);
2501
+ title = `"${value}"`;
2502
+ }
2503
+ else if (column.type === ParamType$1.JSON) {
2504
+ fullValue = JSON.stringify(value);
2505
+ const noValue = (Array.isArray(value)
2506
+ ? (!value || value.length === 0)
2507
+ : Object.keys(value !== null && value !== void 0 ? value : {}).length === 0);
2508
+ visibleValue = (noValue
2509
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2510
+ : fullValue);
2511
+ }
2512
+ // Create UI
2513
+ return (React__default["default"].createElement("td", { key: `${datum.id}-${column.param}`, title: title }, visibleValue));
2514
+ }));
2515
+ // Add cells to a row
2516
+ return (React__default["default"].createElement("tr", { key: datum.id }, cells));
2089
2517
  });
2090
- // Check for failure
2091
- if (!response || !response.body) {
2092
- throw new ErrorWithCode('We didn\'t get a response from the server. Please check your internet connection.', ReactKitErrorCode$1.NoResponse);
2518
+ const tableBody = (React__default["default"].createElement("tbody", null, rows));
2519
+ // Build table
2520
+ const table = (React__default["default"].createElement("table", { className: "table table-dark table-striped" },
2521
+ tableHeader,
2522
+ tableBody));
2523
+ // Count the number of hidden columns
2524
+ const numHiddenCols = (Object.values(columnVisibilityMap)
2525
+ .filter((isVisible) => {
2526
+ return !isVisible;
2527
+ })
2528
+ .length);
2529
+ // Build CSV
2530
+ const csv = genCSV(data, columns);
2531
+ // Build main UI
2532
+ return (React__default["default"].createElement("div", { className: `IntelliTable-container-${id}` },
2533
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-center" },
2534
+ React__default["default"].createElement("h3", { className: "m-0" }, title),
2535
+ React__default["default"].createElement("div", { className: "flex-grow-1 text-end" },
2536
+ React__default["default"].createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: `${title}.csv`, csv: csv }),
2537
+ React__default["default"].createElement("button", { type: "button", className: "btn btn-secondary", "aria-label": `show panel for customizing which columns show in table ${title}`, id: `IntelliTable-${id}-show-column-customization-modal`, onClick: () => {
2538
+ dispatch({
2539
+ type: ActionType$1.ToggleColVisCusModalVisibility,
2540
+ });
2541
+ } },
2542
+ "Show/Hide Cols",
2543
+ numHiddenCols > 0 && (React__default["default"].createElement(React__default["default"].Fragment, null,
2544
+ ' ',
2545
+ "(",
2546
+ numHiddenCols,
2547
+ ' ',
2548
+ "hidden)"))))),
2549
+ React__default["default"].createElement("div", { className: `IntelliTable-table-${id}`, style: {
2550
+ overflowX: 'auto',
2551
+ } }, table)));
2552
+ };
2553
+
2554
+ /**
2555
+ * Log reviewer panel that allows users (must be approved admins) to
2556
+ * review logs written by dce-reactkit
2557
+ * @author Gabe Abrams
2558
+ */
2559
+ // Types of filter drawers
2560
+ var FilterDrawer;
2561
+ (function (FilterDrawer) {
2562
+ FilterDrawer["Date"] = "date";
2563
+ FilterDrawer["Context"] = "context";
2564
+ FilterDrawer["Tag"] = "tag";
2565
+ FilterDrawer["Action"] = "action";
2566
+ FilterDrawer["Advanced"] = "advanced";
2567
+ })(FilterDrawer || (FilterDrawer = {}));
2568
+ /*------------------------------------------------------------------------*/
2569
+ /* Style */
2570
+ /*------------------------------------------------------------------------*/
2571
+ const style = `
2572
+ .LogReviewer-outer-container {
2573
+ /* Full Screen */
2574
+ display: inline-block;
2575
+ left: 0;
2576
+ top: 0;
2577
+ width: 100vw;
2578
+ height: 100vh;
2579
+
2580
+ /* On Top and Fixed */
2581
+ position: fixed;
2582
+ z-index: 90000;
2583
+
2584
+ /* Space around contents */
2585
+ padding: 0.5rem;
2586
+
2587
+ /* Dark Background */
2588
+ background-color: rgba(0, 0, 0, 0.7);
2589
+
2590
+ /* No Clickthrough */
2591
+ pointer-events: none;
2592
+ }
2593
+
2594
+ .LogReviewer-inner-container {
2595
+ /* Full screen, rounded modal-like look */
2596
+ display: flex;
2597
+ height: 100%;
2598
+ border: 0.05rem solid black;
2599
+ border-radius: 0.5rem;
2600
+ overflow: hidden;
2601
+
2602
+ /* Place contents in flex column */
2603
+ flex-direction: column;
2604
+
2605
+ /* Re-allow interaction */
2606
+ pointer-events: all;
2607
+ }
2608
+
2609
+ .LogReviewer-header {
2610
+ /* Elements in flex row */
2611
+ display: flex;
2612
+ flex-direction: row;
2613
+ }
2614
+
2615
+ .LogReviewer-header-title {
2616
+ /* Take up remaining width */
2617
+ flex-grow: 1;
2618
+ }
2619
+
2620
+ .LogReviewer-contents {
2621
+ /* Take up remaining height */
2622
+ flex-grow: 1;
2623
+
2624
+ /* Vertical scroll */
2625
+ overflow-y: auto;
2626
+ }
2627
+ `;
2628
+ /*------------------------------------------------------------------------*/
2629
+ /* Static Functions */
2630
+ /*------------------------------------------------------------------------*/
2631
+ /**
2632
+ * Turn a machine-readable name into a human-readable name
2633
+ * @author Gabe Abrams
2634
+ * @param name machine-readable name
2635
+ * @returns human-readable name
2636
+ */
2637
+ const genHumanReadableName = (machineReadableName) => {
2638
+ let humanReadableName = '';
2639
+ // Add chars and spaces
2640
+ const chars = machineReadableName.split('');
2641
+ chars.forEach((char) => {
2642
+ if (/[A-Z]/.test(char)) {
2643
+ // Uppercase! Add a space before
2644
+ humanReadableName += ' ';
2645
+ }
2646
+ humanReadableName += chars;
2647
+ });
2648
+ // Trim and return
2649
+ return humanReadableName.trim();
2650
+ };
2651
+ /* ------------- Actions ------------ */
2652
+ // Types of actions
2653
+ var ActionType;
2654
+ (function (ActionType) {
2655
+ // Show the loading bar
2656
+ ActionType["StartLoading"] = "start-loading";
2657
+ // Finish loading one or more months of logs
2658
+ ActionType["FinishLoading"] = "finish-loading";
2659
+ // Reset filters to initial values
2660
+ ActionType["ResetFilters"] = "reset-filters";
2661
+ // Choose a filter drawer to toggle
2662
+ ActionType["ToggleFilterDrawer"] = "toggle-filter-drawer";
2663
+ // Hide filter drawer
2664
+ ActionType["HideFilterDrawer"] = "hide-filter-drawer";
2665
+ // Handle the date filter state
2666
+ ActionType["UpdateDateFilterState"] = "update-date-filter-state";
2667
+ // Update the context filter state
2668
+ ActionType["UpdateContextFilterState"] = "update-context-filter-state";
2669
+ // Update the tag filter state
2670
+ ActionType["UpdateTagFilterState"] = "update-tag-filter-state";
2671
+ // Update the action and error filter state
2672
+ ActionType["UpdateActionErrorFilterState"] = "update-action-error-filter-state";
2673
+ // Update the advanced filter state
2674
+ ActionType["UpdateAdvancedFilterState"] = "update-advanced-filter-state";
2675
+ })(ActionType || (ActionType = {}));
2676
+ /**
2677
+ * Reducer that executes actions
2678
+ * @author Gabe Abrams
2679
+ * @param state current state
2680
+ * @param action action to execute
2681
+ */
2682
+ const reducer = (state, action) => {
2683
+ switch (action.type) {
2684
+ case ActionType.StartLoading: {
2685
+ return Object.assign(Object.assign({}, state), { loading: true });
2686
+ }
2687
+ case ActionType.FinishLoading: {
2688
+ return Object.assign(Object.assign({}, state), { loading: false, logMap: action.logMap });
2689
+ }
2690
+ case ActionType.ToggleFilterDrawer: {
2691
+ return Object.assign(Object.assign({}, state), { expandedFilterDrawer: (state.expandedFilterDrawer === action.filterDrawer
2692
+ ? undefined // hide
2693
+ : action.filterDrawer) });
2694
+ }
2695
+ case ActionType.HideFilterDrawer: {
2696
+ return Object.assign(Object.assign({}, state), { expandedFilterDrawer: undefined });
2697
+ }
2698
+ case ActionType.ResetFilters: {
2699
+ return Object.assign(Object.assign({}, state), { dateFilterState: action.initDateFilterState, contextFilterState: action.initContextFilterState, tagFilterState: action.initTagFilterState, actionErrorFilterState: action.initActionErrorFilterState, advancedFilterState: action.initAdvancedFilterState });
2700
+ }
2701
+ case ActionType.UpdateDateFilterState: {
2702
+ return Object.assign(Object.assign({}, state), { dateFilterState: action.dateFilterState });
2703
+ }
2704
+ case ActionType.UpdateContextFilterState: {
2705
+ return Object.assign(Object.assign({}, state), { contextFilterState: action.contextFilterState });
2706
+ }
2707
+ case ActionType.UpdateTagFilterState: {
2708
+ return Object.assign(Object.assign({}, state), { tagFilterState: action.tagFilterState });
2709
+ }
2710
+ case ActionType.UpdateActionErrorFilterState: {
2711
+ return Object.assign(Object.assign({}, state), { actionErrorFilterState: action.actionErrorFilterState });
2712
+ }
2713
+ case ActionType.UpdateAdvancedFilterState: {
2714
+ return Object.assign(Object.assign({}, state), { advancedFilterState: action.advancedFilterState });
2715
+ }
2716
+ default: {
2717
+ return state;
2718
+ }
2093
2719
  }
2094
- if (!response.body.success) {
2095
- // Session expired
2096
- if (response.body.code === ReactKitErrorCode$1.SessionExpired) {
2097
- // Skip notice if session was already expired
2098
- if (sessionAlreadyExpired) {
2099
- // Never return (browser is already reloading)
2100
- yield new Promise(() => {
2101
- // Promise that never returns
2720
+ };
2721
+ /*------------------------------------------------------------------------*/
2722
+ /* Component */
2723
+ /*------------------------------------------------------------------------*/
2724
+ const LogReviewer = (props) => {
2725
+ /*------------------------------------------------------------------------*/
2726
+ /* Setup */
2727
+ /*------------------------------------------------------------------------*/
2728
+ var _a, _b, _c, _d, _e;
2729
+ /* -------------- Props ------------- */
2730
+ // Destructure props
2731
+ const { LogMetadata, onClose, } = props;
2732
+ /* -------------- State ------------- */
2733
+ // Create initial date filter state
2734
+ const today = getTimeInfoInET();
2735
+ const initStartDate = {
2736
+ year: today.year,
2737
+ month: today.month,
2738
+ day: 1,
2739
+ };
2740
+ const initEndDate = {
2741
+ year: today.year,
2742
+ month: today.month,
2743
+ day: today.day,
2744
+ };
2745
+ const initDateFilterState = {
2746
+ startDate: initStartDate,
2747
+ endDate: initEndDate,
2748
+ };
2749
+ // Create initial context filter state
2750
+ const initContextFilterState = {};
2751
+ Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
2752
+ var _a, _b;
2753
+ const contextValue = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
2754
+ if (typeof contextValue === 'string') {
2755
+ // Case: no subcontexts
2756
+ initContextFilterState[contextValue] = true;
2757
+ }
2758
+ else {
2759
+ // Case: subcontexts exist
2760
+ initContextFilterState[contextValue._] = {};
2761
+ Object.values(((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {})[context]).forEach((subcontext) => {
2762
+ const subcontextValue = contextValue[subcontext];
2763
+ initContextFilterState[contextValue._][subcontextValue] = true;
2764
+ });
2765
+ }
2766
+ });
2767
+ // Create initial tag filter state
2768
+ const initTagFilterState = {};
2769
+ Object.values((_b = LogMetadata.Tag) !== null && _b !== void 0 ? _b : {}).forEach((tagValue) => {
2770
+ initTagFilterState[tagValue] = true;
2771
+ });
2772
+ // Create advanced filter state
2773
+ const initAdvancedFilterState = {
2774
+ userFirstName: '',
2775
+ userLastName: '',
2776
+ userEmail: '',
2777
+ userId: '',
2778
+ includeLearners: true,
2779
+ includeTTMs: true,
2780
+ includeAdmins: true,
2781
+ courseId: '',
2782
+ courseName: '',
2783
+ isMobile: undefined,
2784
+ source: undefined,
2785
+ routePath: '',
2786
+ routeTemplate: '',
2787
+ };
2788
+ // Create action and error filter state
2789
+ const initActionErrorFilterState = {
2790
+ type: undefined,
2791
+ errorMessage: '',
2792
+ errorCode: '',
2793
+ target: {},
2794
+ action: {},
2795
+ };
2796
+ Object.values((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {}).forEach((target) => {
2797
+ initActionErrorFilterState.target[target] = true;
2798
+ });
2799
+ Object.values(LogAction$1).forEach((action) => {
2800
+ initActionErrorFilterState.action[action] = true;
2801
+ });
2802
+ // Initial state
2803
+ const initialState = {
2804
+ loading: true,
2805
+ logMap: {},
2806
+ expandedFilterDrawer: undefined,
2807
+ dateFilterState: initDateFilterState,
2808
+ contextFilterState: initContextFilterState,
2809
+ tagFilterState: initTagFilterState,
2810
+ actionErrorFilterState: initActionErrorFilterState,
2811
+ advancedFilterState: initAdvancedFilterState,
2812
+ };
2813
+ // Initialize state
2814
+ const [state, dispatch] = React.useReducer(reducer, initialState);
2815
+ // Destructure common state
2816
+ const { loading, logMap, expandedFilterDrawer, dateFilterState, contextFilterState, tagFilterState, actionErrorFilterState, advancedFilterState, } = state;
2817
+ /*------------------------------------------------------------------------*/
2818
+ /* Component Functions */
2819
+ /*------------------------------------------------------------------------*/
2820
+ /**
2821
+ * Get the list of year/month combos that need to be loaded given a new
2822
+ * start or end date and the existing logMap
2823
+ * @author Gabe Abrams
2824
+ * @param newDateFilterState the new date filter state
2825
+ * @returns list of year/month combos that need to be loaded
2826
+ */
2827
+ const listMonthsToLoad = (newDateFilterState) => {
2828
+ // List of year/month combos that need to be loaded
2829
+ const toLoad = [];
2830
+ // Loop through dates
2831
+ let year = newDateFilterState.startDate.year;
2832
+ let month = newDateFilterState.startDate.month;
2833
+ while (
2834
+ // Earlier year
2835
+ (year <= newDateFilterState.endDate.year)
2836
+ // Current year but included month
2837
+ || (year === newDateFilterState.endDate.year
2838
+ && month <= newDateFilterState.endDate.month)) {
2839
+ // Add to list
2840
+ toLoad.push({
2841
+ year,
2842
+ month,
2843
+ });
2844
+ // Increment
2845
+ month += 1;
2846
+ if (month > 12) {
2847
+ month -= 12;
2848
+ year += 1;
2849
+ }
2850
+ }
2851
+ // Return
2852
+ return toLoad;
2853
+ };
2854
+ /**
2855
+ * Handle updated start/end dates (updates state, loads if necessary)
2856
+ * @author Gabe Abrams
2857
+ * @param newDateFilterState the new date filter state
2858
+ */
2859
+ const handleDateRangeUpdated = (newDateFilterState) => __awaiter(void 0, void 0, void 0, function* () {
2860
+ // Update state
2861
+ dispatch({
2862
+ type: ActionType.UpdateDateFilterState,
2863
+ dateFilterState: newDateFilterState,
2864
+ });
2865
+ // Check which year/month combos we need to load
2866
+ const toLoad = listMonthsToLoad(newDateFilterState);
2867
+ // If nothing to load, finished
2868
+ if (toLoad.length === 0) {
2869
+ return;
2870
+ }
2871
+ // Start loading
2872
+ dispatch({
2873
+ type: ActionType.StartLoading,
2874
+ });
2875
+ // Load required months
2876
+ try {
2877
+ for (let i = 0; i < toLoad.length; i++) {
2878
+ // Destructure
2879
+ const { year, month } = toLoad[i];
2880
+ // Load
2881
+ const logs = yield visitServerEndpoint({
2882
+ path: `${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/${year}/months/${month}`,
2883
+ method: 'GET',
2102
2884
  });
2885
+ // Add to map
2886
+ if (!logMap[year]) {
2887
+ logMap[year] = {};
2888
+ }
2889
+ logMap[year][month] = logs;
2103
2890
  }
2104
- sessionAlreadyExpired = true;
2105
- // Show session expiration message
2106
- {
2107
- // Fallback to alert
2108
- // eslint-disable-next-line no-alert
2109
- alert('Your session has expired. Please start over.');
2891
+ }
2892
+ catch (err) {
2893
+ return showFatalError(err);
2894
+ }
2895
+ // Finish loading
2896
+ dispatch({
2897
+ type: ActionType.FinishLoading,
2898
+ logMap,
2899
+ });
2900
+ });
2901
+ /*------------------------------------------------------------------------*/
2902
+ /* Lifecycle Functions */
2903
+ /*------------------------------------------------------------------------*/
2904
+ /**
2905
+ * Mount
2906
+ * @author Gabe Abrams
2907
+ */
2908
+ React.useEffect(() => {
2909
+ // Perform initial load
2910
+ handleDateRangeUpdated(dateFilterState);
2911
+ }, []);
2912
+ /*------------------------------------------------------------------------*/
2913
+ /* Render */
2914
+ /*------------------------------------------------------------------------*/
2915
+ /*----------------------------------------*/
2916
+ /* Main UI */
2917
+ /*----------------------------------------*/
2918
+ // Body that will be filled with the contents of the panel
2919
+ let body;
2920
+ /* ------------- Loading ------------ */
2921
+ if (loading) {
2922
+ body = (React__default["default"].createElement("div", { className: "text-center p-5" },
2923
+ React__default["default"].createElement(LoadingSpinner, null)));
2924
+ }
2925
+ /* ------------ Review UI ----------- */
2926
+ if (!loading) {
2927
+ /*----------------------------------------*/
2928
+ /* Filters */
2929
+ /*----------------------------------------*/
2930
+ // Filter toggle
2931
+ const filterToggles = (React__default["default"].createElement("div", { className: "LogReviewer-filter-toggles d-flex align-items-center justify-content-center" },
2932
+ React__default["default"].createElement("h3", { className: "m-0" }, "Filters:"),
2933
+ React__default["default"].createElement("div", { className: "LogReviewer-filter-toggle-buttons" },
2934
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-date-filter-drawer", className: `btn btn-${FilterDrawer.Date === expandedFilterDrawer} me-2`, "aria-label": "toggle date filter drawer", onClick: () => {
2935
+ dispatch({
2936
+ type: ActionType.ToggleFilterDrawer,
2937
+ filterDrawer: FilterDrawer.Date,
2938
+ });
2939
+ } },
2940
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCalendar, className: "me-2" }),
2941
+ "Date"),
2942
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-context-filter-drawer", className: `btn btn-${FilterDrawer.Context === expandedFilterDrawer} me-2`, "aria-label": "toggle context filter drawer", onClick: () => {
2943
+ dispatch({
2944
+ type: ActionType.ToggleFilterDrawer,
2945
+ filterDrawer: FilterDrawer.Context,
2946
+ });
2947
+ } },
2948
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "me-2" }),
2949
+ "Context"),
2950
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-tag-filter-drawer", className: `btn btn-${FilterDrawer.Tag === expandedFilterDrawer} me-2`, "aria-label": "toggle tag filter drawer", onClick: () => {
2951
+ dispatch({
2952
+ type: ActionType.ToggleFilterDrawer,
2953
+ filterDrawer: FilterDrawer.Tag,
2954
+ });
2955
+ } },
2956
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTag, className: "me-2" }),
2957
+ "Tag"),
2958
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-action-filter-drawer", className: `btn btn-${FilterDrawer.Action === expandedFilterDrawer} me-2`, "aria-label": "toggle action and error filter drawer", onClick: () => {
2959
+ dispatch({
2960
+ type: ActionType.ToggleFilterDrawer,
2961
+ filterDrawer: FilterDrawer.Action,
2962
+ });
2963
+ } },
2964
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faHammer, className: "me-2" }),
2965
+ "Action"),
2966
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-advanced-filter-drawer", className: `btn btn-${FilterDrawer.Advanced === expandedFilterDrawer}`, "aria-label": "toggle advanced filter drawer", onClick: () => {
2967
+ dispatch({
2968
+ type: ActionType.ToggleFilterDrawer,
2969
+ filterDrawer: FilterDrawer.Advanced,
2970
+ });
2971
+ } },
2972
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faList, className: "me-2" }),
2973
+ "Advanced"))));
2974
+ // Filter drawer
2975
+ let filterDrawer;
2976
+ if (expandedFilterDrawer) {
2977
+ if (expandedFilterDrawer === FilterDrawer.Date) {
2978
+ filterDrawer = (React__default["default"].createElement(TabBox, { title: "Date" },
2979
+ React__default["default"].createElement(SimpleDateChooser, { ariaLabel: "filter start date", name: "filter-start-date", year: dateFilterState.startDate.year, month: dateFilterState.startDate.month, day: dateFilterState.startDate.day, onChange: (month, day, year) => {
2980
+ dispatch({
2981
+ type: ActionType.UpdateDateFilterState,
2982
+ dateFilterState: Object.assign(Object.assign({}, dateFilterState), { startDate: { month, day, year } }),
2983
+ });
2984
+ } }),
2985
+ ' ',
2986
+ "to",
2987
+ ' ',
2988
+ React__default["default"].createElement(SimpleDateChooser, { ariaLabel: "filter end date", name: "filter-end-date", year: dateFilterState.endDate.year, month: dateFilterState.endDate.month, day: dateFilterState.endDate.day, onChange: (month, day, year) => {
2989
+ dispatch({
2990
+ type: ActionType.UpdateDateFilterState,
2991
+ dateFilterState: Object.assign(Object.assign({}, dateFilterState), { endDate: { month, day, year } }),
2992
+ });
2993
+ } })));
2994
+ }
2995
+ else if (expandedFilterDrawer === FilterDrawer.Context) {
2996
+ // Create item picker items
2997
+ const pickableItems = (Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {})
2998
+ .map((context) => {
2999
+ var _a;
3000
+ const value = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
3001
+ if (typeof value === 'string') {
3002
+ // No subcategories
3003
+ const item = {
3004
+ id: context,
3005
+ name: genHumanReadableName(context),
3006
+ isGroup: false,
3007
+ checked: !!contextFilterState[context],
3008
+ };
3009
+ return item;
3010
+ }
3011
+ // Has subcategories
3012
+ const children = (Object.keys(value)
3013
+ .filter((subcontext) => {
3014
+ return subcontext !== '_';
3015
+ })
3016
+ .map((subcontext) => {
3017
+ return {
3018
+ id: `${context}-${subcontext}`,
3019
+ name: genHumanReadableName(subcontext),
3020
+ isGroup: false,
3021
+ checked: !!value[subcontext],
3022
+ };
3023
+ }));
3024
+ const item = {
3025
+ id: context,
3026
+ name: context,
3027
+ isGroup: true,
3028
+ children,
3029
+ };
3030
+ return item;
3031
+ }));
3032
+ // Create filter UI
3033
+ filterDrawer = (React__default["default"].createElement(ItemPicker, { title: "Context", items: pickableItems, onChanged: (updatedItems) => {
3034
+ // Update our state
3035
+ updatedItems.forEach((pickableItem) => {
3036
+ if (pickableItem.isGroup) {
3037
+ // Has subcontexts
3038
+ pickableItem.children.forEach((subcontextItem) => {
3039
+ contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3040
+ });
3041
+ }
3042
+ else {
3043
+ // No subcontexts
3044
+ contextFilterState[pickableItem.id] = (pickableItem.checked);
3045
+ }
3046
+ });
3047
+ dispatch({
3048
+ type: ActionType.UpdateContextFilterState,
3049
+ contextFilterState,
3050
+ });
3051
+ } }));
3052
+ }
3053
+ else if (expandedFilterDrawer === FilterDrawer.Tag) {
3054
+ // Create filter UI
3055
+ filterDrawer = (React__default["default"].createElement(TabBox, { title: "Tags" }, Object.keys(tagFilterState)
3056
+ .map((tag, i) => {
3057
+ const description = genHumanReadableName(tag);
3058
+ return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-tag-${tag}-checkbox`, text: description, ariaLabel: `include logs tagged with "${description}" in results`, noMarginOnRight: i === Object.keys(tagFilterState).length - 1, onChanged: (checked) => {
3059
+ tagFilterState[tag] = checked;
3060
+ } }));
3061
+ })));
3062
+ }
3063
+ else if (expandedFilterDrawer === FilterDrawer.Action) {
3064
+ // Create filter UI
3065
+ filterDrawer = (React__default["default"].createElement(React__default["default"].Fragment, null,
3066
+ React__default["default"].createElement(TabBox, { title: "Log Type" },
3067
+ React__default["default"].createElement(RadioButton, { id: "LogReviewer-type-all", text: "All Logs", onSelected: () => {
3068
+ actionErrorFilterState.type = undefined;
3069
+ dispatch({
3070
+ type: ActionType.UpdateActionErrorFilterState,
3071
+ actionErrorFilterState,
3072
+ });
3073
+ }, ariaLabel: "show logs of all types", selected: actionErrorFilterState.type === undefined }),
3074
+ React__default["default"].createElement(RadioButton, { id: "LogReviewer-type-action-only", text: "Action Logs Only", onSelected: () => {
3075
+ actionErrorFilterState.type = LogType$1.Action;
3076
+ dispatch({
3077
+ type: ActionType.UpdateActionErrorFilterState,
3078
+ actionErrorFilterState,
3079
+ });
3080
+ }, ariaLabel: "only show action logs", selected: actionErrorFilterState.type === LogType$1.Action }),
3081
+ React__default["default"].createElement(RadioButton, { id: "LogReviewer-type-error-only", text: "Action Error Only", onSelected: () => {
3082
+ actionErrorFilterState.type = LogType$1.Error;
3083
+ dispatch({
3084
+ type: ActionType.UpdateActionErrorFilterState,
3085
+ actionErrorFilterState,
3086
+ });
3087
+ }, ariaLabel: "only show error logs", selected: actionErrorFilterState.type === LogType$1.Error, noMarginOnRight: true })),
3088
+ (actionErrorFilterState.type === undefined
3089
+ || actionErrorFilterState.type === LogType$1.Action) && (React__default["default"].createElement(TabBox, { title: "Action Log Details" },
3090
+ React__default["default"].createElement(ButtonInputGroup, { label: "Action" }, Object.keys(LogAction$1)
3091
+ .map((action, i) => {
3092
+ const description = genHumanReadableName(action);
3093
+ return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-action-${action}-checkbox`, text: description, ariaLabel: `include logs with action type "${description}" in results`, noMarginOnRight: i === Object.keys(LogAction$1).length - 1, onChanged: (checked) => {
3094
+ actionErrorFilterState.action[action] = checked;
3095
+ dispatch({
3096
+ type: ActionType.UpdateActionErrorFilterState,
3097
+ actionErrorFilterState,
3098
+ });
3099
+ } }));
3100
+ })),
3101
+ React__default["default"].createElement(ButtonInputGroup, { label: "Target" }, Object.keys((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {})
3102
+ .map((target, i) => {
3103
+ var _a;
3104
+ const description = genHumanReadableName(target);
3105
+ return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, onChanged: (checked) => {
3106
+ actionErrorFilterState.target[target] = checked;
3107
+ dispatch({
3108
+ type: ActionType.UpdateActionErrorFilterState,
3109
+ actionErrorFilterState,
3110
+ });
3111
+ }, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
3112
+ })))),
3113
+ (actionErrorFilterState.type === undefined
3114
+ || actionErrorFilterState.type === LogType$1.Error) && (React__default["default"].createElement(TabBox, { title: "Error Log Details" },
3115
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3116
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Error Message"),
3117
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error message", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3118
+ actionErrorFilterState.errorMessage = e.target.value;
3119
+ dispatch({
3120
+ type: ActionType.UpdateActionErrorFilterState,
3121
+ actionErrorFilterState,
3122
+ });
3123
+ } })),
3124
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3125
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Error Code"),
3126
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3127
+ actionErrorFilterState.errorCode = ((e.target.value)
3128
+ .trim()
3129
+ .toUpperCase());
3130
+ dispatch({
3131
+ type: ActionType.UpdateActionErrorFilterState,
3132
+ actionErrorFilterState,
3133
+ });
3134
+ } }))))));
3135
+ }
3136
+ else if (expandedFilterDrawer === FilterDrawer.Advanced) {
3137
+ // Create advanced filter ui
3138
+ filterDrawer = (React__default["default"].createElement(React__default["default"].Fragment, null,
3139
+ React__default["default"].createElement(TabBox, { title: "User Info" },
3140
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3141
+ React__default["default"].createElement("span", { className: "input-group-text" }, "User First Name"),
3142
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, onChange: (e) => {
3143
+ advancedFilterState.userFirstName = e.target.value;
3144
+ dispatch({
3145
+ type: ActionType.UpdateAdvancedFilterState,
3146
+ advancedFilterState,
3147
+ });
3148
+ } })),
3149
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3150
+ React__default["default"].createElement("span", { className: "input-group-text" }, "User Last Name"),
3151
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, onChange: (e) => {
3152
+ advancedFilterState.userLastName = e.target.value;
3153
+ dispatch({
3154
+ type: ActionType.UpdateAdvancedFilterState,
3155
+ advancedFilterState,
3156
+ });
3157
+ } })),
3158
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3159
+ React__default["default"].createElement("span", { className: "input-group-text" }, "User Email"),
3160
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user email", value: advancedFilterState.userEmail, onChange: (e) => {
3161
+ advancedFilterState.userEmail = ((e.target.value)
3162
+ .trim());
3163
+ dispatch({
3164
+ type: ActionType.UpdateAdvancedFilterState,
3165
+ advancedFilterState,
3166
+ });
3167
+ } })),
3168
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3169
+ React__default["default"].createElement("span", { className: "input-group-text" }, "User Canvas Id"),
3170
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, onChange: (e) => {
3171
+ const { value } = e.target;
3172
+ // Only update if value contains only numbers
3173
+ if (/^\d+$/.test(value)) {
3174
+ advancedFilterState.userId = ((e.target.value)
3175
+ .trim());
3176
+ }
3177
+ dispatch({
3178
+ type: ActionType.UpdateAdvancedFilterState,
3179
+ advancedFilterState,
3180
+ });
3181
+ } })),
3182
+ React__default["default"].createElement(ButtonInputGroup, { label: "Role" },
3183
+ React__default["default"].createElement(CheckboxButton, { text: "Students", onChanged: (checked) => {
3184
+ advancedFilterState.includeLearners = checked;
3185
+ dispatch({
3186
+ type: ActionType.UpdateAdvancedFilterState,
3187
+ advancedFilterState,
3188
+ });
3189
+ }, checked: advancedFilterState.includeLearners, ariaLabel: "show logs from students" }),
3190
+ React__default["default"].createElement(CheckboxButton, { text: "Teaching Team Members", onChanged: (checked) => {
3191
+ advancedFilterState.includeTTMs = checked;
3192
+ dispatch({
3193
+ type: ActionType.UpdateAdvancedFilterState,
3194
+ advancedFilterState,
3195
+ });
3196
+ }, checked: advancedFilterState.includeTTMs, ariaLabel: "show logs from teaching team members" }),
3197
+ React__default["default"].createElement(CheckboxButton, { text: "Admins", onChanged: (checked) => {
3198
+ advancedFilterState.includeAdmins = checked;
3199
+ dispatch({
3200
+ type: ActionType.UpdateAdvancedFilterState,
3201
+ advancedFilterState,
3202
+ });
3203
+ }, checked: advancedFilterState.includeAdmins, ariaLabel: "show logs from admins" }))),
3204
+ React__default["default"].createElement(TabBox, { title: "Course Info" },
3205
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3206
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Course Name"),
3207
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, onChange: (e) => {
3208
+ advancedFilterState.courseName = e.target.value;
3209
+ dispatch({
3210
+ type: ActionType.UpdateAdvancedFilterState,
3211
+ advancedFilterState,
3212
+ });
3213
+ } })),
3214
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3215
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Course Canvas Id"),
3216
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, onChange: (e) => {
3217
+ const { value } = e.target;
3218
+ // Only update if value contains only numbers
3219
+ if (/^\d+$/.test(value)) {
3220
+ advancedFilterState.courseId = ((e.target.value)
3221
+ .trim());
3222
+ }
3223
+ dispatch({
3224
+ type: ActionType.UpdateAdvancedFilterState,
3225
+ advancedFilterState,
3226
+ });
3227
+ } }))),
3228
+ React__default["default"].createElement(TabBox, { title: "Device Info" },
3229
+ React__default["default"].createElement(ButtonInputGroup, { label: "Device Type" },
3230
+ React__default["default"].createElement(RadioButton, { text: "All Devices", ariaLabel: "show logs from all devices", selected: advancedFilterState.isMobile === undefined, onSelected: () => {
3231
+ advancedFilterState.isMobile = undefined;
3232
+ dispatch({
3233
+ type: ActionType.UpdateAdvancedFilterState,
3234
+ advancedFilterState,
3235
+ });
3236
+ } }),
3237
+ React__default["default"].createElement(RadioButton, { text: "Mobile Only", ariaLabel: "show logs from mobile devices", selected: advancedFilterState.isMobile === true, onSelected: () => {
3238
+ advancedFilterState.isMobile = true;
3239
+ dispatch({
3240
+ type: ActionType.UpdateAdvancedFilterState,
3241
+ advancedFilterState,
3242
+ });
3243
+ } }),
3244
+ React__default["default"].createElement(RadioButton, { text: "Desktop Only", ariaLabel: "show logs from desktop devices", selected: advancedFilterState.isMobile === false, onSelected: () => {
3245
+ advancedFilterState.isMobile = false;
3246
+ dispatch({
3247
+ type: ActionType.UpdateAdvancedFilterState,
3248
+ advancedFilterState,
3249
+ });
3250
+ }, noMarginOnRight: true }))),
3251
+ React__default["default"].createElement(TabBox, { title: "Source" },
3252
+ React__default["default"].createElement(ButtonInputGroup, { label: "Source Type" },
3253
+ React__default["default"].createElement(RadioButton, { text: "Both", ariaLabel: "show logs from all sources", selected: advancedFilterState.source === undefined, onSelected: () => {
3254
+ advancedFilterState.source = undefined;
3255
+ dispatch({
3256
+ type: ActionType.UpdateAdvancedFilterState,
3257
+ advancedFilterState,
3258
+ });
3259
+ } }),
3260
+ React__default["default"].createElement(RadioButton, { text: "Client Only", ariaLabel: "show logs from client source", selected: advancedFilterState.source === LogSource$1.Client, onSelected: () => {
3261
+ advancedFilterState.source = LogSource$1.Client;
3262
+ dispatch({
3263
+ type: ActionType.UpdateAdvancedFilterState,
3264
+ advancedFilterState,
3265
+ });
3266
+ } }),
3267
+ React__default["default"].createElement(RadioButton, { text: "Server Only", ariaLabel: "show logs from server source", selected: advancedFilterState.source === LogSource$1.Server, onSelected: () => {
3268
+ advancedFilterState.source = LogSource$1.Server;
3269
+ dispatch({
3270
+ type: ActionType.UpdateAdvancedFilterState,
3271
+ advancedFilterState,
3272
+ });
3273
+ }, noMarginOnRight: true })),
3274
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3275
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Path"),
3276
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route path", placeholder: "e.g. /api/ttm/courses/12345", value: advancedFilterState.routePath, onChange: (e) => {
3277
+ advancedFilterState.courseName = ((e.target.value)
3278
+ .trim());
3279
+ dispatch({
3280
+ type: ActionType.UpdateAdvancedFilterState,
3281
+ advancedFilterState,
3282
+ });
3283
+ } })),
3284
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3285
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Template"),
3286
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route template", value: advancedFilterState.routeTemplate, placeholder: "e.g. /api/ttm/courses/:courseId", onChange: (e) => {
3287
+ advancedFilterState.courseName = ((e.target.value)
3288
+ .trim());
3289
+ dispatch({
3290
+ type: ActionType.UpdateAdvancedFilterState,
3291
+ advancedFilterState,
3292
+ });
3293
+ } })))));
2110
3294
  }
2111
- // Never return (don't continue execution)
2112
- yield new Promise(() => {
2113
- // Promise that never returns
2114
- });
2115
3295
  }
2116
- // Other errors
2117
- throw new ErrorWithCode((response.body.message
2118
- || 'An unknown error occurred. Please contact an admin.'), (response.body.code
2119
- || ReactKitErrorCode$1.NoCode));
3296
+ // Filters UI
3297
+ const filters = (React__default["default"].createElement(React__default["default"].Fragment, null,
3298
+ filterToggles,
3299
+ filterDrawer && (React__default["default"].createElement(Drawer, null, filterDrawer))));
3300
+ // Actually filter the logs
3301
+ // > Perform filters
3302
+ const logs = [];
3303
+ Object.keys(logMap).forEach((year) => {
3304
+ Object.keys(logMap).forEach((month) => {
3305
+ logMap[year][month].forEach((log) => {
3306
+ /* ----------- Date Filter ---------- */
3307
+ var _a;
3308
+ // Before start date
3309
+ if (
3310
+ // Previous year
3311
+ log.year < dateFilterState.startDate.year
3312
+ // Same year, earlier month
3313
+ || ((log.year === dateFilterState.startDate.year)
3314
+ && (log.month < dateFilterState.startDate.month))
3315
+ // Same year, same month, earlier day
3316
+ || ((log.year === dateFilterState.startDate.year)
3317
+ && (log.month === dateFilterState.startDate.month)
3318
+ && (log.day < dateFilterState.startDate.day))) {
3319
+ return;
3320
+ }
3321
+ // After end date
3322
+ if (
3323
+ // Later year
3324
+ log.year > dateFilterState.endDate.year
3325
+ // Same year, later month
3326
+ || ((log.year === dateFilterState.endDate.year)
3327
+ && (log.month > dateFilterState.endDate.month))
3328
+ // Same year, same month, later day
3329
+ || ((log.year === dateFilterState.endDate.year)
3330
+ && (log.month === dateFilterState.endDate.month)
3331
+ && (log.day > dateFilterState.endDate.day))) {
3332
+ return;
3333
+ }
3334
+ /* --------- Context Filter --------- */
3335
+ // Context doesn't match
3336
+ if (
3337
+ // Whole context is deselected
3338
+ contextFilterState[log.context] === false
3339
+ // None of the subcontexts are selected
3340
+ || (Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3341
+ .every((isSelected) => {
3342
+ return !isSelected;
3343
+ }))) {
3344
+ return;
3345
+ }
3346
+ // Subcontext doesn't match
3347
+ if (
3348
+ // Log has a subcontext
3349
+ log.subcontext
3350
+ // Context has subcontexts
3351
+ && (contextFilterState[log.context]
3352
+ && contextFilterState[log.context] !== false
3353
+ && contextFilterState[log.context] !== true)
3354
+ // Subcontext is not selected
3355
+ && !contextFilterState[log.context][log.subcontext]) {
3356
+ return;
3357
+ }
3358
+ /* -------------- Tags -------------- */
3359
+ // No tags match
3360
+ if (
3361
+ // Log has at least one tag
3362
+ log.tags.length > 0
3363
+ // No tags match
3364
+ && (log.tags.every((tag) => {
3365
+ return !tagFilterState[tag];
3366
+ }))) {
3367
+ return;
3368
+ }
3369
+ /* ------- Actions and Errors ------- */
3370
+ // Log type doesn't match
3371
+ if (
3372
+ // Filter won't allow all types
3373
+ actionErrorFilterState.type !== undefined
3374
+ // Log type doesn't match
3375
+ && actionErrorFilterState.type !== log.type) {
3376
+ return;
3377
+ }
3378
+ // Filter errors
3379
+ if (log.type === LogType$1.Error) {
3380
+ // Message doesn't match
3381
+ if (
3382
+ // Message exists
3383
+ log.errorMessage
3384
+ // Message filter exists
3385
+ && actionErrorFilterState.errorMessage.trim().length > 0
3386
+ // Message doesn't match
3387
+ && log.errorMessage.toLowerCase().includes(actionErrorFilterState.errorMessage.trim().toLowerCase())) {
3388
+ return;
3389
+ }
3390
+ // Code doesn't match
3391
+ if (
3392
+ // Code exists
3393
+ log.errorCode
3394
+ // Code filter exists
3395
+ && actionErrorFilterState.errorCode.trim().length > 0
3396
+ // Code doesn't match
3397
+ && log.errorCode.toUpperCase().includes(actionErrorFilterState.errorCode.trim().toUpperCase())) {
3398
+ return;
3399
+ }
3400
+ }
3401
+ // Filter actions
3402
+ if (log.type === LogType$1.Action) {
3403
+ // Target isn't selected
3404
+ if (
3405
+ // Target exists
3406
+ log.target
3407
+ // Target isn't selected
3408
+ && !actionErrorFilterState.target[log.target]) {
3409
+ return;
3410
+ }
3411
+ // Action
3412
+ if (
3413
+ // Action exists
3414
+ log.action
3415
+ // Action isn't selected
3416
+ && !actionErrorFilterState.action[log.action]) {
3417
+ return;
3418
+ }
3419
+ }
3420
+ /* --------- Advanced Filter -------- */
3421
+ // First name doesn't match
3422
+ if (
3423
+ // First name exists
3424
+ log.userFirstName
3425
+ // First name query doesn't match
3426
+ && !log.userFirstName.toLowerCase().includes(advancedFilterState.userFirstName.toLowerCase().trim())) {
3427
+ return;
3428
+ }
3429
+ // Last name doesn't match
3430
+ if (
3431
+ // Last name exists
3432
+ log.userLastName
3433
+ // Last name query doesn't match
3434
+ && !log.userLastName.toLowerCase().includes(advancedFilterState.userLastName.toLowerCase().trim())) {
3435
+ return;
3436
+ }
3437
+ // Email doesn't match
3438
+ if (
3439
+ // Email exists
3440
+ log.userEmail
3441
+ // Email query doesn't match
3442
+ && !log.userEmail.toLowerCase().includes(advancedFilterState.userEmail.toLowerCase().trim())) {
3443
+ return;
3444
+ }
3445
+ // User id doesn't match
3446
+ if (
3447
+ // User id exists
3448
+ log.userId
3449
+ // User id doesn't match
3450
+ && !String(log.userId).includes(advancedFilterState.userId.trim())) {
3451
+ return;
3452
+ }
3453
+ // Learner not allowed
3454
+ if (
3455
+ // User is a learner
3456
+ log.isLearner
3457
+ // Learners aren't included
3458
+ && !advancedFilterState.includeLearners) {
3459
+ return;
3460
+ }
3461
+ // TTM not allowed
3462
+ if (
3463
+ // User is a ttm
3464
+ log.isTTM
3465
+ // TTMs aren't included
3466
+ && !advancedFilterState.includeTTMs) {
3467
+ return;
3468
+ }
3469
+ // Admin not allowed
3470
+ if (
3471
+ // User is an admin
3472
+ log.isAdmin
3473
+ // Admins aren't included
3474
+ && !advancedFilterState.includeAdmins) {
3475
+ return;
3476
+ }
3477
+ // Course Id doesn't match
3478
+ if (
3479
+ // Course Id exists
3480
+ log.courseId
3481
+ // Course Id doesn't match
3482
+ && !String(log.courseId).includes(advancedFilterState.courseId.trim())) {
3483
+ return;
3484
+ }
3485
+ // Course name doesn't match
3486
+ if (
3487
+ // Course name exists
3488
+ log.courseName
3489
+ // Course name doesn't match
3490
+ && !String(log.courseName).includes(advancedFilterState.courseName.trim())) {
3491
+ return;
3492
+ }
3493
+ // Mobile filter doesn't match
3494
+ if (
3495
+ // Mobile filter exists
3496
+ advancedFilterState.isMobile !== undefined
3497
+ // Device info exists
3498
+ && log.device
3499
+ // Mobile filter doesn't match
3500
+ && (advancedFilterState.isMobile === log.device.isMobile)) {
3501
+ return;
3502
+ }
3503
+ // Log source doesn't match
3504
+ if (
3505
+ // Source filter exists
3506
+ advancedFilterState.source !== undefined
3507
+ // Source info exists
3508
+ && log.source
3509
+ // Source filter doesn't match
3510
+ && (advancedFilterState.source !== log.source)) {
3511
+ return;
3512
+ }
3513
+ // Route path doesn't match (Only for server source)
3514
+ if (
3515
+ // Source is server
3516
+ (log.source === LogSource$1.Server)
3517
+ // Route path is being filtered
3518
+ && (advancedFilterState.routePath.trim().length)
3519
+ // Route path doesn't match
3520
+ && !(log.routePath.includes(advancedFilterState.routePath.trim()))) {
3521
+ return;
3522
+ }
3523
+ // Route template doesn't match (Only for server source)
3524
+ if (
3525
+ // Source is server
3526
+ (log.source === LogSource$1.Server)
3527
+ // Route template is being filtered
3528
+ && (advancedFilterState.routeTemplate.trim().length)
3529
+ // Route template doesn't match
3530
+ && !(log.routeTemplate.includes(advancedFilterState.routeTemplate.trim()))) {
3531
+ return;
3532
+ }
3533
+ /* -------------- Done -------------- */
3534
+ // Made it past all filters. Add to the list
3535
+ logs.push(log);
3536
+ });
3537
+ });
3538
+ });
3539
+ /*----------------------------------------*/
3540
+ /* Data */
3541
+ /*----------------------------------------*/
3542
+ // Create data table
3543
+ const columns = [
3544
+ {
3545
+ title: 'First Name',
3546
+ param: 'userFirstName',
3547
+ type: ParamType$1.String,
3548
+ },
3549
+ {
3550
+ title: 'Last Name',
3551
+ param: 'userLastName',
3552
+ type: ParamType$1.String,
3553
+ },
3554
+ {
3555
+ title: 'Email',
3556
+ param: 'userEmail',
3557
+ type: ParamType$1.String,
3558
+ },
3559
+ {
3560
+ title: 'Canvas Id',
3561
+ param: 'userId',
3562
+ type: ParamType$1.Int,
3563
+ },
3564
+ {
3565
+ title: 'Student',
3566
+ param: 'isLearner',
3567
+ type: ParamType$1.Boolean,
3568
+ },
3569
+ {
3570
+ title: 'Teaching Staff',
3571
+ param: 'isTTM',
3572
+ type: ParamType$1.Boolean,
3573
+ startsHidden: true,
3574
+ },
3575
+ {
3576
+ title: 'Admin',
3577
+ param: 'isAdmin',
3578
+ type: ParamType$1.Boolean,
3579
+ startsHidden: true,
3580
+ },
3581
+ {
3582
+ title: 'Course Canvas Id',
3583
+ param: 'courseId',
3584
+ type: ParamType$1.Int,
3585
+ startsHidden: true,
3586
+ },
3587
+ {
3588
+ title: 'Course Name',
3589
+ param: 'courseName',
3590
+ type: ParamType$1.String,
3591
+ },
3592
+ {
3593
+ title: 'Browser Name',
3594
+ param: 'browser.name',
3595
+ type: ParamType$1.String,
3596
+ startsHidden: true,
3597
+ },
3598
+ {
3599
+ title: 'Browser Version',
3600
+ param: 'browser.version',
3601
+ type: ParamType$1.String,
3602
+ startsHidden: true,
3603
+ },
3604
+ {
3605
+ title: 'OS',
3606
+ param: 'device.os',
3607
+ type: ParamType$1.String,
3608
+ startsHidden: true,
3609
+ },
3610
+ {
3611
+ title: 'Mobile',
3612
+ param: 'device.isMobile',
3613
+ type: ParamType$1.Boolean,
3614
+ startsHidden: true,
3615
+ },
3616
+ {
3617
+ title: 'Year',
3618
+ param: 'year',
3619
+ type: ParamType$1.Int,
3620
+ },
3621
+ {
3622
+ title: 'Month',
3623
+ param: 'month',
3624
+ type: ParamType$1.Int,
3625
+ },
3626
+ {
3627
+ title: 'Day',
3628
+ param: 'day',
3629
+ type: ParamType$1.Int,
3630
+ },
3631
+ {
3632
+ title: 'Hour',
3633
+ param: 'hour',
3634
+ type: ParamType$1.Int,
3635
+ },
3636
+ {
3637
+ title: 'Minute',
3638
+ param: 'minute',
3639
+ type: ParamType$1.Int,
3640
+ startsHidden: true,
3641
+ },
3642
+ {
3643
+ title: 'Timestamp',
3644
+ param: 'timestamp',
3645
+ type: ParamType$1.Int,
3646
+ startsHidden: true,
3647
+ },
3648
+ {
3649
+ title: 'Context',
3650
+ param: 'context',
3651
+ type: ParamType$1.String,
3652
+ },
3653
+ {
3654
+ title: 'Subcontext',
3655
+ param: 'subcontext',
3656
+ type: ParamType$1.String,
3657
+ },
3658
+ {
3659
+ title: 'Tags',
3660
+ param: 'tags',
3661
+ type: ParamType$1.JSON,
3662
+ startsHidden: true,
3663
+ },
3664
+ {
3665
+ title: 'Log Level',
3666
+ param: 'level',
3667
+ type: ParamType$1.String,
3668
+ startsHidden: true,
3669
+ },
3670
+ {
3671
+ title: 'Metadata',
3672
+ param: 'metadata',
3673
+ type: ParamType$1.JSON,
3674
+ startsHidden: true,
3675
+ },
3676
+ {
3677
+ title: 'Source',
3678
+ param: 'source',
3679
+ type: ParamType$1.String,
3680
+ },
3681
+ {
3682
+ title: 'Server Route Path',
3683
+ param: 'routePath',
3684
+ type: ParamType$1.String,
3685
+ startsHidden: true,
3686
+ },
3687
+ {
3688
+ title: 'Server Route Template',
3689
+ param: 'routeTemplate',
3690
+ type: ParamType$1.String,
3691
+ startsHidden: true,
3692
+ },
3693
+ {
3694
+ title: 'Type',
3695
+ param: 'type',
3696
+ type: ParamType$1.String,
3697
+ },
3698
+ {
3699
+ title: 'Error Message',
3700
+ param: 'errorMessage',
3701
+ type: ParamType$1.String,
3702
+ startsHidden: true,
3703
+ },
3704
+ {
3705
+ title: 'Error Code',
3706
+ param: 'errorCode',
3707
+ type: ParamType$1.String,
3708
+ startsHidden: true,
3709
+ },
3710
+ {
3711
+ title: 'Error Stack',
3712
+ param: 'errorStack',
3713
+ type: ParamType$1.String,
3714
+ startsHidden: true,
3715
+ },
3716
+ {
3717
+ title: 'Action Target',
3718
+ param: 'target',
3719
+ type: ParamType$1.String,
3720
+ startsHidden: true,
3721
+ },
3722
+ {
3723
+ title: 'Action Type',
3724
+ param: 'action',
3725
+ type: ParamType$1.String,
3726
+ startsHidden: true,
3727
+ },
3728
+ ];
3729
+ // Create intelliTable
3730
+ const dataTable = (React__default["default"].createElement(IntelliTable, { title: "Matching Logs", id: "logs", data: logs, columns: columns }));
3731
+ // Main body
3732
+ body = (React__default["default"].createElement(React__default["default"].Fragment, null,
3733
+ filters,
3734
+ dataTable));
3735
+ }
3736
+ /* ---------- Wrap in Modal --------- */
3737
+ return (React__default["default"].createElement("div", { className: "LogReviewer-outer-container" },
3738
+ React__default["default"].createElement("style", null, style),
3739
+ React__default["default"].createElement("div", { className: "LogReviewer-inner-container" },
3740
+ React__default["default"].createElement("div", { className: "LogReviewer-header" },
3741
+ React__default["default"].createElement("div", { className: "LogReviewer-header-title" },
3742
+ React__default["default"].createElement("h1", { className: "m-0" }, "Log Review Dashboard")),
3743
+ React__default["default"].createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-lg", "aria-label": "close log reviewer panel", onClick: onClose, style: {
3744
+ border: 0,
3745
+ backgroundColor: 'transparent',
3746
+ padding: 0,
3747
+ margin: 0,
3748
+ } },
3749
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes }))),
3750
+ React__default["default"].createElement("div", { className: "LogReviewer-contents" }, body))));
3751
+ };
3752
+
3753
+ /**
3754
+ * One minute in ms
3755
+ * @author Gabe Abrams
3756
+ */
3757
+ const MINUTE_IN_MS = 60000;
3758
+
3759
+ /**
3760
+ * One hour in ms
3761
+ * @author Gabe Abrams
3762
+ */
3763
+ const HOUR_IN_MS = 3600000;
3764
+
3765
+ /**
3766
+ * One day in ms
3767
+ * @author Gabe Abrams
3768
+ */
3769
+ const DAY_IN_MS = 86400000;
3770
+
3771
+ /**
3772
+ * Shorten text so it fits into a certain number of chars
3773
+ * @author Gabe Abrams
3774
+ * @param text the text to abbreviate
3775
+ * @param maxChars the maximum number of chars to include
3776
+ * @returns abbreviated text with length no greater than maxChars
3777
+ * (including ellipses if applicable)
3778
+ */
3779
+ const abbreviate = (text, maxChars) => {
3780
+ // Check if already short enough
3781
+ if (text.trim().length < maxChars) {
3782
+ return text.trim();
3783
+ }
3784
+ // Abbreviate
3785
+ const shortenedText = (text
3786
+ .trim()
3787
+ .substring(0, maxChars - 3)
3788
+ .trim());
3789
+ return `${shortenedText}...`;
3790
+ };
3791
+
3792
+ /**
3793
+ * Sum the numbers in an array
3794
+ * @author Gabe Abrams
3795
+ * @param nums the numbers to sum
3796
+ * @returns the sum of the numbers
3797
+ */
3798
+ const sum = (nums) => {
3799
+ return nums.reduce((a, b) => {
3800
+ return (a + b);
3801
+ }, 0);
3802
+ };
3803
+
3804
+ /**
3805
+ * Get the average of a set of numbers
3806
+ * @author Gabe Abrams
3807
+ * @param nums the numbers to average
3808
+ * @returns average value or 0 if no numbers
3809
+ */
3810
+ const avg = (nums) => {
3811
+ // Handle empty array case
3812
+ if (nums.length === 0) {
3813
+ return 0;
2120
3814
  }
2121
- // Success! Extract the body
2122
- const { body } = response.body;
2123
- // Return
2124
- return body;
2125
- });
3815
+ // Get the total value
3816
+ const total = sum(nums);
3817
+ // Get average
3818
+ return (total / nums.length);
3819
+ };
2126
3820
 
2127
3821
  /**
2128
- * Path that all routes start with
3822
+ * Round a number (ceiling) to a certain number of decimals
2129
3823
  * @author Gabe Abrams
3824
+ * @param num the number to round
3825
+ * @param numDecimals the number of decimals to round to
3826
+ * @returns rounded number
2130
3827
  */
2131
- const ROUTE_PATH_PREFIX = '/dce-reactkit';
3828
+ const ceilToNumDecimals = (num, numDecimals) => {
3829
+ const rounder = 10 ** numDecimals;
3830
+ return (Math.ceil(num * rounder) / rounder);
3831
+ };
2132
3832
 
2133
3833
  /**
2134
- * Path of the route for storing client-side logs
3834
+ * Round a number (floor) to a certain number of decimals
2135
3835
  * @author Gabe Abrams
3836
+ * @param num the number to round
3837
+ * @param numDecimals the number of decimals to round to
3838
+ * @returns rounded number
2136
3839
  */
2137
- const LOG_ROUTE_PATH = `${ROUTE_PATH_PREFIX}/log`;
3840
+ const floorToNumDecimals = (num, numDecimals) => {
3841
+ const rounder = 10 ** numDecimals;
3842
+ return (Math.floor(num * rounder) / rounder);
3843
+ };
2138
3844
 
2139
3845
  /**
2140
- * Server-side API param types
3846
+ * Pad a number's decimal with zeros on the right
3847
+ * (e.g. 5.2 becomes 5.20 with 2 digit padding)
2141
3848
  * @author Gabe Abrams
3849
+ * @param num the number to pad
3850
+ * @param numDigits the minimum number of digits after the decimal
3851
+ * @returns padded number
2142
3852
  */
2143
- var ParamType;
2144
- (function (ParamType) {
2145
- ParamType["Boolean"] = "boolean";
2146
- ParamType["BooleanOptional"] = "boolean-optional";
2147
- ParamType["Float"] = "float";
2148
- ParamType["FloatOptional"] = "float-optional";
2149
- ParamType["Int"] = "int";
2150
- ParamType["IntOptional"] = "int-optional";
2151
- ParamType["JSON"] = "json";
2152
- ParamType["JSONOptional"] = "json-optional";
2153
- ParamType["String"] = "string";
2154
- ParamType["StringOptional"] = "string-optional";
2155
- })(ParamType || (ParamType = {}));
2156
- var ParamType$1 = ParamType;
3853
+ const padDecimalZeros = (num, numDigits) => {
3854
+ // Skip if nothing to do
3855
+ if (numDigits < 1) {
3856
+ return String(num);
3857
+ }
3858
+ // Convert to string
3859
+ let out = String(num);
3860
+ // Add a decimal point if there isn't one
3861
+ if (!out.includes('.')) {
3862
+ out += '.';
3863
+ }
3864
+ // Add zeros
3865
+ while (out.split('.')[1].length < numDigits) {
3866
+ out = `${out}0`;
3867
+ }
3868
+ // Return
3869
+ return out;
3870
+ };
3871
+
3872
+ /**
3873
+ * Pad a number with zeros on the left (e.g. 5 becomes 05 with 2 digit padding)
3874
+ * @author Gabe Abrams
3875
+ * @param num the number to pad
3876
+ * @param numDigits the minimum number of digits before the decimal
3877
+ * @returns padded number
3878
+ */
3879
+ const padZerosLeft = (num, numDigits) => {
3880
+ // Convert to string
3881
+ let out = String(num);
3882
+ // Add zeros
3883
+ while (out.split('.')[0].length < numDigits) {
3884
+ out = `0${out}`;
3885
+ }
3886
+ // Return
3887
+ return out;
3888
+ };
3889
+
3890
+ /**
3891
+ * Route for checking the status of the current user's
3892
+ * access to log review
3893
+ * @author Gabe Abrams
3894
+ */
3895
+ const LOG_REVIEW_STATUS_ROUTE = `/admin${ROUTE_PATH_PREFIX}/logs/access`;
2157
3896
 
2158
- // Import custom error
2159
3897
  // Stored copy of caccl functions
2160
3898
  let _cacclGetLaunchInfo;
2161
3899
  // Stored copy of dce-mango log collection
@@ -2196,10 +3934,20 @@ const internalGetLogCollection = () => {
2196
3934
  * @param opts.getLaunchInfo CACCL LTI's get launch info function
2197
3935
  * @param [opts.logCollection] mongo collection from dce-mango to use for
2198
3936
  * storing logs. If none is included, logs are written to the console
3937
+ * @param [opts.logReviewAdmins=all admins] info on which admins can review
3938
+ * logs from the client. If not included, all Canvas admins are allowed to
3939
+ * review logs. If null, no Canvas admins are allowed to review logs.
3940
+ * If an array of Canvas userIds (numbers), only Canvas admins with those
3941
+ * userIds are allowed to review logs. If a dce-mango collection, only
3942
+ * Canvas admins with entries in that collection ({ userId, ...}) are allowed
3943
+ * to review logs
2199
3944
  */
2200
3945
  const initServer = (opts) => {
2201
3946
  _cacclGetLaunchInfo = opts.getLaunchInfo;
2202
3947
  _logCollection = opts.logCollection;
3948
+ /*----------------------------------------*/
3949
+ /* Logging */
3950
+ /*----------------------------------------*/
2203
3951
  /**
2204
3952
  * Log an event
2205
3953
  * @author Gabe Abrams
@@ -2266,6 +4014,74 @@ const initServer = (opts) => {
2266
4014
  return log;
2267
4015
  },
2268
4016
  }));
4017
+ /*----------------------------------------*/
4018
+ /* Log Reviewer */
4019
+ /*----------------------------------------*/
4020
+ if (opts.logReviewAdmins !== null) {
4021
+ /**
4022
+ * Check if a given user is allowed to review logs
4023
+ * @author Gabe Abrams
4024
+ * @param userId the id of the user
4025
+ * @returns true if the user can review logs
4026
+ */
4027
+ const canReviewLogs = (userId) => __awaiter(void 0, void 0, void 0, function* () {
4028
+ try {
4029
+ // Array of userIds
4030
+ if (Array.isArray(opts.logReviewAdmins)) {
4031
+ return opts.logReviewAdmins.some((allowedId) => {
4032
+ return (userId === allowedId);
4033
+ });
4034
+ }
4035
+ // Must be a collection
4036
+ const matches = yield opts.logReviewAdmins.find({ userId });
4037
+ // Make sure at least one entry matches
4038
+ return matches.length > 0;
4039
+ }
4040
+ catch (err) {
4041
+ // If an error occurred, simply return false
4042
+ return false;
4043
+ }
4044
+ });
4045
+ /**
4046
+ * Check if the current user has access to logs
4047
+ * @author Gabe Abrams
4048
+ * @returns {boolean} true if user has access
4049
+ */
4050
+ opts.app.get(LOG_REVIEW_STATUS_ROUTE, genRouteHandler({
4051
+ handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
4052
+ const { userId } = params;
4053
+ const canReview = yield canReviewLogs(userId);
4054
+ return canReview;
4055
+ }),
4056
+ }));
4057
+ /**
4058
+ * Get all logs for a certain month
4059
+ * @author Gabe Abrams
4060
+ * @param {number} year the year to query (e.g. 2022)
4061
+ * @param {number} month the month to query (e.g. 1 = January)
4062
+ * @returns {Log[]} list of logs from the given month
4063
+ */
4064
+ opts.app.post(`${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/:year/months/:month`, genRouteHandler({
4065
+ paramTypes: {
4066
+ year: ParamType$1.Int,
4067
+ month: ParamType$1.Int,
4068
+ },
4069
+ handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
4070
+ // Get user info
4071
+ const { userId } = params;
4072
+ // Validate user
4073
+ // isAdmin is already checked because path starts with '/admin'
4074
+ const canReview = yield canReviewLogs(userId);
4075
+ if (!canReview) {
4076
+ throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
4077
+ }
4078
+ // Query for logs
4079
+ const logs = yield _logCollection.find({ userId });
4080
+ // Return logs
4081
+ return logs;
4082
+ }),
4083
+ }));
4084
+ }
2269
4085
  };
2270
4086
 
2271
4087
  // Import shared types
@@ -2565,90 +4381,6 @@ const parseUserAgent = (userAgent) => {
2565
4381
  };
2566
4382
  };
2567
4383
 
2568
- /**
2569
- * Type of a log event
2570
- * @author Gabe Abrams
2571
- */
2572
- var LogType;
2573
- (function (LogType) {
2574
- // User action
2575
- LogType["Action"] = "action";
2576
- // Error
2577
- LogType["Error"] = "error";
2578
- })(LogType || (LogType = {}));
2579
- var LogType$1 = LogType;
2580
-
2581
- /**
2582
- * Source of a log event
2583
- * @author Gabe Abrams
2584
- */
2585
- var LogSource;
2586
- (function (LogSource) {
2587
- // Client
2588
- LogSource["Client"] = "client";
2589
- // Server
2590
- LogSource["Server"] = "server";
2591
- })(LogSource || (LogSource = {}));
2592
- var LogSource$1 = LogSource;
2593
-
2594
- /**
2595
- * Built-in metadata for logs
2596
- * @author Gabe Abrams
2597
- */
2598
- const LogBuiltInMetadata = {
2599
- // Contexts
2600
- Context: {
2601
- Uncategorized: 'n/a',
2602
- ServerRenderedErrorPage: '_server-rendered-error-page',
2603
- ServerEndpointError: '_server-endpoint-error',
2604
- },
2605
- // Targets
2606
- Target: {
2607
- NoSpecificTarget: 'n/a',
2608
- },
2609
- };
2610
-
2611
- /**
2612
- * Types of actions
2613
- * @author Gabe Abrams
2614
- */
2615
- var LogAction;
2616
- (function (LogAction) {
2617
- // Target was opened by the user (it was not on screen, but now it is)
2618
- LogAction["Open"] = "open";
2619
- // Target was closed by the user (it was on screen, but now it is not)
2620
- LogAction["Close"] = "close";
2621
- // Target was cancelled by the user (it was on closed without saving)
2622
- LogAction["Cancel"] = "cancel";
2623
- // Target was expanded by the user (it always remains on screen, but size was changed)
2624
- LogAction["Expand"] = "expand";
2625
- // Target was collapsed by the user (it always remains on screen, but size was changed)
2626
- LogAction["Collapse"] = "collapse";
2627
- // Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
2628
- LogAction["View"] = "view";
2629
- // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2630
- LogAction["Interrupt"] = "interrupt";
2631
- // Target was created by the user (it did not exist before)
2632
- LogAction["Create"] = "create";
2633
- // Target was edited by the user (it existed and was changed)
2634
- LogAction["Edit"] = "edit";
2635
- // Target was deleted by the user (it existed and now it doesn't)
2636
- LogAction["Delete"] = "delete";
2637
- // Target was added by the user (it already existed and was added to another place)
2638
- LogAction["Add"] = "add";
2639
- // Target was removed by the user (it was removed from something but still exists)
2640
- LogAction["Remove"] = "remove";
2641
- // Target was activated by the user (click, check, tap, keypress, etc.)
2642
- LogAction["Activate"] = "activate";
2643
- // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2644
- LogAction["Deactivate"] = "deactivate";
2645
- // User showed interest in a target (hover, peek, etc.)
2646
- LogAction["Peek"] = "peek";
2647
- // Unknown action
2648
- LogAction["Unknown"] = "unknown";
2649
- })(LogAction || (LogAction = {}));
2650
- var LogAction$1 = LogAction;
2651
-
2652
4384
  /**
2653
4385
  * Allowed log levels
2654
4386
  * @author Gabe Abrams
@@ -3246,21 +4978,7 @@ const startMinWait = (minWaitMs) => {
3246
4978
  });
3247
4979
  };
3248
4980
 
3249
- // Map of month to three letter description
3250
- const monthMap = {
3251
- 1: 'Jan',
3252
- 2: 'Feb',
3253
- 3: 'Mar',
3254
- 4: 'Apr',
3255
- 5: 'May',
3256
- 6: 'Jun',
3257
- 7: 'Jul',
3258
- 8: 'Aug',
3259
- 9: 'Sep',
3260
- 10: 'Oct',
3261
- 11: 'Nov',
3262
- 12: 'Dec',
3263
- };
4981
+ // Import shared helpers
3264
4982
  /**
3265
4983
  * Get a human-readable description of a date (all in ET)
3266
4984
  * @author Gabe Abrams
@@ -3271,8 +4989,10 @@ const getHumanReadableDate = (dateOrTimestamp) => {
3271
4989
  // Get the date info
3272
4990
  const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
3273
4991
  const currYear = getTimeInfoInET().year;
4992
+ // Get the short month description
4993
+ const monthName = getMonthName(month).short;
3274
4994
  // Create start of description
3275
- let description = `${monthMap[month]} ${day}${getOrdinal(day)}`;
4995
+ let description = `${monthName} ${day}${getOrdinal(day)}`;
3276
4996
  // Add on year if it's different
3277
4997
  if (year !== currYear) {
3278
4998
  description += ` ${year}`;
@@ -3394,41 +5114,6 @@ const parallelLimit = (taskFunctions, limit) => __awaiter(void 0, void 0, void 0
3394
5114
  return results;
3395
5115
  });
3396
5116
 
3397
- /**
3398
- * Log a user action on the client (cannot be used on the server)
3399
- * @author Gabe Abrams
3400
- */
3401
- const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* () {
3402
- var _a, _b, _c, _d, _e, _f;
3403
- return visitServerEndpoint({
3404
- path: LOG_ROUTE_PATH,
3405
- method: 'POST',
3406
- params: {
3407
- context: (typeof opts.context === 'string'
3408
- ? opts.context
3409
- : ((_b = ((_a = opts.context) !== null && _a !== void 0 ? _a : {})._) !== null && _b !== void 0 ? _b : LogBuiltInMetadata.Context.Uncategorized)),
3410
- subcontext: ((_c = opts.subcontext) !== null && _c !== void 0 ? _c : LogBuiltInMetadata.Context.Uncategorized),
3411
- tags: JSON.stringify((_d = opts.tags) !== null && _d !== void 0 ? _d : []),
3412
- metadata: JSON.stringify((_e = opts.metadata) !== null && _e !== void 0 ? _e : {}),
3413
- errorMessage: (opts.error
3414
- ? opts.error.message
3415
- : undefined),
3416
- errorCode: (opts.error
3417
- ? opts.error.code
3418
- : undefined),
3419
- errorStack: (opts.error
3420
- ? opts.error.stack
3421
- : undefined),
3422
- target: (opts.action
3423
- ? ((_f = opts.target) !== null && _f !== void 0 ? _f : LogBuiltInMetadata.Target.NoSpecificTarget)
3424
- : undefined),
3425
- action: (opts.action
3426
- ? opts.action
3427
- : undefined),
3428
- },
3429
- });
3430
- });
3431
-
3432
5117
  /**
3433
5118
  * Initialize a log collection given the dce-mango Collection class
3434
5119
  * @author Gabe Abrams
@@ -3465,6 +5150,7 @@ var DayOfWeek$1 = DayOfWeek;
3465
5150
 
3466
5151
  exports.AppWrapper = AppWrapper;
3467
5152
  exports.ButtonInputGroup = ButtonInputGroup;
5153
+ exports.CSVDownloadButton = CSVDownloadButton;
3468
5154
  exports.CheckboxButton = CheckboxButton;
3469
5155
  exports.CopiableBox = CopiableBox;
3470
5156
  exports.DAY_IN_MS = DAY_IN_MS;
@@ -3473,10 +5159,12 @@ exports.Drawer = Drawer;
3473
5159
  exports.ErrorBox = ErrorBox;
3474
5160
  exports.ErrorWithCode = ErrorWithCode;
3475
5161
  exports.HOUR_IN_MS = HOUR_IN_MS;
5162
+ exports.IntelliTable = IntelliTable;
3476
5163
  exports.ItemPicker = ItemPicker;
3477
5164
  exports.LoadingSpinner = LoadingSpinner;
3478
5165
  exports.LogAction = LogAction$1;
3479
5166
  exports.LogBuiltInMetadata = LogBuiltInMetadata;
5167
+ exports.LogReviewer = LogReviewer;
3480
5168
  exports.LogSource = LogSource$1;
3481
5169
  exports.LogType = LogType$1;
3482
5170
  exports.MINUTE_IN_MS = MINUTE_IN_MS;
@@ -3500,8 +5188,10 @@ exports.ceilToNumDecimals = ceilToNumDecimals;
3500
5188
  exports.confirm = confirm;
3501
5189
  exports.floorToNumDecimals = floorToNumDecimals;
3502
5190
  exports.forceNumIntoBounds = forceNumIntoBounds;
5191
+ exports.genCSV = genCSV;
3503
5192
  exports.genRouteHandler = genRouteHandler;
3504
5193
  exports.getHumanReadableDate = getHumanReadableDate;
5194
+ exports.getMonthName = getMonthName;
3505
5195
  exports.getOrdinal = getOrdinal;
3506
5196
  exports.getPartOfDay = getPartOfDay;
3507
5197
  exports.getTimeInfoInET = getTimeInfoInET;