dce-reactkit 3.2.1 → 3.2.2-beta.10

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