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