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

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