dce-reactkit 3.2.2-beta.1 → 3.2.2-beta.11

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 (59) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/dist/cjs/index.js +2019 -274
  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/LogMetadataType.d.ts +19 -0
  19. package/dist/cjs/types/types/ReactKitErrorCode.d.ts +2 -1
  20. package/dist/esm/index.js +2015 -276
  21. package/dist/esm/index.js.map +1 -1
  22. package/dist/esm/types/components/ButtonInputGroup.d.ts +1 -0
  23. package/dist/esm/types/components/CSVDownloadButton.d.ts +19 -0
  24. package/dist/esm/types/components/IntelliTable.d.ts +17 -0
  25. package/dist/esm/types/components/ItemPicker/index.d.ts +1 -0
  26. package/dist/esm/types/components/LogReviewer.d.ts +13 -0
  27. package/dist/esm/types/components/SimpleDateChooser.d.ts +1 -0
  28. package/dist/esm/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
  29. package/dist/esm/types/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
  30. package/dist/esm/types/helpers/canReviewLogs.d.ts +7 -0
  31. package/dist/esm/types/helpers/genCSV.d.ts +14 -0
  32. package/dist/esm/types/helpers/getMonthName.d.ts +14 -0
  33. package/dist/esm/types/index.d.ts +9 -1
  34. package/dist/esm/types/server/initServer.d.ts +8 -0
  35. package/dist/esm/types/types/IntelliTableColumn.d.ts +12 -0
  36. package/dist/esm/types/types/LogMetadataType.d.ts +19 -0
  37. package/dist/esm/types/types/ReactKitErrorCode.d.ts +2 -1
  38. package/dist/index.d.ts +171 -47
  39. package/package.json +1 -1
  40. package/sandbox.js +78 -17
  41. package/src/components/AppWrapper.tsx +5 -1
  42. package/src/components/ButtonInputGroup.tsx +4 -1
  43. package/src/components/CSVDownloadButton.tsx +102 -0
  44. package/src/components/IntelliTable.tsx +610 -0
  45. package/src/components/ItemPicker/index.tsx +4 -0
  46. package/src/components/LogReviewer.tsx +2097 -0
  47. package/src/components/SimpleDateChooser.tsx +26 -27
  48. package/src/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.ts +9 -0
  49. package/src/constants/LOG_REVIEW_STATUS_ROUTE.ts +10 -0
  50. package/src/helpers/canReviewLogs.ts +33 -0
  51. package/src/helpers/genCSV.ts +59 -0
  52. package/src/helpers/getHumanReadableDate.ts +6 -17
  53. package/src/helpers/getMonthName.ts +29 -0
  54. package/src/helpers/logClientEvent.tsx +5 -0
  55. package/src/index.ts +16 -0
  56. package/src/server/initServer.ts +125 -3
  57. package/src/types/IntelliTableColumn.ts +24 -0
  58. package/src/types/LogMetadataType.ts +23 -0
  59. package/src/types/ReactKitErrorCode.tsx +2 -1
package/dist/cjs/index.js CHANGED
@@ -36,7 +36,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
36
36
  });
37
37
  }
38
38
 
39
- // Highest error code = DRK10
39
+ // Highest error code = DRK11
40
40
  /**
41
41
  * List of error codes built into the react kit
42
42
  * @author Gabe Abrams
@@ -53,6 +53,7 @@ var ReactKitErrorCode;
53
53
  ReactKitErrorCode["NoCACCLGetLaunchInfoFunction"] = "DRK8";
54
54
  ReactKitErrorCode["NotTTM"] = "DRK9";
55
55
  ReactKitErrorCode["NotAdmin"] = "DRK10";
56
+ ReactKitErrorCode["NotAllowedToReviewLogs"] = "DRK11";
56
57
  })(ReactKitErrorCode || (ReactKitErrorCode = {}));
57
58
  var ReactKitErrorCode$1 = ReactKitErrorCode;
58
59
 
@@ -279,7 +280,7 @@ const ModalButtonTypeToLabelAndVariant = {
279
280
  /*------------------------------------------------------------------------*/
280
281
  /* Style */
281
282
  /*------------------------------------------------------------------------*/
282
- const style$6 = `
283
+ const style$7 = `
283
284
  .Modal-backdrop {
284
285
  position: fixed;
285
286
  top: 0;
@@ -453,7 +454,7 @@ const Modal = (props) => {
453
454
  left: 0,
454
455
  right: 0,
455
456
  } },
