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