456
- React__default["default"].createElement("style", null, style$6),
457
+ React__default["default"].createElement("style", null, style$7),
457
458
  React__default["default"].createElement("div", { className: `Modal-backdrop ${backdropAnimationClass}`, style: {
458
459
  zIndex: 5000000003,
459
460
  }, onClick: () => __awaiter(void 0, void 0, void 0, function* () {
@@ -529,6 +530,18 @@ const LogBuiltInMetadata = {
529
530
  },
530
531
  };
531
532
 
533
+ /**
534
+ * Allowed log levels
535
+ * @author Gabe Abrams
536
+ */
537
+ var LogLevel;
538
+ (function (LogLevel) {
539
+ LogLevel["Warn"] = "Warn";
540
+ LogLevel["Info"] = "Info";
541
+ LogLevel["Debug"] = "Debug";
542
+ })(LogLevel || (LogLevel = {}));
543
+ var LogLevel$1 = LogLevel;
544
+
532
545
  // Keep track of whether or not session expiry has already been handled
533
546
  let sessionAlreadyExpired = false;
534
547
  /*------------------------------------------------------------------------*/
@@ -647,7 +660,7 @@ const visitServerEndpoint = (opts) => __awaiter(void 0, void 0, void 0, function
647
660
  * @author Gabe Abrams
648
661
  */
649
662
  const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* () {
650
- var _a, _b, _c, _d, _e, _f;
663
+ var _a, _b, _c, _d, _e, _f, _g;
651
664
  return visitServerEndpoint({
652
665
  path: LOG_ROUTE_PATH,
653
666
  method: 'POST',
@@ -656,8 +669,9 @@ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* ()
656
669
  ? opts.context
657
670
  : ((_b = ((_a = opts.context) !== null && _a !== void 0 ? _a : {})._) !== null && _b !== void 0 ? _b : LogBuiltInMetadata.Context.Uncategorized)),
658
671
  subcontext: ((_c = opts.subcontext) !== null && _c !== void 0 ? _c : LogBuiltInMetadata.Context.Uncategorized),
659
- tags: JSON.stringify((_d = opts.tags) !== null && _d !== void 0 ? _d : []),
660
- metadata: JSON.stringify((_e = opts.metadata) !== null && _e !== void 0 ? _e : {}),
672
+ level: ((_d = opts.level) !== null && _d !== void 0 ? _d : LogLevel$1.Info),
673
+ tags: JSON.stringify((_e = opts.tags) !== null && _e !== void 0 ? _e : []),
674
+ metadata: JSON.stringify((_f = opts.metadata) !== null && _f !== void 0 ? _f : {}),
661
675
  errorMessage: (opts.error
662
676
  ? opts.error.message
663
677
  : undefined),
@@ -668,7 +682,7 @@ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* ()
668
682
  ? opts.error.stack
669
683
  : undefined),
670
684
  target: (opts.action
671
- ? ((_f = opts.target) !== null && _f !== void 0 ? _f : LogBuiltInMetadata.Target.NoSpecificTarget)
685
+ ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoSpecificTarget)
672
686
  : undefined),
673
687
  action: (opts.action
674
688
  ? opts.action
@@ -799,7 +813,11 @@ const showFatalError = (error, errorTitle = 'An Error Occurred') => {
799
813
  // Add log
800
814
  logClientEvent({
801
815
  context: LogBuiltInMetadata.Context.ClientFatalError,
802
- error,
816
+ error: {
817
+ message,
818
+ code,
819
+ stack: (error !== null && error !== void 0 ? error : {}).stack,
820
+ },
803
821
  metadata: {
804
822
  errorTitle,
805
823
  },
@@ -913,7 +931,7 @@ const AppWrapper = (props) => {
913
931
  /*------------------------------------------------------------------------*/
914
932
  /* Style */
915
933
  /*------------------------------------------------------------------------*/
916
- const style$5 = `
934
+ const style$6 = `
917
935
  /* Container fades in */
918
936
  .LoadingSpinner-container {
919
937
  animation-name: LoadingSpinner-container-fade-in;
@@ -991,7 +1009,7 @@ const LoadingSpinner = () => {
991
1009
  /*------------------------------------------------------------------------*/
992
1010
  // Add all four blips to a container
993
1011
  return (React__default["default"].createElement("div", { className: "text-center LoadingSpinner LoadingSpinner-container" },
994
- React__default["default"].createElement("style", null, style$5),
1012
+ React__default["default"].createElement("style", null, style$6),
995
1013
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-1 me-1" }),
996
1014
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-2 me-1" }),
997
1015
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-3 me-1" }),
@@ -1005,7 +1023,7 @@ const LoadingSpinner = () => {
1005
1023
  /*------------------------------------------------------------------------*/
1006
1024
  /* Style */
1007
1025
  /*------------------------------------------------------------------------*/
1008
- const style$4 = `
1026
+ const style$5 = `
1009
1027
  /* Tab Box */
1010
1028
  .TabBox-box {
1011
1029
  /* Light Border */
@@ -1085,7 +1103,7 @@ const TabBox = (props) => {
1085
1103
  /*----------------------------------------*/
1086
1104
  // Full UI
1087
1105
  return (React__default["default"].createElement("div", { className: `TabBox-container ${noBottomMargin ? '' : 'mb-2'}` },
1088
- React__default["default"].createElement("style", null, style$4),
1106
+ React__default["default"].createElement("style", null, style$5),
1089
1107
  React__default["default"].createElement("div", { className: "TabBox-title-container" },
1090
1108
  React__default["default"].createElement("div", { className: "TabBox-title" }, title)),
1091
1109
  React__default["default"].createElement("div", { className: `TabBox-box ps-2 pt-2 pe-2 ${noBottomPadding ? '' : 'pb-2'}` },
@@ -1170,14 +1188,14 @@ const ButtonInputGroup = (props) => {
1170
1188
  /*------------------------------------------------------------------------*/
1171
1189
  /* -------------- Props ------------- */
1172
1190
  // Destructure all props
1173
- const { label, minLabelWidth, children, } = props;
1191
+ const { label, minLabelWidth, children, className, } = props;
1174
1192
  /*------------------------------------------------------------------------*/
1175
1193
  /* Render */
1176
1194
  /*------------------------------------------------------------------------*/
1177
1195
  /*----------------------------------------*/
1178
1196
  /* Main UI */
1179
1197
  /*----------------------------------------*/
1180
- return (React__default["default"].createElement("div", { className: "input-group" },
1198
+ return (React__default["default"].createElement("div", { className: `input-group ${className !== null && className !== void 0 ? className : ''}` },
1181
1199
  React__default["default"].createElement("div", { className: "input-group-prepend d-flex w-100" },
1182
1200
  React__default["default"].createElement("span", { className: "input-group-text", style: {
1183
1201
  minWidth: (minLabelWidth !== null && minLabelWidth !== void 0 ? minLabelWidth : undefined),
@@ -1192,6 +1210,34 @@ const ButtonInputGroup = (props) => {
1192
1210
  } }, children))));
1193
1211
  };
1194
1212
 
1213
+ const monthNames = [
1214
+ { short: 'Jan', full: 'January' },
1215
+ { short: 'Feb', full: 'February' },
1216
+ { short: 'Mar', full: 'March' },
1217
+ { short: 'Apr', full: 'April' },
1218
+ { short: 'May', full: 'May' },
1219
+ { short: 'Jun', full: 'June' },
1220
+ { short: 'Jul', full: 'July' },
1221
+ { short: 'Aug', full: 'August' },
1222
+ { short: 'Sep', full: 'September' },
1223
+ { short: 'Oct', full: 'October' },
1224
+ { short: 'Nov', full: 'November' },
1225
+ { short: 'Dec', full: 'December' },
1226
+ ];
1227
+ /**
1228
+ * Get the name of a month given the month number (1 = January, etc.)
1229
+ * If an invalid number is provided, we will treat it like January
1230
+ * @author Gabe Abrams
1231
+ * @param month the number of the month
1232
+ * @returns object containing multiple month name formats:
1233
+ * { short, full } where short will look like "Jan" and full will look like
1234
+ * "January"
1235
+ */
1236
+ const getMonthName = (month) => {
1237
+ var _a;
1238
+ return ((_a = monthNames[month - 1]) !== null && _a !== void 0 ? _a : monthNames[0]);
1239
+ };
1240
+
1195
1241
  const ORDINALS = ['th', 'st', 'nd', 'rd'];
1196
1242
  /**
1197
1243
  * Get a number's ordinal
@@ -1261,29 +1307,23 @@ const getTimeInfoInET = (dateOrTimestamp) => {
1261
1307
  };
1262
1308
  };
1263
1309
 
1310
+ /**
1311
+ * Force a number to stay within specific bounds
1312
+ * @author Gabe Abrams
1313
+ * @param num the number to move into the bounds
1314
+ * @param min the minimum number in the bound
1315
+ * @param max the maximum number in the bound
1316
+ * @returns bounded number
1317
+ */
1318
+ const forceNumIntoBounds = (num, min, max) => {
1319
+ return Math.max(min, Math.min(max, num));
1320
+ };
1321
+
1264
1322
  /**
1265
1323
  * A very simple, lightweight date chooser
1266
1324
  * @author Gabe Abrams
1267
1325
  */
1268
1326
  /*------------------------------------------------------------------------*/
1269
- /* Constants */
1270
- /*------------------------------------------------------------------------*/
1271
- // Constants
1272
- const MONTH_NAMES = [
1273
- 'January',
1274
- 'February',
1275
- 'March',
1276
- 'April',
1277
- 'May',
1278
- 'June',
1279
- 'July',
1280
- 'August',
1281
- 'September',
1282
- 'October',
1283
- 'November',
1284
- 'December',
1285
- ];
1286
- /*------------------------------------------------------------------------*/
1287
1327
  /* Component */
1288
1328
  /*------------------------------------------------------------------------*/
1289
1329
  const SimpleDateChooser = (props) => {
@@ -1292,8 +1332,8 @@ const SimpleDateChooser = (props) => {
1292
1332
  /*------------------------------------------------------------------------*/
1293
1333
  var _a;
1294
1334
  /* -------------- Props ------------- */
1295
- const { ariaLabel, name, month, day, year, onChange, } = props;
1296
- const numMonthsToShow = Math.min((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6, 12);
1335
+ const { ariaLabel, name, month, day, year, onChange, chooseFromPast, } = props;
1336
+ let numMonthsToShow = forceNumIntoBounds(((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6), 1, 12);
1297
1337
  /*------------------------------------------------------------------------*/
1298
1338
  /* Render */
1299
1339
  /*------------------------------------------------------------------------*/
@@ -1303,16 +1343,25 @@ const SimpleDateChooser = (props) => {
1303
1343
  // Determine the set of choices allowed
1304
1344
  const today = getTimeInfoInET();
1305
1345
  const choices = [];
1346
+ let startYear = today.year;
1347
+ let startMonth = today.month;
1348
+ if (chooseFromPast) {
1349
+ startMonth -= Math.max(0, numMonthsToShow - 1);
1350
+ if (startMonth <= 0) {
1351
+ startMonth += 12;
1352
+ startYear -= 1;
1353
+ }
1354
+ }
1306
1355
  for (let i = 0; i < numMonthsToShow; i++) {
1307
1356
  // Get month and year info
1308
- const unmoddedMonth = (today.month + i);
1357
+ const unmoddedMonth = (startMonth + i);
1309
1358
  const month = (unmoddedMonth > 12
1310
1359
  ? unmoddedMonth - 12
1311
1360
  : unmoddedMonth);
1312
- const monthName = MONTH_NAMES[month - 1];
1361
+ const monthName = getMonthName(month).full;
1313
1362
  const year = (unmoddedMonth > 12
1314
- ? today.year + 1
1315
- : today.year);
1363
+ ? startYear + 1
1364
+ : startYear);
1316
1365
  // Figure out which days are allowed
1317
1366
  const days = [];
1318
1367
  const numDaysInMonth = (new Date(year, month, 0)).getDate();
@@ -1368,7 +1417,7 @@ const SimpleDateChooser = (props) => {
1368
1417
  /*------------------------------------------------------------------------*/
1369
1418
  /* Style */
1370
1419
  /*------------------------------------------------------------------------*/
1371
- const style$3 = `
1420
+ const style$4 = `
1372
1421
  .Drawer-container {
1373
1422
  margin-left: 1rem;
1374
1423
  margin-right: 1rem;
@@ -1400,7 +1449,7 @@ const Drawer = (props) => {
1400
1449
  /* Main UI */
1401
1450
  /*----------------------------------------*/
1402
1451
  return (React__default["default"].createElement("div", { className: "Drawer-container" },
1403
- React__default["default"].createElement("style", null, style$3),
1452
+ React__default["default"].createElement("style", null, style$4),
1404
1453
  children));
1405
1454
  };
1406
1455
 
@@ -1411,7 +1460,7 @@ const Drawer = (props) => {
1411
1460
  /*------------------------------------------------------------------------*/
1412
1461
  /* Style */
1413
1462
  /*------------------------------------------------------------------------*/
1414
- const style$2 = `
1463
+ const style$3 = `
1415
1464
  .PopSuccessMark-outer-container {
1416
1465
  position: relative;
1417
1466
  display: inline-block;
@@ -1516,7 +1565,7 @@ const PopSuccessMark = (props) => {
1516
1565
  width: `${sizeRem}rem`,
1517
1566
  height: `${sizeRem}rem`,
1518
1567
  }, "aria-label": "checkmark indicating success" },
1519
- React__default["default"].createElement("style", null, style$2),
1568
+ React__default["default"].createElement("style", null, style$3),
1520
1569
  React__default["default"].createElement("div", { className: `PopSuccessMark-check-stroke-1 bg-${checkVariant}`, style: {
1521
1570
  borderRadius: `${sizeRem / 5}rem`,
1522
1571
  } }),
@@ -1532,7 +1581,7 @@ const PopSuccessMark = (props) => {
1532
1581
  /*------------------------------------------------------------------------*/
1533
1582
  /* Style */
1534
1583
  /*------------------------------------------------------------------------*/
1535
- const style$1 = `
1584
+ const style$2 = `
1536
1585
  .PopFailureMark-outer-container {
1537
1586
  position: relative;
1538
1587
  display: inline-block;
@@ -1636,7 +1685,7 @@ const PopFailureMark = (props) => {
1636
1685
  width: `${sizeRem}rem`,
1637
1686
  height: `${sizeRem}rem`,
1638
1687
  }, "aria-label": "mark indicating failure" },
1639
- React__default["default"].createElement("style", null, style$1),
1688
+ React__default["default"].createElement("style", null, style$2),
1640
1689
  React__default["default"].createElement("div", { className: `PopFailureMark-x-stroke-1 bg-${xVariant}`, style: {
1641
1690
  borderRadius: `${sizeRem / 5}rem`,
1642
1691
  } }),
@@ -1652,7 +1701,7 @@ const PopFailureMark = (props) => {
1652
1701
  /*------------------------------------------------------------------------*/
1653
1702
  /* Style */
1654
1703
  /*------------------------------------------------------------------------*/
1655
- const style = `
1704
+ const style$1 = `
1656
1705
  .PopPendingMark-outer-container {
1657
1706
  position: relative;
1658
1707
  display: inline-block;
@@ -1725,7 +1774,7 @@ const PopPendingMark = (props) => {
1725
1774
  width: `${sizeRem}rem`,
1726
1775
  height: `${sizeRem}rem`,
1727
1776
  }, "aria-label": "mark indicating that the item is pending" },
1728
- React__default["default"].createElement("style", null, style),
1777
+ React__default["default"].createElement("style", null, style$1),
1729
1778
  React__default["default"].createElement("div", null,
1730
1779
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faHourglass, className: `PopPendingMark-hourglass text-${hourglassVariant}`, style: {
1731
1780
  fontSize: `${sizeRem * 0.6}rem`,
@@ -1738,27 +1787,27 @@ const PopPendingMark = (props) => {
1738
1787
  */
1739
1788
  /* ------------- Actions ------------ */
1740
1789
  // Types of actions
1741
- var ActionType$1;
1790
+ var ActionType$3;
1742
1791
  (function (ActionType) {
1743
1792
  // Indicate that the text was recently copied
1744
1793
  ActionType["IndicateRecentlyCopied"] = "indicate-recently-copied";
1745
1794
  // Clear the status
1746
1795
  ActionType["ClearRecentlyCopiedStatus"] = "clear-recently-copied-status";
1747
- })(ActionType$1 || (ActionType$1 = {}));
1796
+ })(ActionType$3 || (ActionType$3 = {}));
1748
1797
  /**
1749
1798
  * Reducer that executes actions
1750
1799
  * @author Gabe Abrams
1751
1800
  * @param state current state
1752
1801
  * @param action action to execute
1753
1802
  */
1754
- const reducer$1 = (state, action) => {
1803
+ const reducer$3 = (state, action) => {
1755
1804
  switch (action.type) {
1756
- case ActionType$1.IndicateRecentlyCopied: {
1805
+ case ActionType$3.IndicateRecentlyCopied: {
1757
1806
  return {
1758
1807
  recentlyCopied: true,
1759
1808
  };
1760
1809
  }
1761
- case ActionType$1.ClearRecentlyCopiedStatus: {
1810
+ case ActionType$3.ClearRecentlyCopiedStatus: {
1762
1811
  return {
1763
1812
  recentlyCopied: false,
1764
1813
  };
@@ -1784,7 +1833,7 @@ const CopiableBox = (props) => {
1784
1833
  recentlyCopied: false,
1785
1834
  };
1786
1835
  // Initialize state
1787
- const [state, dispatch] = React.useReducer(reducer$1, initialState);
1836
+ const [state, dispatch] = React.useReducer(reducer$3, initialState);
1788
1837
  // Destructure common state
1789
1838
  const { recentlyCopied, } = state;
1790
1839
  /*------------------------------------------------------------------------*/
@@ -1804,13 +1853,13 @@ const CopiableBox = (props) => {
1804
1853
  }
1805
1854
  // Show copied notice
1806
1855
  dispatch({
1807
- type: ActionType$1.IndicateRecentlyCopied,
1856
+ type: ActionType$3.IndicateRecentlyCopied,
1808
1857
  });
1809
1858
  // Wait a moment
1810
1859
  yield waitMs(4000);
1811
1860
  // Hide copied notice
1812
1861
  dispatch({
1813
- type: ActionType$1.ClearRecentlyCopiedStatus,
1862
+ type: ActionType$3.ClearRecentlyCopiedStatus,
1814
1863
  });
1815
1864
  });
1816
1865
  /*------------------------------------------------------------------------*/
@@ -1865,20 +1914,20 @@ const CopiableBox = (props) => {
1865
1914
  */
1866
1915
  /* ------------- Actions ------------ */
1867
1916
  // Types of actions
1868
- var ActionType;
1917
+ var ActionType$2;
1869
1918
  (function (ActionType) {
1870
1919
  // Toggle whether the children are being shown
1871
1920
  ActionType["ToggleItems"] = "toggle-items";
1872
- })(ActionType || (ActionType = {}));
1921
+ })(ActionType$2 || (ActionType$2 = {}));
1873
1922
  /**
1874
1923
  * Reducer that executes actions
1875
1924
  * @author Yuen Ler Chow
1876
1925
  * @param state current state
1877
1926
  * @param action action to execute
1878
1927
  */
1879
- const reducer = (state, action) => {
1928
+ const reducer$2 = (state, action) => {
1880
1929
  switch (action.type) {
1881
- case ActionType.ToggleItems: {
1930
+ case ActionType$2.ToggleItems: {
1882
1931
  return { isShowingItems: !state.isShowingItems };
1883
1932
  }
1884
1933
  default: {
@@ -1902,7 +1951,7 @@ const NestableItemList = (props) => {
1902
1951
  isShowingItems: false,
1903
1952
  };
1904
1953
  // Initialize state
1905
- const [state, dispatch] = React.useReducer(reducer, initialState);
1954
+ const [state, dispatch] = React.useReducer(reducer$2, initialState);
1906
1955
  // Destructure common state
1907
1956
  const { isShowingItems, } = state;
1908
1957
  /*------------------------------------------------------------------------*/
@@ -1992,7 +2041,7 @@ const NestableItemList = (props) => {
1992
2041
  backgroundColor: 'transparent',
1993
2042
  }, type: "button", onClick: () => {
1994
2043
  dispatch({
1995
- type: ActionType.ToggleItems,
2044
+ type: ActionType$2.ToggleItems,
1996
2045
  });
1997
2046
  }, "aria-label": `${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}` },
1998
2047
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: isShowingItems ? freeSolidSvgIcons.faChevronDown : freeSolidSvgIcons.faChevronRight })))),
@@ -2021,7 +2070,7 @@ const ItemPicker = (props) => {
2021
2070
  /*------------------------------------------------------------------------*/
2022
2071
  /* -------------- Props ------------- */
2023
2072
  // Destructure all props
2024
- const { title, items, onChanged, } = props;
2073
+ const { title, items, onChanged, noBottomMargin, } = props;
2025
2074
  /*------------------------------------------------------------------------*/
2026
2075
  /* Component Functions */
2027
2076
  /*------------------------------------------------------------------------*/
@@ -2031,192 +2080,1860 @@ const ItemPicker = (props) => {
2031
2080
  /*----------------------------------------*/
2032
2081
  /* Main UI */
2033
2082
  /*----------------------------------------*/
2034
- return (React__default["default"].createElement(TabBox, { title: title },
2083
+ return (React__default["default"].createElement(TabBox, { title: title, noBottomMargin: noBottomMargin },
2035
2084
  React__default["default"].createElement("div", { style: { overflowX: 'auto' } },
2036
2085
  React__default["default"].createElement(NestableItemList, { items: items, onChanged: onChanged }))));
2037
2086
  };
2038
2087
 
2039
2088
  /**
2040
- * One minute in ms
2041
- * @author Gabe Abrams
2042
- */
2043
- const MINUTE_IN_MS = 60000;
2044
-
2045
- /**
2046
- * One hour in ms
2089
+ * Path of the route for storing client-side logs
2047
2090
  * @author Gabe Abrams
2048
2091
  */
2049
- const HOUR_IN_MS = 3600000;
2092
+ const LOG_REVIEW_ROUTE_PATH_PREFIX = `/admin${ROUTE_PATH_PREFIX}/logs`;
2050
2093
 
2051
2094
  /**
2052
- * One day in ms
2095
+ * Source of a log event
2053
2096
  * @author Gabe Abrams
2054
2097
  */
2055
- const DAY_IN_MS = 86400000;
2098
+ var LogSource;
2099
+ (function (LogSource) {
2100
+ // Client
2101
+ LogSource["Client"] = "client";
2102
+ // Server
2103
+ LogSource["Server"] = "server";
2104
+ })(LogSource || (LogSource = {}));
2105
+ var LogSource$1 = LogSource;
2056
2106
 
2057
2107
  /**
2058
- * Shorten text so it fits into a certain number of chars
2108
+ * Type of a log event
2059
2109
  * @author Gabe Abrams
2060
- * @param text the text to abbreviate
2061
- * @param maxChars the maximum number of chars to include
2062
- * @returns abbreviated text with length no greater than maxChars
2063
- * (including ellipses if applicable)
2064
2110
  */
2065
- const abbreviate = (text, maxChars) => {
2066
- // Check if already short enough
2067
- if (text.trim().length < maxChars) {
2068
- return text.trim();
2069
- }
2070
- // Abbreviate
2071
- const shortenedText = (text
2072
- .trim()
2073
- .substring(0, maxChars - 3)
2074
- .trim());
2075
- return `${shortenedText}...`;
2076
- };
2111
+ var LogType;
2112
+ (function (LogType) {
2113
+ // User action
2114
+ LogType["Action"] = "action";
2115
+ // Error
2116
+ LogType["Error"] = "error";
2117
+ })(LogType || (LogType = {}));
2118
+ var LogType$1 = LogType;
2077
2119
 
2078
2120
  /**
2079
- * Sum the numbers in an array
2121
+ * Types of actions
2080
2122
  * @author Gabe Abrams
2081
- * @param nums the numbers to sum
2082
- * @returns the sum of the numbers
2083
2123
  */
2084
- const sum = (nums) => {
2085
- return nums.reduce((a, b) => {
2086
- return (a + b);
2087
- }, 0);
2088
- };
2124
+ var LogAction;
2125
+ (function (LogAction) {
2126
+ // Target was opened by the user (it was not on screen, but now it is)
2127
+ LogAction["Open"] = "open";
2128
+ // Target was closed by the user (it was on screen, but now it is not)
2129
+ LogAction["Close"] = "close";
2130
+ // Target was cancelled by the user (it was on closed without saving)
2131
+ LogAction["Cancel"] = "cancel";
2132
+ // Target was expanded by the user (it always remains on screen, but size was changed)
2133
+ LogAction["Expand"] = "expand";
2134
+ // Target was collapsed by the user (it always remains on screen, but size was changed)
2135
+ LogAction["Collapse"] = "collapse";
2136
+ // Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
2137
+ LogAction["View"] = "view";
2138
+ // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2139
+ LogAction["Interrupt"] = "interrupt";
2140
+ // Target was created by the user (it did not exist before)
2141
+ LogAction["Create"] = "create";
2142
+ // Target was edited by the user (it existed and was changed)
2143
+ LogAction["Edit"] = "edit";
2144
+ // Target was deleted by the user (it existed and now it doesn't)
2145
+ LogAction["Delete"] = "delete";
2146
+ // Target was added by the user (it already existed and was added to another place)
2147
+ LogAction["Add"] = "add";
2148
+ // Target was removed by the user (it was removed from something but still exists)
2149
+ LogAction["Remove"] = "remove";
2150
+ // Target was activated by the user (click, check, tap, keypress, etc.)
2151
+ LogAction["Activate"] = "activate";
2152
+ // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2153
+ LogAction["Deactivate"] = "deactivate";
2154
+ // User showed interest in a target (hover, peek, etc.)
2155
+ LogAction["Peek"] = "peek";
2156
+ // Unknown action
2157
+ LogAction["Unknown"] = "unknown";
2158
+ })(LogAction || (LogAction = {}));
2159
+ var LogAction$1 = LogAction;
2089
2160
 
2090
2161
  /**
2091
- * Get the average of a set of numbers
2162
+ * Server-side API param types
2092
2163
  * @author Gabe Abrams
2093
- * @param nums the numbers to average
2094
- * @returns average value or 0 if no numbers
2095
2164
  */
2096
- const avg = (nums) => {
2097
- // Handle empty array case
2098
- if (nums.length === 0) {
2099
- return 0;
2100
- }
2101
- // Get the total value
2102
- const total = sum(nums);
2103
- // Get average
2104
- return (total / nums.length);
2105
- };
2165
+ var ParamType;
2166
+ (function (ParamType) {
2167
+ ParamType["Boolean"] = "boolean";
2168
+ ParamType["BooleanOptional"] = "boolean-optional";
2169
+ ParamType["Float"] = "float";
2170
+ ParamType["FloatOptional"] = "float-optional";
2171
+ ParamType["Int"] = "int";
2172
+ ParamType["IntOptional"] = "int-optional";
2173
+ ParamType["JSON"] = "json";
2174
+ ParamType["JSONOptional"] = "json-optional";
2175
+ ParamType["String"] = "string";
2176
+ ParamType["StringOptional"] = "string-optional";
2177
+ })(ParamType || (ParamType = {}));
2178
+ var ParamType$1 = ParamType;
2106
2179
 
2107
2180
  /**
2108
- * Round a number (ceiling) to a certain number of decimals
2181
+ * Round a number to a certain number of decimals
2109
2182
  * @author Gabe Abrams
2110
2183
  * @param num the number to round
2111
2184
  * @param numDecimals the number of decimals to round to
2112
2185
  * @returns rounded number
2113
2186
  */
2114
- const ceilToNumDecimals = (num, numDecimals) => {
2187
+ const roundToNumDecimals = (num, numDecimals) => {
2115
2188
  const rounder = 10 ** numDecimals;
2116
- return (Math.ceil(num * rounder) / rounder);
2189
+ return (Math.round(num * rounder) / rounder);
2117
2190
  };
2118
2191
 
2119
2192
  /**
2120
- * Round a number (floor) to a certain number of decimals
2193
+ * Escape a CSV cell if needed
2121
2194
  * @author Gabe Abrams
2122
- * @param num the number to round
2123
- * @param numDecimals the number of decimals to round to
2124
- * @returns rounded number
2195
+ * @param text the cell contents
2196
+ * @returns escaped cell text
2125
2197
  */
2126
- const floorToNumDecimals = (num, numDecimals) => {
2127
- const rounder = 10 ** numDecimals;
2128
- return (Math.floor(num * rounder) / rounder);
2198
+ const escapeCellText = (text) => {
2199
+ if (!text.includes(',')) {
2200
+ // No need to escape
2201
+ return text;
2202
+ }
2203
+ // Perform escape
2204
+ return `"${text.replace(/"/g, '""')}`;
2129
2205
  };
2130
-
2131
2206
  /**
2132
- * Force a number to stay within specific bounds
2207
+ * Generate a CSV file
2133
2208
  * @author Gabe Abrams
2134
- * @param num the number to move into the bounds
2135
- * @param min the minimum number in the bound
2136
- * @param max the maximum number in the bound
2137
- * @returns bounded number
2209
+ * @param data list of row data in the form of json objects
2210
+ * @param columns list of columns to include in the csv
2211
+ * @returns multiline csv string
2138
2212
  */
2139
- const forceNumIntoBounds = (num, min, max) => {
2140
- return Math.max(min, Math.min(max, num));
2213
+ const genCSV = (data, columns) => {
2214
+ let csv = '';
2215
+ // Add header
2216
+ csv += (columns
2217
+ .map((column) => {
2218
+ return escapeCellText(column.title);
2219
+ })
2220
+ .join(','));
2221
+ // Add each row
2222
+ data.forEach((datum) => {
2223
+ csv += (columns
2224
+ .map((column) => {
2225
+ return escapeCellText(datum[column.param]);
2226
+ })
2227
+ .join(','));
2228
+ });
2229
+ // Return
2230
+ return csv;
2141
2231
  };
2142
2232
 
2143
2233
  /**
2144
- * Pad a number's decimal with zeros on the right
2145
- * (e.g. 5.2 becomes 5.20 with 2 digit padding)
2234
+ * Button for downloading a csv file
2146
2235
  * @author Gabe Abrams
2147
- * @param num the number to pad
2148
- * @param numDigits the minimum number of digits after the decimal
2149
- * @returns padded number
2150
2236
  */
2151
- const padDecimalZeros = (num, numDigits) => {
2152
- // Skip if nothing to do
2153
- if (numDigits < 1) {
2154
- return String(num);
2155
- }
2156
- // Convert to string
2157
- let out = String(num);
2158
- // Add a decimal point if there isn't one
2159
- if (!out.includes('.')) {
2160
- out += '.';
2161
- }
2162
- // Add zeros
2163
- while (out.split('.')[1].length < numDigits) {
2164
- out = `${out}0`;
2165
- }
2166
- // Return
2167
- return out;
2237
+ /*------------------------------------------------------------------------*/
2238
+ /* Component */
2239
+ /*------------------------------------------------------------------------*/
2240
+ const CSVDownloadButton = (props) => {
2241
+ /*------------------------------------------------------------------------*/
2242
+ /* Setup */
2243
+ /*------------------------------------------------------------------------*/
2244
+ /* -------------- Props ------------- */
2245
+ // Destructure all props
2246
+ const { filename, csv, id, className, ariaLabel, style, onClick, children, } = props;
2247
+ /*------------------------------------------------------------------------*/
2248
+ /* Render */
2249
+ /*------------------------------------------------------------------------*/
2250
+ /*----------------------------------------*/
2251
+ /* Main UI */
2252
+ /*----------------------------------------*/
2253
+ // Render the button
2254
+ return (React__default["default"].createElement("a", { id: id, download: filename, href: `data:application/octet-stream,${csv}`, className: `CSVDownloadButton-button ${className !== null && className !== void 0 ? className : 'btn btn-secondary'}`, "aria-label": (ariaLabel
2255
+ ? `Click to download ${filename}`
2256
+ : ariaLabel), style: style, onClick: onClick },
2257
+ !children && (React__default["default"].createElement(React__default["default"].Fragment, null,
2258
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCloudDownloadAlt, className: "mr-2" }),
2259
+ "Download CSV")),
2260
+ children));
2168
2261
  };
2169
2262
 
2170
2263
  /**
2171
- * Pad a number with zeros on the left (e.g. 5 becomes 05 with 2 digit padding)
2264
+ * Intelligent table
2172
2265
  * @author Gabe Abrams
2173
- * @param num the number to pad
2174
- * @param numDigits the minimum number of digits before the decimal
2266
+ */
2267
+ // Sort types
2268
+ var SortType;
2269
+ (function (SortType) {
2270
+ // Ascending
2271
+ SortType["Ascending"] = "ascending";
2272
+ // Descending
2273
+ SortType["Descending"] = "descending";
2274
+ })(SortType || (SortType = {}));
2275
+ /* ------------- Actions ------------ */
2276
+ // Types of actions
2277
+ var ActionType$1;
2278
+ (function (ActionType) {
2279
+ // Toggle sort column param
2280
+ ActionType["ToggleSortColumn"] = "toggle-sort-column";
2281
+ // Toggle the visibility of a column
2282
+ ActionType["ToggleColumnVisibility"] = "toggle-column-visibility";
2283
+ // Toggle the column visibility customization modal
2284
+ ActionType["ToggleColVisCusModalVisibility"] = "toggle-col-vis-cus-modal-visibility";
2285
+ })(ActionType$1 || (ActionType$1 = {}));
2286
+ /**
2287
+ * Reducer that executes actions
2288
+ * @author Gabe Abrams
2289
+ * @param state current state
2290
+ * @param action action to execute
2291
+ */
2292
+ const reducer$1 = (state, action) => {
2293
+ switch (action.type) {
2294
+ case ActionType$1.ToggleSortColumn: {
2295
+ if (action.param !== state.sortColumnParam) {
2296
+ // Different column param
2297
+ return Object.assign(Object.assign({}, state), { sortColumnParam: action.param, sortType: SortType.Ascending });
2298
+ }
2299
+ if (state.sortType === SortType.Ascending) {
2300
+ // Switch to descending
2301
+ return Object.assign(Object.assign({}, state), { sortType: SortType.Descending });
2302
+ }
2303
+ // Stop sorting by column
2304
+ return Object.assign(Object.assign({}, state), { sortColumnParam: undefined, sortType: SortType.Ascending });
2305
+ }
2306
+ case ActionType$1.ToggleColumnVisibility: {
2307
+ const { columnVisibilityMap } = state;
2308
+ columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
2309
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2310
+ }
2311
+ case ActionType$1.ToggleColVisCusModalVisibility: {
2312
+ return Object.assign(Object.assign({}, state), { columnVisibilityCustomizationModalVisible: !state.columnVisibilityCustomizationModalVisible });
2313
+ }
2314
+ default: {
2315
+ return state;
2316
+ }
2317
+ }
2318
+ };
2319
+ /*------------------------------------------------------------------------*/
2320
+ /* Component */
2321
+ /*------------------------------------------------------------------------*/
2322
+ const IntelliTable = (props) => {
2323
+ /*------------------------------------------------------------------------*/
2324
+ /* Setup */
2325
+ /*------------------------------------------------------------------------*/
2326
+ var _a;
2327
+ /* -------------- Props ------------- */
2328
+ // Destructure all props
2329
+ const { title, id, data, columns, } = props;
2330
+ /* -------------- State ------------- */
2331
+ // Initial state
2332
+ const initColumnVisibilityMap = {};
2333
+ columns.forEach((column) => {
2334
+ initColumnVisibilityMap[column.param] = !column.startsHidden;
2335
+ });
2336
+ const initialState = {
2337
+ sortColumnParam: undefined,
2338
+ sortType: SortType.Descending,
2339
+ columnVisibilityMap: initColumnVisibilityMap,
2340
+ columnVisibilityCustomizationModalVisible: false,
2341
+ };
2342
+ // Initialize state
2343
+ const [state, dispatch] = React.useReducer(reducer$1, initialState);
2344
+ // Destructure common state
2345
+ const { sortColumnParam, sortType, columnVisibilityMap, columnVisibilityCustomizationModalVisible, } = state;
2346
+ /* ------- Col Vis Customization Modal ------ */
2347
+ if (columnVisibilityCustomizationModalVisible) {
2348
+ // Create modal
2349
+ (React__default["default"].createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2350
+ dispatch({
2351
+ type: ActionType$1.ToggleColVisCusModalVisibility,
2352
+ });
2353
+ } }, columns.map((column) => {
2354
+ return (React__default["default"].createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, text: column.title, onChanged: () => {
2355
+ dispatch({
2356
+ type: ActionType$1.ToggleColumnVisibility,
2357
+ param: column.param,
2358
+ });
2359
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column` }));
2360
+ })));
2361
+ }
2362
+ /*----------------------------------------*/
2363
+ /* Main UI */
2364
+ /*----------------------------------------*/
2365
+ // Table header
2366
+ const headerCells = (columns
2367
+ .filter((column) => {
2368
+ return columnVisibilityMap[column.param];
2369
+ })
2370
+ .map((column) => {
2371
+ // Custom info based on current sort type
2372
+ let sortButtonAriaLabel;
2373
+ let sortIcon = freeSolidSvgIcons.faSort;
2374
+ if (!sortColumnParam) {
2375
+ // Not being sorted yet
2376
+ sortButtonAriaLabel = `sort ascending by ${column.title}`;
2377
+ sortIcon = freeSolidSvgIcons.faSort;
2378
+ }
2379
+ else if (column.param === sortColumnParam) {
2380
+ // Already sorted by this column
2381
+ if (sortType === SortType.Ascending) {
2382
+ // Sorted ascending
2383
+ sortButtonAriaLabel = `sort descending by ${column.title}`;
2384
+ sortIcon = freeSolidSvgIcons.faSortDown;
2385
+ }
2386
+ else {
2387
+ // Sorted descending
2388
+ sortButtonAriaLabel = `stop sorting by ${column.title}`;
2389
+ sortIcon = freeSolidSvgIcons.faSortUp;
2390
+ }
2391
+ }
2392
+ else {
2393
+ // Sorted by a different column
2394
+ sortButtonAriaLabel = `sort ascending by ${column.title}`;
2395
+ sortIcon = freeSolidSvgIcons.faSort;
2396
+ }
2397
+ // Create the cell UI
2398
+ return (React__default["default"].createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}` },
2399
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-center flex-row h-100" },
2400
+ React__default["default"].createElement("span", { className: "text-nowrap" }, column.title),
2401
+ React__default["default"].createElement("div", null,
2402
+ React__default["default"].createElement("button", { type: "button", className: "btn btn-light btn-sm ms-1", "aria-label": sortButtonAriaLabel, onClick: () => {
2403
+ dispatch({
2404
+ type: ActionType$1.ToggleSortColumn,
2405
+ param: column.param,
2406
+ });
2407
+ } },
2408
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: sortIcon }))))));
2409
+ }));
2410
+ const tableHeader = (React__default["default"].createElement("thead", null,
2411
+ React__default["default"].createElement("tr", null, headerCells)));
2412
+ // Sort data
2413
+ let sortedData = [...data];
2414
+ const paramType = (_a = columns.find((column) => {
2415
+ return (column.param === sortColumnParam);
2416
+ })) === null || _a === void 0 ? void 0 : _a.type;
2417
+ const descending = (sortType === SortType.Descending);
2418
+ if (sortColumnParam) {
2419
+ sortedData.sort((a, b) => {
2420
+ const aVal = a[sortColumnParam];
2421
+ const bVal = b[sortColumnParam];
2422
+ // Sort differently based on the data type
2423
+ // > Boolean
2424
+ if (paramType === ParamType$1.Boolean) {
2425
+ if (aVal && !bVal) {
2426
+ return (descending ? -1 : 1);
2427
+ }
2428
+ if (!aVal && bVal) {
2429
+ return (descending ? 1 : -1);
2430
+ }
2431
+ return 0;
2432
+ }
2433
+ // > Number
2434
+ if (paramType === ParamType$1.Int
2435
+ || paramType === ParamType$1.Float) {
2436
+ return (descending
2437
+ ? (bVal - aVal)
2438
+ : (aVal - bVal));
2439
+ }
2440
+ // > String
2441
+ if (paramType === ParamType$1.String) {
2442
+ if (aVal < bVal) {
2443
+ return (descending ? -1 : 1);
2444
+ }
2445
+ if (aVal > bVal) {
2446
+ return (descending ? 1 : -1);
2447
+ }
2448
+ return 0;
2449
+ }
2450
+ // > JSON
2451
+ if (paramType === ParamType$1.JSON) {
2452
+ const aSize = (Array.isArray(aVal)
2453
+ ? aVal.length
2454
+ : Object.keys(aVal).length);
2455
+ const bSize = (Array.isArray(bVal)
2456
+ ? bVal.length
2457
+ : Object.keys(bVal).length);
2458
+ return (descending
2459
+ ? (bSize - aSize)
2460
+ : (aSize - bSize));
2461
+ }
2462
+ // No sort
2463
+ return 0;
2464
+ });
2465
+ }
2466
+ // Table body
2467
+ const rows = sortedData.map((datum) => {
2468
+ // Build cells
2469
+ const cells = (columns
2470
+ .filter((column) => {
2471
+ return columnVisibilityMap[column.param];
2472
+ })
2473
+ .map((column) => {
2474
+ // Get value
2475
+ let value = datum;
2476
+ const paramParts = column.param.split('.');
2477
+ paramParts.forEach((paramPart) => {
2478
+ value = (value !== null && value !== void 0 ? value : {})[paramPart];
2479
+ });
2480
+ let fullValue;
2481
+ let visibleValue;
2482
+ let title = '';
2483
+ if (column.type === ParamType$1.Boolean) {
2484
+ fullValue = !!(value);
2485
+ const noValue = (value === undefined
2486
+ || value === null);
2487
+ visibleValue = (noValue
2488
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCheckCircle }))
2489
+ : (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faXmarkCircle })));
2490
+ title = (fullValue ? 'True' : 'False');
2491
+ }
2492
+ else if (column.type === ParamType$1.Int) {
2493
+ fullValue = Number.parseInt(value, 10);
2494
+ const noValue = Number.isNaN(fullValue);
2495
+ visibleValue = (noValue
2496
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2497
+ : fullValue);
2498
+ title = String(fullValue);
2499
+ }
2500
+ else if (column.type === ParamType$1.Float) {
2501
+ fullValue = Number.parseFloat(value);
2502
+ const noValue = Number.isNaN(fullValue);
2503
+ visibleValue = (noValue
2504
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2505
+ : roundToNumDecimals(fullValue, 2));
2506
+ title = String(fullValue);
2507
+ }
2508
+ else if (column.type === ParamType$1.String) {
2509
+ fullValue = String(value).trim();
2510
+ const noValue = (value.trim().length) === 0;
2511
+ visibleValue = (noValue
2512
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2513
+ : fullValue);
2514
+ title = `"${value}"`;
2515
+ }
2516
+ else if (column.type === ParamType$1.JSON) {
2517
+ fullValue = JSON.stringify(value);
2518
+ const noValue = (Array.isArray(value)
2519
+ ? (!value || value.length === 0)
2520
+ : Object.keys(value !== null && value !== void 0 ? value : {}).length === 0);
2521
+ visibleValue = (noValue
2522
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2523
+ : fullValue);
2524
+ }
2525
+ // Create UI
2526
+ return (React__default["default"].createElement("td", { key: `${datum.id}-${column.param}`, title: title }, visibleValue));
2527
+ }));
2528
+ // Add cells to a row
2529
+ return (React__default["default"].createElement("tr", { key: datum.id }, cells));
2530
+ });
2531
+ const tableBody = (React__default["default"].createElement("tbody", null, rows));
2532
+ // Build table
2533
+ const table = (React__default["default"].createElement("table", { className: "table table-dark table-striped" },
2534
+ tableHeader,
2535
+ tableBody));
2536
+ // Count the number of hidden columns
2537
+ const numHiddenCols = (Object.values(columnVisibilityMap)
2538
+ .filter((isVisible) => {
2539
+ return !isVisible;
2540
+ })
2541
+ .length);
2542
+ // Build CSV
2543
+ const csv = genCSV(data, columns);
2544
+ // Build main UI
2545
+ return (React__default["default"].createElement("div", { className: `IntelliTable-container-${id}` },
2546
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-center" },
2547
+ React__default["default"].createElement("h3", { className: "m-0" }, title),
2548
+ React__default["default"].createElement("div", { className: "flex-grow-1 text-end" },
2549
+ React__default["default"].createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: `${title}.csv`, csv: csv }),
2550
+ React__default["default"].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: () => {
2551
+ dispatch({
2552
+ type: ActionType$1.ToggleColVisCusModalVisibility,
2553
+ });
2554
+ } },
2555
+ "Show/Hide Cols",
2556
+ numHiddenCols > 0 && (React__default["default"].createElement(React__default["default"].Fragment, null,
2557
+ ' ',
2558
+ "(",
2559
+ numHiddenCols,
2560
+ ' ',
2561
+ "hidden)"))))),
2562
+ React__default["default"].createElement("div", { className: `IntelliTable-table-${id}`, style: {
2563
+ overflowX: 'auto',
2564
+ } }, table)));
2565
+ };
2566
+
2567
+ /**
2568
+ * Log reviewer panel that allows users (must be approved admins) to
2569
+ * review logs written by dce-reactkit
2570
+ * @author Gabe Abrams
2571
+ */
2572
+ // Types of filter drawers
2573
+ var FilterDrawer;
2574
+ (function (FilterDrawer) {
2575
+ FilterDrawer["Date"] = "date";
2576
+ FilterDrawer["Context"] = "context";
2577
+ FilterDrawer["Tag"] = "tag";
2578
+ FilterDrawer["Action"] = "action";
2579
+ FilterDrawer["Advanced"] = "advanced";
2580
+ })(FilterDrawer || (FilterDrawer = {}));
2581
+ /*------------------------------------------------------------------------*/
2582
+ /* Style */
2583
+ /*------------------------------------------------------------------------*/
2584
+ const style = `
2585
+ .LogReviewer-outer-container {
2586
+ /* Full Screen */
2587
+ display: inline-block;
2588
+ left: 0;
2589
+ top: 0;
2590
+ width: 100vw;
2591
+ height: 100vh;
2592
+
2593
+ /* On Top and Fixed */
2594
+ position: fixed;
2595
+ z-index: 90000;
2596
+
2597
+ /* Space around contents */
2598
+ padding: 0.5rem;
2599
+
2600
+ /* Dark Background */
2601
+ background-color: rgba(0, 0, 0, 0.7);
2602
+
2603
+ /* No Clickthrough */
2604
+ pointer-events: none;
2605
+ }
2606
+
2607
+ .LogReviewer-inner-container {
2608
+ /* Full screen, rounded modal-like look */
2609
+ display: flex;
2610
+ height: 100%;
2611
+ border: 0.05rem solid black;
2612
+ border-radius: 0.5rem;
2613
+ overflow: hidden;
2614
+ padding: 0.7rem;
2615
+
2616
+ /* Solid background */
2617
+ background-color: white;
2618
+ color: black;
2619
+
2620
+ /* Place contents in flex column */
2621
+ flex-direction: column;
2622
+
2623
+ /* Re-allow interaction */
2624
+ pointer-events: all;
2625
+ }
2626
+
2627
+ .LogReviewer-header {
2628
+ /* Elements in flex row */
2629
+ display: flex;
2630
+ flex-direction: row;
2631
+ }
2632
+
2633
+ .LogReviewer-header-title {
2634
+ /* Take up remaining width */
2635
+ flex-grow: 1;
2636
+ }
2637
+
2638
+ .LogReviewer-contents {
2639
+ /* Take up remaining height */
2640
+ flex-grow: 1;
2641
+
2642
+ /* Vertical scroll */
2643
+ overflow-y: auto;
2644
+ }
2645
+
2646
+ .LogReviewer-header-close-button {
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: #333 !important;
2653
+ }
2654
+ .LogReviewer-header-close-button:hover {
2655
+ border: 0 !important;
2656
+ background-color: transparent !important;
2657
+ padding-top: 0 !important;
2658
+ padding-bottom: 0 !important;
2659
+ margin: 0 !important;
2660
+ color: #000 !important;
2661
+ }
2662
+ `;
2663
+ /*------------------------------------------------------------------------*/
2664
+ /* Static Functions */
2665
+ /*------------------------------------------------------------------------*/
2666
+ /**
2667
+ * Turn a machine-readable name into a human-readable name
2668
+ * @author Gabe Abrams
2669
+ * @param name machine-readable name
2670
+ * @returns human-readable name
2671
+ */
2672
+ const genHumanReadableName = (machineReadableName) => {
2673
+ let humanReadableName = '';
2674
+ // Add chars and spaces
2675
+ const chars = machineReadableName.split('');
2676
+ chars.forEach((char) => {
2677
+ if (/[A-Z]/.test(char)) {
2678
+ // Uppercase! Add a space before
2679
+ humanReadableName += ' ';
2680
+ }
2681
+ humanReadableName += char;
2682
+ });
2683
+ // Trim and return
2684
+ return humanReadableName.trim();
2685
+ };
2686
+ /* ------------- Actions ------------ */
2687
+ // Types of actions
2688
+ var ActionType;
2689
+ (function (ActionType) {
2690
+ // Show the loading bar
2691
+ ActionType["StartLoading"] = "start-loading";
2692
+ // Finish loading one or more months of logs
2693
+ ActionType["FinishLoading"] = "finish-loading";
2694
+ // Reset filters to initial values
2695
+ ActionType["ResetFilters"] = "reset-filters";
2696
+ // Choose a filter drawer to toggle
2697
+ ActionType["ToggleFilterDrawer"] = "toggle-filter-drawer";
2698
+ // Hide filter drawer
2699
+ ActionType["HideFilterDrawer"] = "hide-filter-drawer";
2700
+ // Handle the date filter state
2701
+ ActionType["UpdateDateFilterState"] = "update-date-filter-state";
2702
+ // Update the context filter state
2703
+ ActionType["UpdateContextFilterState"] = "update-context-filter-state";
2704
+ // Update the tag filter state
2705
+ ActionType["UpdateTagFilterState"] = "update-tag-filter-state";
2706
+ // Update the action and error filter state
2707
+ ActionType["UpdateActionErrorFilterState"] = "update-action-error-filter-state";
2708
+ // Update the advanced filter state
2709
+ ActionType["UpdateAdvancedFilterState"] = "update-advanced-filter-state";
2710
+ })(ActionType || (ActionType = {}));
2711
+ /**
2712
+ * Reducer that executes actions
2713
+ * @author Gabe Abrams
2714
+ * @param state current state
2715
+ * @param action action to execute
2716
+ */
2717
+ const reducer = (state, action) => {
2718
+ switch (action.type) {
2719
+ case ActionType.StartLoading: {
2720
+ return Object.assign(Object.assign({}, state), { loading: true });
2721
+ }
2722
+ case ActionType.FinishLoading: {
2723
+ return Object.assign(Object.assign({}, state), { loading: false, logMap: action.logMap });
2724
+ }
2725
+ case ActionType.ToggleFilterDrawer: {
2726
+ return Object.assign(Object.assign({}, state), { expandedFilterDrawer: (state.expandedFilterDrawer === action.filterDrawer
2727
+ ? undefined // hide
2728
+ : action.filterDrawer) });
2729
+ }
2730
+ case ActionType.HideFilterDrawer: {
2731
+ return Object.assign(Object.assign({}, state), { expandedFilterDrawer: undefined });
2732
+ }
2733
+ case ActionType.ResetFilters: {
2734
+ return Object.assign(Object.assign({}, state), { dateFilterState: action.initDateFilterState, contextFilterState: action.initContextFilterState, tagFilterState: action.initTagFilterState, actionErrorFilterState: action.initActionErrorFilterState, advancedFilterState: action.initAdvancedFilterState });
2735
+ }
2736
+ case ActionType.UpdateDateFilterState: {
2737
+ return Object.assign(Object.assign({}, state), { dateFilterState: action.dateFilterState });
2738
+ }
2739
+ case ActionType.UpdateContextFilterState: {
2740
+ return Object.assign(Object.assign({}, state), { contextFilterState: action.contextFilterState });
2741
+ }
2742
+ case ActionType.UpdateTagFilterState: {
2743
+ return Object.assign(Object.assign({}, state), { tagFilterState: action.tagFilterState });
2744
+ }
2745
+ case ActionType.UpdateActionErrorFilterState: {
2746
+ return Object.assign(Object.assign({}, state), { actionErrorFilterState: action.actionErrorFilterState });
2747
+ }
2748
+ case ActionType.UpdateAdvancedFilterState: {
2749
+ return Object.assign(Object.assign({}, state), { advancedFilterState: action.advancedFilterState });
2750
+ }
2751
+ default: {
2752
+ return state;
2753
+ }
2754
+ }
2755
+ };
2756
+ /*------------------------------------------------------------------------*/
2757
+ /* Component */
2758
+ /*------------------------------------------------------------------------*/
2759
+ const LogReviewer = (props) => {
2760
+ /*------------------------------------------------------------------------*/
2761
+ /* Setup */
2762
+ /*------------------------------------------------------------------------*/
2763
+ var _a, _b, _c, _d, _e, _f;
2764
+ /* -------------- Props ------------- */
2765
+ // Destructure props
2766
+ const { LogMetadata, onClose, } = props;
2767
+ /* -------------- State ------------- */
2768
+ // Create initial date filter state
2769
+ const today = getTimeInfoInET();
2770
+ const initStartDate = {
2771
+ year: today.year,
2772
+ month: today.month,
2773
+ day: 1,
2774
+ };
2775
+ const initEndDate = {
2776
+ year: today.year,
2777
+ month: today.month,
2778
+ day: today.day,
2779
+ };
2780
+ const initDateFilterState = {
2781
+ startDate: initStartDate,
2782
+ endDate: initEndDate,
2783
+ };
2784
+ // Create initial context filter state
2785
+ const initContextFilterState = {};
2786
+ Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
2787
+ var _a, _b;
2788
+ const contextValue = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
2789
+ if (typeof contextValue === 'string') {
2790
+ // Case: no subcontexts
2791
+ initContextFilterState[contextValue] = true;
2792
+ }
2793
+ else {
2794
+ // Case: subcontexts exist
2795
+ initContextFilterState[contextValue._] = {};
2796
+ Object.values(((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {})[context]).forEach((subcontext) => {
2797
+ const subcontextValue = contextValue[subcontext];
2798
+ initContextFilterState[contextValue._][subcontextValue] = true;
2799
+ });
2800
+ }
2801
+ });
2802
+ // Create initial tag filter state
2803
+ const initTagFilterState = {};
2804
+ Object.values((_b = LogMetadata.Tag) !== null && _b !== void 0 ? _b : {}).forEach((tagValue) => {
2805
+ initTagFilterState[tagValue] = true;
2806
+ });
2807
+ // Create advanced filter state
2808
+ const initAdvancedFilterState = {
2809
+ userFirstName: '',
2810
+ userLastName: '',
2811
+ userEmail: '',
2812
+ userId: '',
2813
+ includeLearners: true,
2814
+ includeTTMs: true,
2815
+ includeAdmins: true,
2816
+ courseId: '',
2817
+ courseName: '',
2818
+ isMobile: undefined,
2819
+ source: undefined,
2820
+ routePath: '',
2821
+ routeTemplate: '',
2822
+ };
2823
+ // Create action and error filter state
2824
+ const initActionErrorFilterState = {
2825
+ type: undefined,
2826
+ errorMessage: '',
2827
+ errorCode: '',
2828
+ target: {},
2829
+ action: {},
2830
+ };
2831
+ Object.values((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {}).forEach((target) => {
2832
+ initActionErrorFilterState.target[target] = true;
2833
+ });
2834
+ Object.values(LogAction$1).forEach((action) => {
2835
+ initActionErrorFilterState.action[action] = true;
2836
+ });
2837
+ // Initial state
2838
+ const initialState = {
2839
+ loading: true,
2840
+ logMap: {},
2841
+ expandedFilterDrawer: undefined,
2842
+ dateFilterState: initDateFilterState,
2843
+ contextFilterState: initContextFilterState,
2844
+ tagFilterState: initTagFilterState,
2845
+ actionErrorFilterState: initActionErrorFilterState,
2846
+ advancedFilterState: initAdvancedFilterState,
2847
+ };
2848
+ // Initialize state
2849
+ const [state, dispatch] = React.useReducer(reducer, initialState);
2850
+ // Destructure common state
2851
+ const { loading, logMap, expandedFilterDrawer, dateFilterState, contextFilterState, tagFilterState, actionErrorFilterState, advancedFilterState, } = state;
2852
+ /*------------------------------------------------------------------------*/
2853
+ /* Component Functions */
2854
+ /*------------------------------------------------------------------------*/
2855
+ /**
2856
+ * Get the list of year/month combos that need to be loaded given a new
2857
+ * start or end date and the existing logMap
2858
+ * @author Gabe Abrams
2859
+ * @param newDateFilterState the new date filter state
2860
+ * @returns list of year/month combos that need to be loaded
2861
+ */
2862
+ const listMonthsToLoad = (newDateFilterState) => {
2863
+ // List of year/month combos that need to be loaded
2864
+ const toLoad = [];
2865
+ // Loop through dates
2866
+ let year = newDateFilterState.startDate.year;
2867
+ let month = newDateFilterState.startDate.month;
2868
+ while (
2869
+ // Earlier year
2870
+ (year <= newDateFilterState.endDate.year)
2871
+ // Current year but included month
2872
+ || (year === newDateFilterState.endDate.year
2873
+ && month <= newDateFilterState.endDate.month)) {
2874
+ // Add to list
2875
+ toLoad.push({
2876
+ year,
2877
+ month,
2878
+ });
2879
+ // Increment
2880
+ month += 1;
2881
+ if (month > 12) {
2882
+ month -= 12;
2883
+ year += 1;
2884
+ }
2885
+ }
2886
+ // Return
2887
+ return toLoad;
2888
+ };
2889
+ /**
2890
+ * Handle updated start/end dates (updates state, loads if necessary)
2891
+ * @author Gabe Abrams
2892
+ * @param newDateFilterState the new date filter state
2893
+ */
2894
+ const handleDateRangeUpdated = (newDateFilterState) => __awaiter(void 0, void 0, void 0, function* () {
2895
+ // Update state
2896
+ dispatch({
2897
+ type: ActionType.UpdateDateFilterState,
2898
+ dateFilterState: newDateFilterState,
2899
+ });
2900
+ // Check which year/month combos we need to load
2901
+ const toLoad = listMonthsToLoad(newDateFilterState);
2902
+ // If nothing to load, finished
2903
+ if (toLoad.length === 0) {
2904
+ return;
2905
+ }
2906
+ // Start loading
2907
+ dispatch({
2908
+ type: ActionType.StartLoading,
2909
+ });
2910
+ // Load required months
2911
+ try {
2912
+ for (let i = 0; i < toLoad.length; i++) {
2913
+ // Destructure
2914
+ const { year, month } = toLoad[i];
2915
+ // Load
2916
+ const logs = yield visitServerEndpoint({
2917
+ path: `${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/${year}/months/${month}`,
2918
+ method: 'GET',
2919
+ });
2920
+ // Add to map
2921
+ if (!logMap[year]) {
2922
+ logMap[year] = {};
2923
+ }
2924
+ logMap[year][month] = logs;
2925
+ }
2926
+ }
2927
+ catch (err) {
2928
+ return showFatalError(err);
2929
+ }
2930
+ // Finish loading
2931
+ dispatch({
2932
+ type: ActionType.FinishLoading,
2933
+ logMap,
2934
+ });
2935
+ });
2936
+ /*------------------------------------------------------------------------*/
2937
+ /* Lifecycle Functions */
2938
+ /*------------------------------------------------------------------------*/
2939
+ /**
2940
+ * Mount
2941
+ * @author Gabe Abrams
2942
+ */
2943
+ React.useEffect(() => {
2944
+ // Perform initial load
2945
+ handleDateRangeUpdated(dateFilterState);
2946
+ }, []);
2947
+ /*------------------------------------------------------------------------*/
2948
+ /* Render */
2949
+ /*------------------------------------------------------------------------*/
2950
+ /*----------------------------------------*/
2951
+ /* Main UI */
2952
+ /*----------------------------------------*/
2953
+ // Body that will be filled with the contents of the panel
2954
+ let body;
2955
+ /* ------------- Loading ------------ */
2956
+ if (loading) {
2957
+ body = (React__default["default"].createElement("div", { className: "text-center p-5" },
2958
+ React__default["default"].createElement(LoadingSpinner, null)));
2959
+ }
2960
+ /* ------------ Review UI ----------- */
2961
+ if (!loading) {
2962
+ /*----------------------------------------*/
2963
+ /* Filters */
2964
+ /*----------------------------------------*/
2965
+ // Filter toggle
2966
+ const filterToggles = (React__default["default"].createElement("div", { className: "LogReviewer-filter-toggles" },
2967
+ React__default["default"].createElement("h3", { className: "m-0" }, "Filters:"),
2968
+ React__default["default"].createElement("div", { className: "LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0" },
2969
+ React__default["default"].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: () => {
2970
+ dispatch({
2971
+ type: ActionType.ToggleFilterDrawer,
2972
+ filterDrawer: FilterDrawer.Date,
2973
+ });
2974
+ } },
2975
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCalendar, className: "me-2" }),
2976
+ "Date"),
2977
+ React__default["default"].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: () => {
2978
+ dispatch({
2979
+ type: ActionType.ToggleFilterDrawer,
2980
+ filterDrawer: FilterDrawer.Context,
2981
+ });
2982
+ } },
2983
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "me-2" }),
2984
+ "Context"),
2985
+ (LogMetadata.Tag && Object.keys(LogMetadata.Tag).length > 0) && (React__default["default"].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: () => {
2986
+ dispatch({
2987
+ type: ActionType.ToggleFilterDrawer,
2988
+ filterDrawer: FilterDrawer.Tag,
2989
+ });
2990
+ } },
2991
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTag, className: "me-2" }),
2992
+ "Tag")),
2993
+ React__default["default"].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: () => {
2994
+ dispatch({
2995
+ type: ActionType.ToggleFilterDrawer,
2996
+ filterDrawer: FilterDrawer.Action,
2997
+ });
2998
+ } },
2999
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faHammer, className: "me-2" }),
3000
+ "Action"),
3001
+ React__default["default"].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: () => {
3002
+ dispatch({
3003
+ type: ActionType.ToggleFilterDrawer,
3004
+ filterDrawer: FilterDrawer.Advanced,
3005
+ });
3006
+ } },
3007
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faList, className: "me-2" }),
3008
+ "Advanced"))));
3009
+ // Filter drawer
3010
+ let filterDrawer;
3011
+ if (expandedFilterDrawer) {
3012
+ if (expandedFilterDrawer === FilterDrawer.Date) {
3013
+ filterDrawer = (React__default["default"].createElement(TabBox, { title: "Date" },
3014
+ React__default["default"].createElement(SimpleDateChooser, { ariaLabel: "filter start date", name: "filter-start-date", year: dateFilterState.startDate.year, month: dateFilterState.startDate.month, day: dateFilterState.startDate.day, chooseFromPast: true, onChange: (month, day, year) => {
3015
+ dispatch({
3016
+ type: ActionType.UpdateDateFilterState,
3017
+ dateFilterState: Object.assign(Object.assign({}, dateFilterState), { startDate: { month, day, year } }),
3018
+ });
3019
+ } }),
3020
+ ' ',
3021
+ "to",
3022
+ ' ',
3023
+ React__default["default"].createElement(SimpleDateChooser, { ariaLabel: "filter end date", name: "filter-end-date", year: dateFilterState.endDate.year, month: dateFilterState.endDate.month, day: dateFilterState.endDate.day, chooseFromPast: true, onChange: (month, day, year) => {
3024
+ dispatch({
3025
+ type: ActionType.UpdateDateFilterState,
3026
+ dateFilterState: Object.assign(Object.assign({}, dateFilterState), { endDate: { month, day, year } }),
3027
+ });
3028
+ } })));
3029
+ }
3030
+ else if (expandedFilterDrawer === FilterDrawer.Context) {
3031
+ // Create item picker items
3032
+ const pickableItems = (Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {})
3033
+ .map((context) => {
3034
+ var _a;
3035
+ const value = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
3036
+ if (typeof value === 'string') {
3037
+ // No subcategories
3038
+ const item = {
3039
+ id: context,
3040
+ name: genHumanReadableName(context),
3041
+ isGroup: false,
3042
+ checked: !!contextFilterState[context],
3043
+ };
3044
+ return item;
3045
+ }
3046
+ // Has subcategories
3047
+ const children = (Object.keys(value)
3048
+ .filter((subcontext) => {
3049
+ return subcontext !== '_';
3050
+ })
3051
+ .map((subcontext) => {
3052
+ return {
3053
+ id: `${context}-${subcontext}`,
3054
+ name: genHumanReadableName(subcontext),
3055
+ isGroup: false,
3056
+ checked: !!value[subcontext],
3057
+ };
3058
+ }));
3059
+ const item = {
3060
+ id: context,
3061
+ name: context,
3062
+ isGroup: true,
3063
+ children,
3064
+ };
3065
+ return item;
3066
+ }));
3067
+ // Create filter UI
3068
+ filterDrawer = (React__default["default"].createElement(ItemPicker, { title: "Context", items: pickableItems, onChanged: (updatedItems) => {
3069
+ // Update our state
3070
+ updatedItems.forEach((pickableItem) => {
3071
+ if (pickableItem.isGroup) {
3072
+ // Has subcontexts
3073
+ pickableItem.children.forEach((subcontextItem) => {
3074
+ contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3075
+ });
3076
+ }
3077
+ else {
3078
+ // No subcontexts
3079
+ contextFilterState[pickableItem.id] = (pickableItem.checked);
3080
+ }
3081
+ });
3082
+ dispatch({
3083
+ type: ActionType.UpdateContextFilterState,
3084
+ contextFilterState,
3085
+ });
3086
+ } }));
3087
+ }
3088
+ else if (expandedFilterDrawer === FilterDrawer.Tag) {
3089
+ // Create filter UI
3090
+ filterDrawer = (React__default["default"].createElement(TabBox, { title: "Tags" }, Object.keys(tagFilterState)
3091
+ .map((tag, i) => {
3092
+ const description = genHumanReadableName(tag);
3093
+ return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-tag-${tag}-checkbox`, text: description, ariaLabel: `include logs tagged with "${description}" in results`, noMarginOnRight: i === Object.keys(tagFilterState).length - 1, onChanged: (checked) => {
3094
+ tagFilterState[tag] = checked;
3095
+ } }));
3096
+ })));
3097
+ }
3098
+ else if (expandedFilterDrawer === FilterDrawer.Action) {
3099
+ // Create filter UI
3100
+ filterDrawer = (React__default["default"].createElement(React__default["default"].Fragment, null,
3101
+ React__default["default"].createElement(TabBox, { title: "Log Type" },
3102
+ React__default["default"].createElement(RadioButton, { id: "LogReviewer-type-all", text: "All Logs", onSelected: () => {
3103
+ actionErrorFilterState.type = undefined;
3104
+ dispatch({
3105
+ type: ActionType.UpdateActionErrorFilterState,
3106
+ actionErrorFilterState,
3107
+ });
3108
+ }, ariaLabel: "show logs of all types", selected: actionErrorFilterState.type === undefined }),
3109
+ React__default["default"].createElement(RadioButton, { id: "LogReviewer-type-action-only", text: "Action Logs Only", onSelected: () => {
3110
+ actionErrorFilterState.type = LogType$1.Action;
3111
+ dispatch({
3112
+ type: ActionType.UpdateActionErrorFilterState,
3113
+ actionErrorFilterState,
3114
+ });
3115
+ }, ariaLabel: "only show action logs", selected: actionErrorFilterState.type === LogType$1.Action }),
3116
+ React__default["default"].createElement(RadioButton, { id: "LogReviewer-type-error-only", text: "Action Error Only", onSelected: () => {
3117
+ actionErrorFilterState.type = LogType$1.Error;
3118
+ dispatch({
3119
+ type: ActionType.UpdateActionErrorFilterState,
3120
+ actionErrorFilterState,
3121
+ });
3122
+ }, ariaLabel: "only show error logs", selected: actionErrorFilterState.type === LogType$1.Error, noMarginOnRight: true })),
3123
+ (actionErrorFilterState.type === undefined
3124
+ || actionErrorFilterState.type === LogType$1.Action) && (React__default["default"].createElement(TabBox, { title: "Action Log Details" },
3125
+ React__default["default"].createElement(ButtonInputGroup, { label: "Action", className: "mb-2" }, Object.keys(LogAction$1)
3126
+ .map((action, i) => {
3127
+ const description = genHumanReadableName(action);
3128
+ return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-action-${action}-checkbox`, text: description, ariaLabel: `include logs with action type "${description}" in results`, noMarginOnRight: i === Object.keys(LogAction$1).length - 1, onChanged: (checked) => {
3129
+ actionErrorFilterState.action[action] = checked;
3130
+ dispatch({
3131
+ type: ActionType.UpdateActionErrorFilterState,
3132
+ actionErrorFilterState,
3133
+ });
3134
+ } }));
3135
+ })),
3136
+ React__default["default"].createElement(ButtonInputGroup, { label: "Target" },
3137
+ (Object.keys((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {}).length === 0) && (React__default["default"].createElement("div", null, "This app does not have any targets yet.")),
3138
+ Object.keys((_f = LogMetadata.Target) !== null && _f !== void 0 ? _f : {})
3139
+ .map((target, i) => {
3140
+ var _a;
3141
+ const description = genHumanReadableName(target);
3142
+ return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, onChanged: (checked) => {
3143
+ actionErrorFilterState.target[target] = checked;
3144
+ dispatch({
3145
+ type: ActionType.UpdateActionErrorFilterState,
3146
+ actionErrorFilterState,
3147
+ });
3148
+ }, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
3149
+ })))),
3150
+ (actionErrorFilterState.type === undefined
3151
+ || actionErrorFilterState.type === LogType$1.Error) && (React__default["default"].createElement(TabBox, { title: "Error Log Details" },
3152
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3153
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Error Message"),
3154
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error message", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3155
+ actionErrorFilterState.errorMessage = e.target.value;
3156
+ dispatch({
3157
+ type: ActionType.UpdateActionErrorFilterState,
3158
+ actionErrorFilterState,
3159
+ });
3160
+ } })),
3161
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3162
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Error Code"),
3163
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3164
+ actionErrorFilterState.errorCode = ((e.target.value)
3165
+ .trim()
3166
+ .toUpperCase());
3167
+ dispatch({
3168
+ type: ActionType.UpdateActionErrorFilterState,
3169
+ actionErrorFilterState,
3170
+ });
3171
+ } }))))));
3172
+ }
3173
+ else if (expandedFilterDrawer === FilterDrawer.Advanced) {
3174
+ // Create advanced filter ui
3175
+ filterDrawer = (React__default["default"].createElement(React__default["default"].Fragment, null,
3176
+ React__default["default"].createElement(TabBox, { title: "User Info" },
3177
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3178
+ React__default["default"].createElement("span", { className: "input-group-text" }, "User First Name"),
3179
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, onChange: (e) => {
3180
+ advancedFilterState.userFirstName = e.target.value;
3181
+ dispatch({
3182
+ type: ActionType.UpdateAdvancedFilterState,
3183
+ advancedFilterState,
3184
+ });
3185
+ } })),
3186
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3187
+ React__default["default"].createElement("span", { className: "input-group-text" }, "User Last Name"),
3188
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, onChange: (e) => {
3189
+ advancedFilterState.userLastName = e.target.value;
3190
+ dispatch({
3191
+ type: ActionType.UpdateAdvancedFilterState,
3192
+ advancedFilterState,
3193
+ });
3194
+ } })),
3195
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3196
+ React__default["default"].createElement("span", { className: "input-group-text" }, "User Email"),
3197
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user email", value: advancedFilterState.userEmail, onChange: (e) => {
3198
+ advancedFilterState.userEmail = ((e.target.value)
3199
+ .trim());
3200
+ dispatch({
3201
+ type: ActionType.UpdateAdvancedFilterState,
3202
+ advancedFilterState,
3203
+ });
3204
+ } })),
3205
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3206
+ React__default["default"].createElement("span", { className: "input-group-text" }, "User Canvas Id"),
3207
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, onChange: (e) => {
3208
+ const { value } = e.target;
3209
+ // Only update if value contains only numbers
3210
+ if (/^\d+$/.test(value)) {
3211
+ advancedFilterState.userId = ((e.target.value)
3212
+ .trim());
3213
+ }
3214
+ dispatch({
3215
+ type: ActionType.UpdateAdvancedFilterState,
3216
+ advancedFilterState,
3217
+ });
3218
+ } })),
3219
+ React__default["default"].createElement(ButtonInputGroup, { label: "Role" },
3220
+ React__default["default"].createElement(CheckboxButton, { text: "Students", onChanged: (checked) => {
3221
+ advancedFilterState.includeLearners = checked;
3222
+ dispatch({
3223
+ type: ActionType.UpdateAdvancedFilterState,
3224
+ advancedFilterState,
3225
+ });
3226
+ }, checked: advancedFilterState.includeLearners, ariaLabel: "show logs from students" }),
3227
+ React__default["default"].createElement(CheckboxButton, { text: "Teaching Team Members", onChanged: (checked) => {
3228
+ advancedFilterState.includeTTMs = checked;
3229
+ dispatch({
3230
+ type: ActionType.UpdateAdvancedFilterState,
3231
+ advancedFilterState,
3232
+ });
3233
+ }, checked: advancedFilterState.includeTTMs, ariaLabel: "show logs from teaching team members" }),
3234
+ React__default["default"].createElement(CheckboxButton, { text: "Admins", onChanged: (checked) => {
3235
+ advancedFilterState.includeAdmins = checked;
3236
+ dispatch({
3237
+ type: ActionType.UpdateAdvancedFilterState,
3238
+ advancedFilterState,
3239
+ });
3240
+ }, checked: advancedFilterState.includeAdmins, ariaLabel: "show logs from admins" }))),
3241
+ React__default["default"].createElement(TabBox, { title: "Course Info" },
3242
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3243
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Course Name"),
3244
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, onChange: (e) => {
3245
+ advancedFilterState.courseName = e.target.value;
3246
+ dispatch({
3247
+ type: ActionType.UpdateAdvancedFilterState,
3248
+ advancedFilterState,
3249
+ });
3250
+ } })),
3251
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3252
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Course Canvas Id"),
3253
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, onChange: (e) => {
3254
+ const { value } = e.target;
3255
+ // Only update if value contains only numbers
3256
+ if (/^\d+$/.test(value)) {
3257
+ advancedFilterState.courseId = ((e.target.value)
3258
+ .trim());
3259
+ }
3260
+ dispatch({
3261
+ type: ActionType.UpdateAdvancedFilterState,
3262
+ advancedFilterState,
3263
+ });
3264
+ } }))),
3265
+ React__default["default"].createElement(TabBox, { title: "Device Info" },
3266
+ React__default["default"].createElement(ButtonInputGroup, { label: "Device Type" },
3267
+ React__default["default"].createElement(RadioButton, { text: "All Devices", ariaLabel: "show logs from all devices", selected: advancedFilterState.isMobile === undefined, onSelected: () => {
3268
+ advancedFilterState.isMobile = undefined;
3269
+ dispatch({
3270
+ type: ActionType.UpdateAdvancedFilterState,
3271
+ advancedFilterState,
3272
+ });
3273
+ } }),
3274
+ React__default["default"].createElement(RadioButton, { text: "Mobile Only", ariaLabel: "show logs from mobile devices", selected: advancedFilterState.isMobile === true, onSelected: () => {
3275
+ advancedFilterState.isMobile = true;
3276
+ dispatch({
3277
+ type: ActionType.UpdateAdvancedFilterState,
3278
+ advancedFilterState,
3279
+ });
3280
+ } }),
3281
+ React__default["default"].createElement(RadioButton, { text: "Desktop Only", ariaLabel: "show logs from desktop devices", selected: advancedFilterState.isMobile === false, onSelected: () => {
3282
+ advancedFilterState.isMobile = false;
3283
+ dispatch({
3284
+ type: ActionType.UpdateAdvancedFilterState,
3285
+ advancedFilterState,
3286
+ });
3287
+ }, noMarginOnRight: true }))),
3288
+ React__default["default"].createElement(TabBox, { title: "Source" },
3289
+ React__default["default"].createElement(ButtonInputGroup, { label: "Source Type" },
3290
+ React__default["default"].createElement(RadioButton, { text: "Both", ariaLabel: "show logs from all sources", selected: advancedFilterState.source === undefined, onSelected: () => {
3291
+ advancedFilterState.source = undefined;
3292
+ dispatch({
3293
+ type: ActionType.UpdateAdvancedFilterState,
3294
+ advancedFilterState,
3295
+ });
3296
+ } }),
3297
+ React__default["default"].createElement(RadioButton, { text: "Client Only", ariaLabel: "show logs from client source", selected: advancedFilterState.source === LogSource$1.Client, onSelected: () => {
3298
+ advancedFilterState.source = LogSource$1.Client;
3299
+ dispatch({
3300
+ type: ActionType.UpdateAdvancedFilterState,
3301
+ advancedFilterState,
3302
+ });
3303
+ } }),
3304
+ React__default["default"].createElement(RadioButton, { text: "Server Only", ariaLabel: "show logs from server source", selected: advancedFilterState.source === LogSource$1.Server, onSelected: () => {
3305
+ advancedFilterState.source = LogSource$1.Server;
3306
+ dispatch({
3307
+ type: ActionType.UpdateAdvancedFilterState,
3308
+ advancedFilterState,
3309
+ });
3310
+ }, noMarginOnRight: true })),
3311
+ advancedFilterState.source !== LogSource$1.Client && (React__default["default"].createElement("div", { className: "mt-2" },
3312
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3313
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Path"),
3314
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route path", placeholder: "e.g. /api/ttm/courses/12345", value: advancedFilterState.routePath, onChange: (e) => {
3315
+ advancedFilterState.courseName = ((e.target.value)
3316
+ .trim());
3317
+ dispatch({
3318
+ type: ActionType.UpdateAdvancedFilterState,
3319
+ advancedFilterState,
3320
+ });
3321
+ } })),
3322
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3323
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Template"),
3324
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route template", value: advancedFilterState.routeTemplate, placeholder: "e.g. /api/ttm/courses/:courseId", onChange: (e) => {
3325
+ advancedFilterState.courseName = ((e.target.value)
3326
+ .trim());
3327
+ dispatch({
3328
+ type: ActionType.UpdateAdvancedFilterState,
3329
+ advancedFilterState,
3330
+ });
3331
+ } })))))));
3332
+ }
3333
+ }
3334
+ // Filters UI
3335
+ const filters = (React__default["default"].createElement(React__default["default"].Fragment, null,
3336
+ filterToggles,
3337
+ filterDrawer && (React__default["default"].createElement(Drawer, null, filterDrawer))));
3338
+ // Actually filter the logs
3339
+ // > Perform filters
3340
+ const logs = [];
3341
+ Object.keys(logMap).forEach((year) => {
3342
+ Object.keys(logMap[year]).forEach((month) => {
3343
+ logMap[year][month].forEach((log) => {
3344
+ /* ----------- Date Filter ---------- */
3345
+ var _a;
3346
+ // Before start date
3347
+ if (
3348
+ // Previous year
3349
+ log.year < dateFilterState.startDate.year
3350
+ // Same year, earlier month
3351
+ || ((log.year === dateFilterState.startDate.year)
3352
+ && (log.month < dateFilterState.startDate.month))
3353
+ // Same year, same month, earlier day
3354
+ || ((log.year === dateFilterState.startDate.year)
3355
+ && (log.month === dateFilterState.startDate.month)
3356
+ && (log.day < dateFilterState.startDate.day))) {
3357
+ return;
3358
+ }
3359
+ // After end date
3360
+ if (
3361
+ // Later year
3362
+ log.year > dateFilterState.endDate.year
3363
+ // Same year, later month
3364
+ || ((log.year === dateFilterState.endDate.year)
3365
+ && (log.month > dateFilterState.endDate.month))
3366
+ // Same year, same month, later day
3367
+ || ((log.year === dateFilterState.endDate.year)
3368
+ && (log.month === dateFilterState.endDate.month)
3369
+ && (log.day > dateFilterState.endDate.day))) {
3370
+ return;
3371
+ }
3372
+ /* --------- Context Filter --------- */
3373
+ // Context doesn't match
3374
+ if (
3375
+ // Whole context is deselected
3376
+ contextFilterState[log.context] === false
3377
+ // None of the subcontexts are selected
3378
+ || (Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3379
+ .every((isSelected) => {
3380
+ return !isSelected;
3381
+ }))) {
3382
+ return;
3383
+ }
3384
+ // Subcontext doesn't match
3385
+ if (
3386
+ // Log has a subcontext
3387
+ log.subcontext
3388
+ // Context has subcontexts
3389
+ && (contextFilterState[log.context]
3390
+ && contextFilterState[log.context] !== false
3391
+ && contextFilterState[log.context] !== true)
3392
+ // Subcontext is not selected
3393
+ && !contextFilterState[log.context][log.subcontext]) {
3394
+ return;
3395
+ }
3396
+ /* -------------- Tags -------------- */
3397
+ // No tags match
3398
+ if (
3399
+ // Log has at least one tag
3400
+ log.tags.length > 0
3401
+ // No tags match
3402
+ && (log.tags.every((tag) => {
3403
+ return !tagFilterState[tag];
3404
+ }))) {
3405
+ return;
3406
+ }
3407
+ /* ------- Actions and Errors ------- */
3408
+ // Log type doesn't match
3409
+ if (
3410
+ // Filter won't allow all types
3411
+ actionErrorFilterState.type !== undefined
3412
+ // Log type doesn't match
3413
+ && actionErrorFilterState.type !== log.type) {
3414
+ return;
3415
+ }
3416
+ // Filter errors
3417
+ if (log.type === LogType$1.Error) {
3418
+ // Message doesn't match
3419
+ if (
3420
+ // Message exists
3421
+ log.errorMessage
3422
+ // Message filter exists
3423
+ && actionErrorFilterState.errorMessage.trim().length > 0
3424
+ // Message doesn't match
3425
+ && log.errorMessage.toLowerCase().includes(actionErrorFilterState.errorMessage.trim().toLowerCase())) {
3426
+ return;
3427
+ }
3428
+ // Code doesn't match
3429
+ if (
3430
+ // Code exists
3431
+ log.errorCode
3432
+ // Code filter exists
3433
+ && actionErrorFilterState.errorCode.trim().length > 0
3434
+ // Code doesn't match
3435
+ && log.errorCode.toUpperCase().includes(actionErrorFilterState.errorCode.trim().toUpperCase())) {
3436
+ return;
3437
+ }
3438
+ }
3439
+ // Filter actions
3440
+ if (log.type === LogType$1.Action) {
3441
+ // Target isn't selected
3442
+ if (
3443
+ // Target exists
3444
+ log.target
3445
+ // Target isn't selected
3446
+ && !actionErrorFilterState.target[log.target]) {
3447
+ return;
3448
+ }
3449
+ // Action
3450
+ if (
3451
+ // Action exists
3452
+ log.action
3453
+ // Action isn't selected
3454
+ && !actionErrorFilterState.action[log.action]) {
3455
+ return;
3456
+ }
3457
+ }
3458
+ /* --------- Advanced Filter -------- */
3459
+ // First name doesn't match
3460
+ if (
3461
+ // First name exists
3462
+ log.userFirstName
3463
+ // First name query doesn't match
3464
+ && !log.userFirstName.toLowerCase().includes(advancedFilterState.userFirstName.toLowerCase().trim())) {
3465
+ return;
3466
+ }
3467
+ // Last name doesn't match
3468
+ if (
3469
+ // Last name exists
3470
+ log.userLastName
3471
+ // Last name query doesn't match
3472
+ && !log.userLastName.toLowerCase().includes(advancedFilterState.userLastName.toLowerCase().trim())) {
3473
+ return;
3474
+ }
3475
+ // Email doesn't match
3476
+ if (
3477
+ // Email exists
3478
+ log.userEmail
3479
+ // Email query doesn't match
3480
+ && !log.userEmail.toLowerCase().includes(advancedFilterState.userEmail.toLowerCase().trim())) {
3481
+ return;
3482
+ }
3483
+ // User id doesn't match
3484
+ if (
3485
+ // User id exists
3486
+ log.userId
3487
+ // User id doesn't match
3488
+ && !String(log.userId).includes(advancedFilterState.userId.trim())) {
3489
+ return;
3490
+ }
3491
+ // Learner not allowed
3492
+ if (
3493
+ // User is a learner
3494
+ log.isLearner
3495
+ // Learners aren't included
3496
+ && !advancedFilterState.includeLearners) {
3497
+ return;
3498
+ }
3499
+ // TTM not allowed
3500
+ if (
3501
+ // User is a ttm
3502
+ log.isTTM
3503
+ // TTMs aren't included
3504
+ && !advancedFilterState.includeTTMs) {
3505
+ return;
3506
+ }
3507
+ // Admin not allowed
3508
+ if (
3509
+ // User is an admin
3510
+ log.isAdmin
3511
+ // Admins aren't included
3512
+ && !advancedFilterState.includeAdmins) {
3513
+ return;
3514
+ }
3515
+ // Course Id doesn't match
3516
+ if (
3517
+ // Course Id exists
3518
+ log.courseId
3519
+ // Course Id doesn't match
3520
+ && !String(log.courseId).includes(advancedFilterState.courseId.trim())) {
3521
+ return;
3522
+ }
3523
+ // Course name doesn't match
3524
+ if (
3525
+ // Course name exists
3526
+ log.courseName
3527
+ // Course name doesn't match
3528
+ && !String(log.courseName).includes(advancedFilterState.courseName.trim())) {
3529
+ return;
3530
+ }
3531
+ // Mobile filter doesn't match
3532
+ if (
3533
+ // Mobile filter exists
3534
+ advancedFilterState.isMobile !== undefined
3535
+ // Device info exists
3536
+ && log.device
3537
+ // Mobile filter doesn't match
3538
+ && (advancedFilterState.isMobile === log.device.isMobile)) {
3539
+ return;
3540
+ }
3541
+ // Log source doesn't match
3542
+ if (
3543
+ // Source filter exists
3544
+ advancedFilterState.source !== undefined
3545
+ // Source info exists
3546
+ && log.source
3547
+ // Source filter doesn't match
3548
+ && (advancedFilterState.source !== log.source)) {
3549
+ return;
3550
+ }
3551
+ // Route path doesn't match (Only for server source)
3552
+ if (
3553
+ // Source is server
3554
+ (log.source === LogSource$1.Server)
3555
+ // Route path is being filtered
3556
+ && (advancedFilterState.routePath.trim().length)
3557
+ // Route path doesn't match
3558
+ && !(log.routePath.includes(advancedFilterState.routePath.trim()))) {
3559
+ return;
3560
+ }
3561
+ // Route template doesn't match (Only for server source)
3562
+ if (
3563
+ // Source is server
3564
+ (log.source === LogSource$1.Server)
3565
+ // Route template is being filtered
3566
+ && (advancedFilterState.routeTemplate.trim().length)
3567
+ // Route template doesn't match
3568
+ && !(log.routeTemplate.includes(advancedFilterState.routeTemplate.trim()))) {
3569
+ return;
3570
+ }
3571
+ /* -------------- Done -------------- */
3572
+ // Made it past all filters. Add to the list
3573
+ logs.push(log);
3574
+ });
3575
+ });
3576
+ });
3577
+ /*----------------------------------------*/
3578
+ /* Data */
3579
+ /*----------------------------------------*/
3580
+ // Create data table
3581
+ const columns = [
3582
+ {
3583
+ title: 'First Name',
3584
+ param: 'userFirstName',
3585
+ type: ParamType$1.String,
3586
+ },
3587
+ {
3588
+ title: 'Last Name',
3589
+ param: 'userLastName',
3590
+ type: ParamType$1.String,
3591
+ },
3592
+ {
3593
+ title: 'Email',
3594
+ param: 'userEmail',
3595
+ type: ParamType$1.String,
3596
+ },
3597
+ {
3598
+ title: 'Canvas Id',
3599
+ param: 'userId',
3600
+ type: ParamType$1.Int,
3601
+ },
3602
+ {
3603
+ title: 'Student',
3604
+ param: 'isLearner',
3605
+ type: ParamType$1.Boolean,
3606
+ },
3607
+ {
3608
+ title: 'Teaching Staff',
3609
+ param: 'isTTM',
3610
+ type: ParamType$1.Boolean,
3611
+ startsHidden: true,
3612
+ },
3613
+ {
3614
+ title: 'Admin',
3615
+ param: 'isAdmin',
3616
+ type: ParamType$1.Boolean,
3617
+ startsHidden: true,
3618
+ },
3619
+ {
3620
+ title: 'Course Canvas Id',
3621
+ param: 'courseId',
3622
+ type: ParamType$1.Int,
3623
+ startsHidden: true,
3624
+ },
3625
+ {
3626
+ title: 'Course Name',
3627
+ param: 'courseName',
3628
+ type: ParamType$1.String,
3629
+ },
3630
+ {
3631
+ title: 'Browser Name',
3632
+ param: 'browser.name',
3633
+ type: ParamType$1.String,
3634
+ startsHidden: true,
3635
+ },
3636
+ {
3637
+ title: 'Browser Version',
3638
+ param: 'browser.version',
3639
+ type: ParamType$1.String,
3640
+ startsHidden: true,
3641
+ },
3642
+ {
3643
+ title: 'OS',
3644
+ param: 'device.os',
3645
+ type: ParamType$1.String,
3646
+ startsHidden: true,
3647
+ },
3648
+ {
3649
+ title: 'Mobile',
3650
+ param: 'device.isMobile',
3651
+ type: ParamType$1.Boolean,
3652
+ startsHidden: true,
3653
+ },
3654
+ {
3655
+ title: 'Year',
3656
+ param: 'year',
3657
+ type: ParamType$1.Int,
3658
+ },
3659
+ {
3660
+ title: 'Month',
3661
+ param: 'month',
3662
+ type: ParamType$1.Int,
3663
+ },
3664
+ {
3665
+ title: 'Day',
3666
+ param: 'day',
3667
+ type: ParamType$1.Int,
3668
+ },
3669
+ {
3670
+ title: 'Hour',
3671
+ param: 'hour',
3672
+ type: ParamType$1.Int,
3673
+ },
3674
+ {
3675
+ title: 'Minute',
3676
+ param: 'minute',
3677
+ type: ParamType$1.Int,
3678
+ startsHidden: true,
3679
+ },
3680
+ {
3681
+ title: 'Timestamp',
3682
+ param: 'timestamp',
3683
+ type: ParamType$1.Int,
3684
+ startsHidden: true,
3685
+ },
3686
+ {
3687
+ title: 'Context',
3688
+ param: 'context',
3689
+ type: ParamType$1.String,
3690
+ },
3691
+ {
3692
+ title: 'Subcontext',
3693
+ param: 'subcontext',
3694
+ type: ParamType$1.String,
3695
+ },
3696
+ {
3697
+ title: 'Tags',
3698
+ param: 'tags',
3699
+ type: ParamType$1.JSON,
3700
+ startsHidden: true,
3701
+ },
3702
+ {
3703
+ title: 'Log Level',
3704
+ param: 'level',
3705
+ type: ParamType$1.String,
3706
+ startsHidden: true,
3707
+ },
3708
+ {
3709
+ title: 'Metadata',
3710
+ param: 'metadata',
3711
+ type: ParamType$1.JSON,
3712
+ startsHidden: true,
3713
+ },
3714
+ {
3715
+ title: 'Source',
3716
+ param: 'source',
3717
+ type: ParamType$1.String,
3718
+ },
3719
+ {
3720
+ title: 'Server Route Path',
3721
+ param: 'routePath',
3722
+ type: ParamType$1.String,
3723
+ startsHidden: true,
3724
+ },
3725
+ {
3726
+ title: 'Server Route Template',
3727
+ param: 'routeTemplate',
3728
+ type: ParamType$1.String,
3729
+ startsHidden: true,
3730
+ },
3731
+ {
3732
+ title: 'Type',
3733
+ param: 'type',
3734
+ type: ParamType$1.String,
3735
+ },
3736
+ {
3737
+ title: 'Error Message',
3738
+ param: 'errorMessage',
3739
+ type: ParamType$1.String,
3740
+ startsHidden: true,
3741
+ },
3742
+ {
3743
+ title: 'Error Code',
3744
+ param: 'errorCode',
3745
+ type: ParamType$1.String,
3746
+ startsHidden: true,
3747
+ },
3748
+ {
3749
+ title: 'Error Stack',
3750
+ param: 'errorStack',
3751
+ type: ParamType$1.String,
3752
+ startsHidden: true,
3753
+ },
3754
+ {
3755
+ title: 'Action Target',
3756
+ param: 'target',
3757
+ type: ParamType$1.String,
3758
+ startsHidden: true,
3759
+ },
3760
+ {
3761
+ title: 'Action Type',
3762
+ param: 'action',
3763
+ type: ParamType$1.String,
3764
+ startsHidden: true,
3765
+ },
3766
+ ];
3767
+ // Create intelliTable
3768
+ const dataTable = (logs.length === 0
3769
+ ? (React__default["default"].createElement(React__default["default"].Fragment, null,
3770
+ React__default["default"].createElement("h3", { className: "m-0" }, "Matching Logs:"),
3771
+ React__default["default"].createElement("div", { className: "alert alert-warning text-center" },
3772
+ React__default["default"].createElement("h4", { className: "m-1" }, "No Logs to Show"),
3773
+ React__default["default"].createElement("div", null, "Either your filters are too strict or no matching logs have been created yet."))))
3774
+ : (React__default["default"].createElement(IntelliTable, { title: "Matching Logs:", id: "logs", data: logs, columns: columns })));
3775
+ // Main body
3776
+ body = (React__default["default"].createElement(React__default["default"].Fragment, null,
3777
+ filters,
3778
+ React__default["default"].createElement("div", { className: "mt-2" }, dataTable)));
3779
+ }
3780
+ /* ---------- Wrap in Modal --------- */
3781
+ return (React__default["default"].createElement("div", { className: "LogReviewer-outer-container" },
3782
+ React__default["default"].createElement("style", null, style),
3783
+ React__default["default"].createElement("div", { className: "LogReviewer-inner-container" },
3784
+ React__default["default"].createElement("div", { className: "LogReviewer-header" },
3785
+ React__default["default"].createElement("div", { className: "LogReviewer-header-title" },
3786
+ React__default["default"].createElement("h3", { className: "text-center m-0" }, "Log Review Dashboard")),
3787
+ React__default["default"].createElement("div", { style: { width: 0 } },
3788
+ React__default["default"].createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-dark btn-lg pe-0", "aria-label": "close log reviewer panel", onClick: onClose },
3789
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes })))),
3790
+ React__default["default"].createElement("div", { className: "LogReviewer-contents" }, body))));
3791
+ };
3792
+
3793
+ /**
3794
+ * One minute in ms
3795
+ * @author Gabe Abrams
3796
+ */
3797
+ const MINUTE_IN_MS = 60000;
3798
+
3799
+ /**
3800
+ * One hour in ms
3801
+ * @author Gabe Abrams
3802
+ */
3803
+ const HOUR_IN_MS = 3600000;
3804
+
3805
+ /**
3806
+ * One day in ms
3807
+ * @author Gabe Abrams
3808
+ */
3809
+ const DAY_IN_MS = 86400000;
3810
+
3811
+ /**
3812
+ * Shorten text so it fits into a certain number of chars
3813
+ * @author Gabe Abrams
3814
+ * @param text the text to abbreviate
3815
+ * @param maxChars the maximum number of chars to include
3816
+ * @returns abbreviated text with length no greater than maxChars
3817
+ * (including ellipses if applicable)
3818
+ */
3819
+ const abbreviate = (text, maxChars) => {
3820
+ // Check if already short enough
3821
+ if (text.trim().length < maxChars) {
3822
+ return text.trim();
3823
+ }
3824
+ // Abbreviate
3825
+ const shortenedText = (text
3826
+ .trim()
3827
+ .substring(0, maxChars - 3)
3828
+ .trim());
3829
+ return `${shortenedText}...`;
3830
+ };
3831
+
3832
+ /**
3833
+ * Sum the numbers in an array
3834
+ * @author Gabe Abrams
3835
+ * @param nums the numbers to sum
3836
+ * @returns the sum of the numbers
3837
+ */
3838
+ const sum = (nums) => {
3839
+ return nums.reduce((a, b) => {
3840
+ return (a + b);
3841
+ }, 0);
3842
+ };
3843
+
3844
+ /**
3845
+ * Get the average of a set of numbers
3846
+ * @author Gabe Abrams
3847
+ * @param nums the numbers to average
3848
+ * @returns average value or 0 if no numbers
3849
+ */
3850
+ const avg = (nums) => {
3851
+ // Handle empty array case
3852
+ if (nums.length === 0) {
3853
+ return 0;
3854
+ }
3855
+ // Get the total value
3856
+ const total = sum(nums);
3857
+ // Get average
3858
+ return (total / nums.length);
3859
+ };
3860
+
3861
+ /**
3862
+ * Round a number (ceiling) to a certain number of decimals
3863
+ * @author Gabe Abrams
3864
+ * @param num the number to round
3865
+ * @param numDecimals the number of decimals to round to
3866
+ * @returns rounded number
3867
+ */
3868
+ const ceilToNumDecimals = (num, numDecimals) => {
3869
+ const rounder = 10 ** numDecimals;
3870
+ return (Math.ceil(num * rounder) / rounder);
3871
+ };
3872
+
3873
+ /**
3874
+ * Round a number (floor) to a certain number of decimals
3875
+ * @author Gabe Abrams
3876
+ * @param num the number to round
3877
+ * @param numDecimals the number of decimals to round to
3878
+ * @returns rounded number
3879
+ */
3880
+ const floorToNumDecimals = (num, numDecimals) => {
3881
+ const rounder = 10 ** numDecimals;
3882
+ return (Math.floor(num * rounder) / rounder);
3883
+ };
3884
+
3885
+ /**
3886
+ * Pad a number's decimal with zeros on the right
3887
+ * (e.g. 5.2 becomes 5.20 with 2 digit padding)
3888
+ * @author Gabe Abrams
3889
+ * @param num the number to pad
3890
+ * @param numDigits the minimum number of digits after the decimal
2175
3891
  * @returns padded number
2176
3892
  */
2177
- const padZerosLeft = (num, numDigits) => {
3893
+ const padDecimalZeros = (num, numDigits) => {
3894
+ // Skip if nothing to do
3895
+ if (numDigits < 1) {
3896
+ return String(num);
3897
+ }
2178
3898
  // Convert to string
2179
3899
  let out = String(num);
3900
+ // Add a decimal point if there isn't one
3901
+ if (!out.includes('.')) {
3902
+ out += '.';
3903
+ }
2180
3904
  // Add zeros
2181
- while (out.split('.')[0].length < numDigits) {
2182
- out = `0${out}`;
3905
+ while (out.split('.')[1].length < numDigits) {
3906
+ out = `${out}0`;
2183
3907
  }
2184
3908
  // Return
2185
3909
  return out;
2186
3910
  };
2187
3911
 
2188
3912
  /**
2189
- * Round a number to a certain number of decimals
3913
+ * Pad a number with zeros on the left (e.g. 5 becomes 05 with 2 digit padding)
2190
3914
  * @author Gabe Abrams
2191
- * @param num the number to round
2192
- * @param numDecimals the number of decimals to round to
2193
- * @returns rounded number
3915
+ * @param num the number to pad
3916
+ * @param numDigits the minimum number of digits before the decimal
3917
+ * @returns padded number
2194
3918
  */
2195
- const roundToNumDecimals = (num, numDecimals) => {
2196
- const rounder = 10 ** numDecimals;
2197
- return (Math.round(num * rounder) / rounder);
3919
+ const padZerosLeft = (num, numDigits) => {
3920
+ // Convert to string
3921
+ let out = String(num);
3922
+ // Add zeros
3923
+ while (out.split('.')[0].length < numDigits) {
3924
+ out = `0${out}`;
3925
+ }
3926
+ // Return
3927
+ return out;
2198
3928
  };
2199
3929
 
2200
3930
  /**
2201
- * Server-side API param types
3931
+ * Route for checking the status of the current user's
3932
+ * access to log review
2202
3933
  * @author Gabe Abrams
2203
3934
  */
2204
- var ParamType;
2205
- (function (ParamType) {
2206
- ParamType["Boolean"] = "boolean";
2207
- ParamType["BooleanOptional"] = "boolean-optional";
2208
- ParamType["Float"] = "float";
2209
- ParamType["FloatOptional"] = "float-optional";
2210
- ParamType["Int"] = "int";
2211
- ParamType["IntOptional"] = "int-optional";
2212
- ParamType["JSON"] = "json";
2213
- ParamType["JSONOptional"] = "json-optional";
2214
- ParamType["String"] = "string";
2215
- ParamType["StringOptional"] = "string-optional";
2216
- })(ParamType || (ParamType = {}));
2217
- var ParamType$1 = ParamType;
3935
+ const LOG_REVIEW_STATUS_ROUTE = `${ROUTE_PATH_PREFIX}/logs/access_allowed`;
2218
3936
 
2219
- // Import custom error
2220
3937
  // Stored copy of caccl functions
2221
3938
  let _cacclGetLaunchInfo;
2222
3939
  // Stored copy of dce-mango log collection
@@ -2257,10 +3974,20 @@ const internalGetLogCollection = () => {
2257
3974
  * @param opts.getLaunchInfo CACCL LTI's get launch info function
2258
3975
  * @param [opts.logCollection] mongo collection from dce-mango to use for
2259
3976
  * storing logs. If none is included, logs are written to the console
3977
+ * @param [opts.logReviewAdmins=all] info on which admins can review
3978
+ * logs from the client. If not included, all Canvas admins are allowed to
3979
+ * review logs. If null, no Canvas admins are allowed to review logs.
3980
+ * If an array of Canvas userIds (numbers), only Canvas admins with those
3981
+ * userIds are allowed to review logs. If a dce-mango collection, only
3982
+ * Canvas admins with entries in that collection ({ userId, ...}) are allowed
3983
+ * to review logs
2260
3984
  */
2261
3985
  const initServer = (opts) => {
2262
3986
  _cacclGetLaunchInfo = opts.getLaunchInfo;
2263
3987
  _logCollection = opts.logCollection;
3988
+ /*----------------------------------------*/
3989
+ /* Logging */
3990
+ /*----------------------------------------*/
2264
3991
  /**
2265
3992
  * Log an event
2266
3993
  * @author Gabe Abrams
@@ -2327,6 +4054,84 @@ const initServer = (opts) => {
2327
4054
  return log;
2328
4055
  },
2329
4056
  }));
4057
+ /*----------------------------------------*/
4058
+ /* Log Reviewer */
4059
+ /*----------------------------------------*/
4060
+ /**
4061
+ * Check if a given user is allowed to review logs
4062
+ * @author Gabe Abrams
4063
+ * @param userId the id of the user
4064
+ * @param isAdmin if true, the user is an admin
4065
+ * @returns true if the user can review logs
4066
+ */
4067
+ const canReviewLogs = (userId, isAdmin) => __awaiter(void 0, void 0, void 0, function* () {
4068
+ // Immediately deny access if user is not an admin
4069
+ if (!isAdmin) {
4070
+ return false;
4071
+ }
4072
+ // If all admins are allowed, we're done
4073
+ if (!opts.logReviewAdmins) {
4074
+ return true;
4075
+ }
4076
+ // Do a dynamic check
4077
+ try {
4078
+ // Array of userIds
4079
+ if (Array.isArray(opts.logReviewAdmins)) {
4080
+ return opts.logReviewAdmins.some((allowedId) => {
4081
+ return (userId === allowedId);
4082
+ });
4083
+ }
4084
+ // Must be a collection
4085
+ const matches = yield opts.logReviewAdmins.find({ userId });
4086
+ // Make sure at least one entry matches
4087
+ return matches.length > 0;
4088
+ }
4089
+ catch (err) {
4090
+ // If an error occurred, simply return false
4091
+ return false;
4092
+ }
4093
+ });
4094
+ /**
4095
+ * Check if the current user has access to logs
4096
+ * @author Gabe Abrams
4097
+ * @returns {boolean} true if user has access
4098
+ */
4099
+ opts.app.get(LOG_REVIEW_STATUS_ROUTE, genRouteHandler({
4100
+ handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
4101
+ const { userId, isAdmin } = params;
4102
+ const canReview = yield canReviewLogs(userId, isAdmin);
4103
+ return canReview;
4104
+ }),
4105
+ }));
4106
+ /**
4107
+ * Get all logs for a certain month
4108
+ * @author Gabe Abrams
4109
+ * @param {number} year the year to query (e.g. 2022)
4110
+ * @param {number} month the month to query (e.g. 1 = January)
4111
+ * @returns {Log[]} list of logs from the given month
4112
+ */
4113
+ opts.app.get(`${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/:year/months/:month`, genRouteHandler({
4114
+ paramTypes: {
4115
+ year: ParamType$1.Int,
4116
+ month: ParamType$1.Int,
4117
+ },
4118
+ handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
4119
+ // Get user info
4120
+ const { year, month, userId, isAdmin, } = params;
4121
+ // Validate user
4122
+ const canReview = yield canReviewLogs(userId, isAdmin);
4123
+ if (!canReview) {
4124
+ throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
4125
+ }
4126
+ // Query for logs
4127
+ const logs = yield _logCollection.find({
4128
+ year,
4129
+ month,
4130
+ });
4131
+ // Return logs
4132
+ return logs;
4133
+ }),
4134
+ }));
2330
4135
  };
2331
4136
 
2332
4137
  // Import shared types
@@ -2626,85 +4431,6 @@ const parseUserAgent = (userAgent) => {
2626
4431
  };
2627
4432
  };
2628
4433
 
2629
- /**
2630
- * Type of a log event
2631
- * @author Gabe Abrams
2632
- */
2633
- var LogType;
2634
- (function (LogType) {
2635
- // User action
2636
- LogType["Action"] = "action";
2637
- // Error
2638
- LogType["Error"] = "error";
2639
- })(LogType || (LogType = {}));
2640
- var LogType$1 = LogType;
2641
-
2642
- /**
2643
- * Source of a log event
2644
- * @author Gabe Abrams
2645
- */
2646
- var LogSource;
2647
- (function (LogSource) {
2648
- // Client
2649
- LogSource["Client"] = "client";
2650
- // Server
2651
- LogSource["Server"] = "server";
2652
- })(LogSource || (LogSource = {}));
2653
- var LogSource$1 = LogSource;
2654
-
2655
- /**
2656
- * Types of actions
2657
- * @author Gabe Abrams
2658
- */
2659
- var LogAction;
2660
- (function (LogAction) {
2661
- // Target was opened by the user (it was not on screen, but now it is)
2662
- LogAction["Open"] = "open";
2663
- // Target was closed by the user (it was on screen, but now it is not)
2664
- LogAction["Close"] = "close";
2665
- // Target was cancelled by the user (it was on closed without saving)
2666
- LogAction["Cancel"] = "cancel";
2667
- // Target was expanded by the user (it always remains on screen, but size was changed)
2668
- LogAction["Expand"] = "expand";
2669
- // Target was collapsed by the user (it always remains on screen, but size was changed)
2670
- LogAction["Collapse"] = "collapse";
2671
- // Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
2672
- LogAction["View"] = "view";
2673
- // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2674
- LogAction["Interrupt"] = "interrupt";
2675
- // Target was created by the user (it did not exist before)
2676
- LogAction["Create"] = "create";
2677
- // Target was edited by the user (it existed and was changed)
2678
- LogAction["Edit"] = "edit";
2679
- // Target was deleted by the user (it existed and now it doesn't)
2680
- LogAction["Delete"] = "delete";
2681
- // Target was added by the user (it already existed and was added to another place)
2682
- LogAction["Add"] = "add";
2683
- // Target was removed by the user (it was removed from something but still exists)
2684
- LogAction["Remove"] = "remove";
2685
- // Target was activated by the user (click, check, tap, keypress, etc.)
2686
- LogAction["Activate"] = "activate";
2687
- // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2688
- LogAction["Deactivate"] = "deactivate";
2689
- // User showed interest in a target (hover, peek, etc.)
2690
- LogAction["Peek"] = "peek";
2691
- // Unknown action
2692
- LogAction["Unknown"] = "unknown";
2693
- })(LogAction || (LogAction = {}));
2694
- var LogAction$1 = LogAction;
2695
-
2696
- /**
2697
- * Allowed log levels
2698
- * @author Gabe Abrams
2699
- */
2700
- var LogLevel;
2701
- (function (LogLevel) {
2702
- LogLevel["Warn"] = "Warn";
2703
- LogLevel["Info"] = "Info";
2704
- LogLevel["Debug"] = "Debug";
2705
- })(LogLevel || (LogLevel = {}));
2706
- var LogLevel$1 = LogLevel;
2707
-
2708
4434
  /**
2709
4435
  * Generate an express API route handler
2710
4436
  * @author Gabe Abrams
@@ -3290,21 +5016,7 @@ const startMinWait = (minWaitMs) => {
3290
5016
  });
3291
5017
  };
3292
5018
 
3293
- // Map of month to three letter description
3294
- const monthMap = {
3295
- 1: 'Jan',
3296
- 2: 'Feb',
3297
- 3: 'Mar',
3298
- 4: 'Apr',
3299
- 5: 'May',
3300
- 6: 'Jun',
3301
- 7: 'Jul',
3302
- 8: 'Aug',
3303
- 9: 'Sep',
3304
- 10: 'Oct',
3305
- 11: 'Nov',
3306
- 12: 'Dec',
3307
- };
5019
+ // Import shared helpers
3308
5020
  /**
3309
5021
  * Get a human-readable description of a date (all in ET)
3310
5022
  * @author Gabe Abrams
@@ -3315,8 +5027,10 @@ const getHumanReadableDate = (dateOrTimestamp) => {
3315
5027
  // Get the date info
3316
5028
  const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
3317
5029
  const currYear = getTimeInfoInET().year;
5030
+ // Get the short month description
5031
+ const monthName = getMonthName(month).short;
3318
5032
  // Create start of description
3319
- let description = `${monthMap[month]} ${day}${getOrdinal(day)}`;
5033
+ let description = `${monthName} ${day}${getOrdinal(day)}`;
3320
5034
  // Add on year if it's different
3321
5035
  if (year !== currYear) {
3322
5036
  description += ` ${year}`;
@@ -3456,6 +5170,31 @@ const initLogCollection = (Collection) => {
3456
5170
  });
3457
5171
  };
3458
5172
 
5173
+ // Cache user's ability
5174
+ let canReview = undefined;
5175
+ /**
5176
+ * Check if the current user can review logs
5177
+ * @author Gabe Abrams
5178
+ * @returns true if current user can review logs
5179
+ */
5180
+ const canReviewLogs = () => __awaiter(void 0, void 0, void 0, function* () {
5181
+ // If cached, use that value
5182
+ if (canReview !== undefined) {
5183
+ return canReview;
5184
+ }
5185
+ // Ask on server
5186
+ try {
5187
+ canReview = !!(yield visitServerEndpoint({
5188
+ path: LOG_REVIEW_STATUS_ROUTE,
5189
+ method: 'GET',
5190
+ }));
5191
+ }
5192
+ catch (err) {
5193
+ canReview = false;
5194
+ }
5195
+ return canReview;
5196
+ });
5197
+
3459
5198
  /**
3460
5199
  * Days of the week
3461
5200
  * @author Gabe Abrams
@@ -3474,6 +5213,7 @@ var DayOfWeek$1 = DayOfWeek;
3474
5213
 
3475
5214
  exports.AppWrapper = AppWrapper;
3476
5215
  exports.ButtonInputGroup = ButtonInputGroup;
5216
+ exports.CSVDownloadButton = CSVDownloadButton;
3477
5217
  exports.CheckboxButton = CheckboxButton;
3478
5218
  exports.CopiableBox = CopiableBox;
3479
5219
  exports.DAY_IN_MS = DAY_IN_MS;
@@ -3482,10 +5222,12 @@ exports.Drawer = Drawer;
3482
5222
  exports.ErrorBox = ErrorBox;
3483
5223
  exports.ErrorWithCode = ErrorWithCode;
3484
5224
  exports.HOUR_IN_MS = HOUR_IN_MS;
5225
+ exports.IntelliTable = IntelliTable;
3485
5226
  exports.ItemPicker = ItemPicker;
3486
5227
  exports.LoadingSpinner = LoadingSpinner;
3487
5228
  exports.LogAction = LogAction$1;
3488
5229
  exports.LogBuiltInMetadata = LogBuiltInMetadata;
5230
+ exports.LogReviewer = LogReviewer;
3489
5231
  exports.LogSource = LogSource$1;
3490
5232
  exports.LogType = LogType$1;
3491
5233
  exports.MINUTE_IN_MS = MINUTE_IN_MS;
@@ -3505,12 +5247,15 @@ exports.Variant = Variant$1;
3505
5247
  exports.abbreviate = abbreviate;
3506
5248
  exports.alert = alert$1;
3507
5249
  exports.avg = avg;
5250
+ exports.canReviewLogs = canReviewLogs;
3508
5251
  exports.ceilToNumDecimals = ceilToNumDecimals;
3509
5252
  exports.confirm = confirm;
3510
5253
  exports.floorToNumDecimals = floorToNumDecimals;
3511
5254
  exports.forceNumIntoBounds = forceNumIntoBounds;
5255
+ exports.genCSV = genCSV;
3512
5256
  exports.genRouteHandler = genRouteHandler;
3513
5257
  exports.getHumanReadableDate = getHumanReadableDate;
5258
+ exports.getMonthName = getMonthName;
3514
5259
  exports.getOrdinal = getOrdinal;
3515
5260
  exports.getPartOfDay = getPartOfDay;
3516
5261
  exports.getTimeInfoInET = getTimeInfoInET;