dce-reactkit 3.2.2-beta.5 → 3.2.2-beta.51

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 (46) hide show
  1. package/dist/cjs/index.js +741 -446
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/ButtonInputGroup.d.ts +1 -0
  4. package/dist/cjs/types/components/Drawer.d.ts +1 -0
  5. package/dist/cjs/types/components/IntelliTable.d.ts +1 -0
  6. package/dist/cjs/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  7. package/dist/cjs/types/components/ItemPicker/index.d.ts +1 -0
  8. package/dist/cjs/types/types/LogAction.d.ts +16 -16
  9. package/dist/cjs/types/types/LogBuiltInMetadata.d.ts +1 -1
  10. package/dist/cjs/types/types/LogMetadataContextMap.d.ts +11 -0
  11. package/dist/cjs/types/types/LogMetadataTagMap.d.ts +8 -0
  12. package/dist/cjs/types/types/LogMetadataTargetMap.d.ts +8 -0
  13. package/dist/cjs/types/types/LogMetadataType.d.ts +6 -12
  14. package/dist/esm/index.js +742 -447
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/types/components/ButtonInputGroup.d.ts +1 -0
  17. package/dist/esm/types/components/Drawer.d.ts +1 -0
  18. package/dist/esm/types/components/IntelliTable.d.ts +1 -0
  19. package/dist/esm/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  20. package/dist/esm/types/components/ItemPicker/index.d.ts +1 -0
  21. package/dist/esm/types/types/LogAction.d.ts +16 -16
  22. package/dist/esm/types/types/LogBuiltInMetadata.d.ts +1 -1
  23. package/dist/esm/types/types/LogMetadataContextMap.d.ts +11 -0
  24. package/dist/esm/types/types/LogMetadataTagMap.d.ts +8 -0
  25. package/dist/esm/types/types/LogMetadataTargetMap.d.ts +8 -0
  26. package/dist/esm/types/types/LogMetadataType.d.ts +6 -12
  27. package/dist/index.d.ts +51 -29
  28. package/package.json +1 -1
  29. package/src/components/ButtonInputGroup.tsx +5 -2
  30. package/src/components/CSVDownloadButton.tsx +2 -2
  31. package/src/components/Drawer.tsx +9 -1
  32. package/src/components/IntelliTable.tsx +181 -26
  33. package/src/components/ItemPicker/NestableItemList.tsx +32 -14
  34. package/src/components/ItemPicker/index.tsx +4 -0
  35. package/src/components/LogReviewer.tsx +635 -406
  36. package/src/components/SimpleDateChooser.tsx +28 -23
  37. package/src/helpers/genCSV.ts +22 -4
  38. package/src/helpers/genRouteHandler.ts +2 -2
  39. package/src/helpers/logClientEvent.tsx +1 -1
  40. package/src/server/initServer.ts +10 -3
  41. package/src/types/LogAction.ts +16 -16
  42. package/src/types/LogBuiltInMetadata.ts +5 -5
  43. package/src/types/LogMetadataContextMap.ts +15 -0
  44. package/src/types/LogMetadataTagMap.ts +9 -0
  45. package/src/types/LogMetadataTargetMap.ts +9 -0
  46. package/src/types/LogMetadataType.ts +8 -15
package/dist/cjs/index.js CHANGED
@@ -519,14 +519,14 @@ const LOG_ROUTE_PATH = `${ROUTE_PATH_PREFIX}/log`;
519
519
  const LogBuiltInMetadata = {
520
520
  // Contexts
521
521
  Context: {
522
- Uncategorized: 'n/a',
523
- ServerRenderedErrorPage: '_server-rendered-error-page',
524
- ServerEndpointError: '_server-endpoint-error',
525
- ClientFatalError: '_client-fatal-error',
522
+ Uncategorized: 'Uncategorized',
523
+ ServerRenderedErrorPage: 'ServerRenderedErrorPage',
524
+ ServerEndpointError: 'ServerEndpointError',
525
+ ClientFatalError: 'ClientFatalError',
526
526
  },
527
527
  // Targets
528
528
  Target: {
529
- NoSpecificTarget: 'n/a',
529
+ NoTarget: 'NoTarget',
530
530
  },
531
531
  };
532
532
 
@@ -682,7 +682,7 @@ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* ()
682
682
  ? opts.error.stack
683
683
  : undefined),
684
684
  target: (opts.action
685
- ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoSpecificTarget)
685
+ ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoTarget)
686
686
  : undefined),
687
687
  action: (opts.action
688
688
  ? opts.action
@@ -1188,21 +1188,21 @@ const ButtonInputGroup = (props) => {
1188
1188
  /*------------------------------------------------------------------------*/
1189
1189
  /* -------------- Props ------------- */
1190
1190
  // Destructure all props
1191
- const { label, minLabelWidth, children, } = props;
1191
+ const { label, minLabelWidth, children, className, } = props;
1192
1192
  /*------------------------------------------------------------------------*/
1193
1193
  /* Render */
1194
1194
  /*------------------------------------------------------------------------*/
1195
1195
  /*----------------------------------------*/
1196
1196
  /* Main UI */
1197
1197
  /*----------------------------------------*/
1198
- return (React__default["default"].createElement("div", { className: "input-group" },
1198
+ return (React__default["default"].createElement("div", { className: `input-group ${className !== null && className !== void 0 ? className : ''}` },
1199
1199
  React__default["default"].createElement("div", { className: "input-group-prepend d-flex w-100" },
1200
1200
  React__default["default"].createElement("span", { className: "input-group-text", style: {
1201
1201
  minWidth: (minLabelWidth !== null && minLabelWidth !== void 0 ? minLabelWidth : undefined),
1202
1202
  borderTopRightRadius: 0,
1203
1203
  borderBottomRightRadius: 0,
1204
1204
  } }, label),
1205
- React__default["default"].createElement("span", { className: "input-group-text flex-grow-1 rounded-right", style: {
1205
+ React__default["default"].createElement("span", { className: "input-group-text flex-grow-1 rounded-right d-flex flex-wrap", style: {
1206
1206
  backgroundColor: 'white',
1207
1207
  borderTopLeftRadius: 0,
1208
1208
  borderBottomLeftRadius: 0,
@@ -1307,18 +1307,6 @@ const getTimeInfoInET = (dateOrTimestamp) => {
1307
1307
  };
1308
1308
  };
1309
1309
 
1310
- /**
1311
- * Force a number to stay within specific bounds
1312
- * @author Gabe Abrams
1313
- * @param num the number to move into the bounds
1314
- * @param min the minimum number in the bound
1315
- * @param max the maximum number in the bound
1316
- * @returns bounded number
1317
- */
1318
- const forceNumIntoBounds = (num, min, max) => {
1319
- return Math.max(min, Math.min(max, num));
1320
- };
1321
-
1322
1310
  /**
1323
1311
  * A very simple, lightweight date chooser
1324
1312
  * @author Gabe Abrams
@@ -1333,7 +1321,7 @@ const SimpleDateChooser = (props) => {
1333
1321
  var _a;
1334
1322
  /* -------------- Props ------------- */
1335
1323
  const { ariaLabel, name, month, day, year, onChange, chooseFromPast, } = props;
1336
- let numMonthsToShow = forceNumIntoBounds(((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6), 1, 12);
1324
+ const numMonthsToShow = ((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6);
1337
1325
  /*------------------------------------------------------------------------*/
1338
1326
  /* Render */
1339
1327
  /*------------------------------------------------------------------------*/
@@ -1347,7 +1335,7 @@ const SimpleDateChooser = (props) => {
1347
1335
  let startMonth = today.month;
1348
1336
  if (chooseFromPast) {
1349
1337
  startMonth -= Math.max(0, numMonthsToShow - 1);
1350
- if (startMonth <= 0) {
1338
+ while (startMonth <= 0) {
1351
1339
  startMonth += 12;
1352
1340
  startYear -= 1;
1353
1341
  }
@@ -1355,22 +1343,35 @@ const SimpleDateChooser = (props) => {
1355
1343
  for (let i = 0; i < numMonthsToShow; i++) {
1356
1344
  // Get month and year info
1357
1345
  const unmoddedMonth = (startMonth + i);
1358
- const month = (unmoddedMonth > 12
1359
- ? unmoddedMonth - 12
1360
- : unmoddedMonth);
1346
+ let month = unmoddedMonth;
1347
+ while (month > 12) {
1348
+ month -= 12;
1349
+ }
1361
1350
  const monthName = getMonthName(month).full;
1362
- const year = (unmoddedMonth > 12
1363
- ? startYear + 1
1364
- : startYear);
1351
+ // Year is start year +1 for each 12 months
1352
+ const year = (startYear + (Math.floor(unmoddedMonth / 12)));
1365
1353
  // Figure out which days are allowed
1366
1354
  const days = [];
1367
1355
  const numDaysInMonth = (new Date(year, month, 0)).getDate();
1368
- const firstDay = (month === today.month
1369
- ? today.day // Current month: start at current date
1370
- : 1 // Future month: start at beginning of month
1371
- );
1372
- for (let day = firstDay; day <= numDaysInMonth; day++) {
1373
- days.push(day);
1356
+ if (chooseFromPast) {
1357
+ // Past selection
1358
+ const numDaysToAdd = ((month === today.month)
1359
+ ? today.day // Current month, only add up to today
1360
+ : numDaysInMonth // Past month, add all days
1361
+ );
1362
+ for (let day = 1; day <= numDaysToAdd; day++) {
1363
+ days.push(day);
1364
+ }
1365
+ }
1366
+ else {
1367
+ // Future selection: add all remaining days of the month
1368
+ const firstDay = (month === today.month
1369
+ ? today.day // Current month: start at current date
1370
+ : 1 // Future month: start at beginning of month
1371
+ );
1372
+ for (let day = firstDay; day <= numDaysInMonth; day++) {
1373
+ days.push(day);
1374
+ }
1374
1375
  }
1375
1376
  choices.push({
1376
1377
  choiceName: `${monthName} ${year}`,
@@ -1441,14 +1442,16 @@ const Drawer = (props) => {
1441
1442
  /*------------------------------------------------------------------------*/
1442
1443
  /* -------------- Props ------------- */
1443
1444
  // Destructure all props
1444
- const { children, } = props;
1445
+ const { customBackgroundColor, children, } = props;
1445
1446
  /*------------------------------------------------------------------------*/
1446
1447
  /* Render */
1447
1448
  /*------------------------------------------------------------------------*/
1448
1449
  /*----------------------------------------*/
1449
1450
  /* Main UI */
1450
1451
  /*----------------------------------------*/
1451
- return (React__default["default"].createElement("div", { className: "Drawer-container" },
1452
+ return (React__default["default"].createElement("div", { className: "Drawer-container", style: {
1453
+ backgroundColor: (customBackgroundColor !== null && customBackgroundColor !== void 0 ? customBackgroundColor : undefined),
1454
+ } },
1452
1455
  React__default["default"].createElement("style", null, style$4),
1453
1456
  children));
1454
1457
  };
@@ -1911,24 +1914,25 @@ const CopiableBox = (props) => {
1911
1914
  /**
1912
1915
  * Reusable nested item picker
1913
1916
  * @author Yuen Ler Chow
1917
+ * @author Gabe Abrams
1914
1918
  */
1915
1919
  /* ------------- Actions ------------ */
1916
1920
  // Types of actions
1917
1921
  var ActionType$2;
1918
1922
  (function (ActionType) {
1919
- // Toggle whether the children are being shown
1920
- ActionType["ToggleItems"] = "toggle-items";
1923
+ // Toggle whether a child are being shown
1924
+ ActionType["ToggleChild"] = "toggle-child";
1921
1925
  })(ActionType$2 || (ActionType$2 = {}));
1922
1926
  /**
1923
1927
  * Reducer that executes actions
1924
- * @author Yuen Ler Chow
1928
+ * @author Gabe Abrams
1925
1929
  * @param state current state
1926
1930
  * @param action action to execute
1927
1931
  */
1928
1932
  const reducer$2 = (state, action) => {
1929
1933
  switch (action.type) {
1930
- case ActionType$2.ToggleItems: {
1931
- return { isShowingItems: !state.isShowingItems };
1934
+ case ActionType$2.ToggleChild: {
1935
+ return Object.assign(Object.assign({}, state), { childExpanded: Object.assign(Object.assign({}, state.childExpanded), { [String(action.id)]: !state.childExpanded[String(action.id)] }) });
1932
1936
  }
1933
1937
  default: {
1934
1938
  return state;
@@ -1946,14 +1950,19 @@ const NestableItemList = (props) => {
1946
1950
  // Destructure all props
1947
1951
  const { items, onChanged, } = props;
1948
1952
  /* -------------- State ------------- */
1953
+ // Create initial map of child expanded booleans
1954
+ const initChildExpanded = {};
1955
+ items.forEach((item) => {
1956
+ initChildExpanded[String(item.id)] = false;
1957
+ });
1949
1958
  // Initial state
1950
1959
  const initialState = {
1951
- isShowingItems: false,
1960
+ childExpanded: initChildExpanded,
1952
1961
  };
1953
1962
  // Initialize state
1954
1963
  const [state, dispatch] = React.useReducer(reducer$2, initialState);
1955
1964
  // Destructure common state
1956
- const { isShowingItems, } = state;
1965
+ const { childExpanded, } = state;
1957
1966
  /*------------------------------------------------------------------------*/
1958
1967
  /* Component Functions */
1959
1968
  /*------------------------------------------------------------------------*/
@@ -2041,14 +2050,15 @@ const NestableItemList = (props) => {
2041
2050
  backgroundColor: 'transparent',
2042
2051
  }, type: "button", onClick: () => {
2043
2052
  dispatch({
2044
- type: ActionType$2.ToggleItems,
2053
+ type: ActionType$2.ToggleChild,
2054
+ id: item.id,
2045
2055
  });
2046
- }, "aria-label": `${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}` },
2047
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: isShowingItems ? freeSolidSvgIcons.faChevronDown : freeSolidSvgIcons.faChevronRight })))),
2056
+ }, "aria-label": `${childExpanded[item.id] ? 'Hide' : 'Show'} items in ${item.name}` },
2057
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: childExpanded[item.id] ? freeSolidSvgIcons.faChevronDown : freeSolidSvgIcons.faChevronRight })))),
2048
2058
  React__default["default"].createElement(CheckboxButton, { className: `NestableItemList-CheckboxButton-${item.id}`, text: item.name, checked: item.isGroup ? allChecked(item.children) : item.checked, dashed: item.isGroup ? !noneChecked(item.children) : false, onChanged: (checked) => {
2049
2059
  onChanged(changeChecked(item.id, checked, items));
2050
2060
  }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light }),
2051
- item.isGroup && isShowingItems && (React__default["default"].createElement("div", { className: "NestableItemList-children-container", style: {
2061
+ (item.isGroup && childExpanded[item.id]) && (React__default["default"].createElement("div", { className: "NestableItemList-children-container", style: {
2052
2062
  paddingLeft: '2.2rem',
2053
2063
  } },
2054
2064
  React__default["default"].createElement(NestableItemList, { items: item.children, onChanged: (updatedItems) => {
@@ -2070,7 +2080,7 @@ const ItemPicker = (props) => {
2070
2080
  /*------------------------------------------------------------------------*/
2071
2081
  /* -------------- Props ------------- */
2072
2082
  // Destructure all props
2073
- const { title, items, onChanged, } = props;
2083
+ const { title, items, onChanged, noBottomMargin, } = props;
2074
2084
  /*------------------------------------------------------------------------*/
2075
2085
  /* Component Functions */
2076
2086
  /*------------------------------------------------------------------------*/
@@ -2080,11 +2090,34 @@ const ItemPicker = (props) => {
2080
2090
  /*----------------------------------------*/
2081
2091
  /* Main UI */
2082
2092
  /*----------------------------------------*/
2083
- return (React__default["default"].createElement(TabBox, { title: title },
2093
+ return (React__default["default"].createElement(TabBox, { title: title, noBottomMargin: noBottomMargin },
2084
2094
  React__default["default"].createElement("div", { style: { overflowX: 'auto' } },
2085
2095
  React__default["default"].createElement(NestableItemList, { items: items, onChanged: onChanged }))));
2086
2096
  };
2087
2097
 
2098
+ // Import shared helpers
2099
+ /**
2100
+ * Get a human-readable description of a date (all in ET)
2101
+ * @author Gabe Abrams
2102
+ * @param [dateOrTimestamp=today] the date or timestamp for the date to describe
2103
+ * @returns human-readable description of the date
2104
+ */
2105
+ const getHumanReadableDate = (dateOrTimestamp) => {
2106
+ // Get the date info
2107
+ const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
2108
+ const currYear = getTimeInfoInET().year;
2109
+ // Get the short month description
2110
+ const monthName = getMonthName(month).short;
2111
+ // Create start of description
2112
+ let description = `${monthName} ${day}${getOrdinal(day)}`;
2113
+ // Add on year if it's different
2114
+ if (year !== currYear) {
2115
+ description += ` ${year}`;
2116
+ }
2117
+ // Return description
2118
+ return description;
2119
+ };
2120
+
2088
2121
  /**
2089
2122
  * Path of the route for storing client-side logs
2090
2123
  * @author Gabe Abrams
@@ -2124,37 +2157,37 @@ var LogType$1 = LogType;
2124
2157
  var LogAction;
2125
2158
  (function (LogAction) {
2126
2159
  // Target was opened by the user (it was not on screen, but now it is)
2127
- LogAction["Open"] = "open";
2160
+ LogAction["Open"] = "Open";
2128
2161
  // Target was closed by the user (it was on screen, but now it is not)
2129
- LogAction["Close"] = "close";
2162
+ LogAction["Close"] = "Close";
2130
2163
  // Target was cancelled by the user (it was on closed without saving)
2131
- LogAction["Cancel"] = "cancel";
2164
+ LogAction["Cancel"] = "Cancel";
2132
2165
  // Target was expanded by the user (it always remains on screen, but size was changed)
2133
- LogAction["Expand"] = "expand";
2166
+ LogAction["Expand"] = "Expand";
2134
2167
  // Target was collapsed by the user (it always remains on screen, but size was changed)
2135
- LogAction["Collapse"] = "collapse";
2168
+ LogAction["Collapse"] = "Collapse";
2136
2169
  // Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
2137
- LogAction["View"] = "view";
2170
+ LogAction["View"] = "View";
2138
2171
  // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2139
- LogAction["Interrupt"] = "interrupt";
2172
+ LogAction["Interrupt"] = "Interrupt";
2140
2173
  // Target was created by the user (it did not exist before)
2141
- LogAction["Create"] = "create";
2174
+ LogAction["Create"] = "Create";
2142
2175
  // Target was edited by the user (it existed and was changed)
2143
- LogAction["Edit"] = "edit";
2176
+ LogAction["Edit"] = "Edit";
2144
2177
  // Target was deleted by the user (it existed and now it doesn't)
2145
- LogAction["Delete"] = "delete";
2178
+ LogAction["Delete"] = "Delete";
2146
2179
  // Target was added by the user (it already existed and was added to another place)
2147
- LogAction["Add"] = "add";
2180
+ LogAction["Add"] = "Add";
2148
2181
  // Target was removed by the user (it was removed from something but still exists)
2149
- LogAction["Remove"] = "remove";
2182
+ LogAction["Remove"] = "Remove";
2150
2183
  // Target was activated by the user (click, check, tap, keypress, etc.)
2151
- LogAction["Activate"] = "activate";
2184
+ LogAction["Activate"] = "Activate";
2152
2185
  // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2153
- LogAction["Deactivate"] = "deactivate";
2186
+ LogAction["Deactivate"] = "Deactivate";
2154
2187
  // User showed interest in a target (hover, peek, etc.)
2155
- LogAction["Peek"] = "peek";
2188
+ LogAction["Peek"] = "Peek";
2156
2189
  // Unknown action
2157
- LogAction["Unknown"] = "unknown";
2190
+ LogAction["Unknown"] = "Unknown";
2158
2191
  })(LogAction || (LogAction = {}));
2159
2192
  var LogAction$1 = LogAction;
2160
2193
 
@@ -2196,12 +2229,12 @@ const roundToNumDecimals = (num, numDecimals) => {
2196
2229
  * @returns escaped cell text
2197
2230
  */
2198
2231
  const escapeCellText = (text) => {
2199
- if (!text.includes(',')) {
2232
+ if (!String(text).includes(',')) {
2200
2233
  // No need to escape
2201
- return text;
2234
+ return String(text);
2202
2235
  }
2203
2236
  // Perform escape
2204
- return `"${text.replace(/"/g, '""')}`;
2237
+ return `"${String(text).replace(/"/g, '""')}`;
2205
2238
  };
2206
2239
  /**
2207
2240
  * Generate a CSV file
@@ -2220,9 +2253,26 @@ const genCSV = (data, columns) => {
2220
2253
  .join(','));
2221
2254
  // Add each row
2222
2255
  data.forEach((datum) => {
2256
+ csv += '\n';
2223
2257
  csv += (columns
2224
2258
  .map((column) => {
2225
- return escapeCellText(datum[column.param]);
2259
+ let contents;
2260
+ const cell = datum[column.param];
2261
+ if (typeof cell === 'string'
2262
+ || typeof cell === 'number') {
2263
+ contents = String(cell);
2264
+ }
2265
+ else if (typeof cell === 'undefined'
2266
+ || cell === null) {
2267
+ contents = '';
2268
+ }
2269
+ else if (typeof cell === 'object') {
2270
+ contents = JSON.stringify(cell);
2271
+ }
2272
+ else {
2273
+ contents = '';
2274
+ }
2275
+ return escapeCellText(contents);
2226
2276
  })
2227
2277
  .join(','));
2228
2278
  });
@@ -2251,11 +2301,12 @@ const CSVDownloadButton = (props) => {
2251
2301
  /* Main UI */
2252
2302
  /*----------------------------------------*/
2253
2303
  // Render the button
2254
- return (React__default["default"].createElement("a", { id: id, download: filename, href: `data:application/octet-stream,${csv}`, className: `CSVDownloadButton-button ${className !== null && className !== void 0 ? className : 'btn btn-secondary'}`, "aria-label": (ariaLabel
2304
+ return (React__default["default"].createElement("a", { id: id, download: filename, href: `data:application/octet-stream,${encodeURIComponent(csv)}`, className: `CSVDownloadButton-button ${className !== null && className !== void 0 ? className : 'btn btn-secondary'}`, "aria-label": (ariaLabel
2255
2305
  ? `Click to download ${filename}`
2256
2306
  : ariaLabel), style: style, onClick: onClick },
2257
2307
  !children && (React__default["default"].createElement(React__default["default"].Fragment, null,
2258
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCloudDownloadAlt, className: "mr-2" }),
2308
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCloudDownloadAlt }),
2309
+ ' ',
2259
2310
  "Download CSV")),
2260
2311
  children));
2261
2312
  };
@@ -2279,9 +2330,13 @@ var ActionType$1;
2279
2330
  // Toggle sort column param
2280
2331
  ActionType["ToggleSortColumn"] = "toggle-sort-column";
2281
2332
  // Toggle the visibility of a column
2282
- ActionType["ToggleColumnVisibility"] = "toggle-column-visibility";
2333
+ ActionType["UpdateColumnVisibility"] = "update-column-visibility";
2283
2334
  // Toggle the column visibility customization modal
2284
2335
  ActionType["ToggleColVisCusModalVisibility"] = "toggle-col-vis-cus-modal-visibility";
2336
+ // Show all columns
2337
+ ActionType["ShowAllColumns"] = "show-all-columns";
2338
+ // Hide all columns
2339
+ ActionType["HideAllColumns"] = "hide-all-columns";
2285
2340
  })(ActionType$1 || (ActionType$1 = {}));
2286
2341
  /**
2287
2342
  * Reducer that executes actions
@@ -2303,14 +2358,28 @@ const reducer$1 = (state, action) => {
2303
2358
  // Stop sorting by column
2304
2359
  return Object.assign(Object.assign({}, state), { sortColumnParam: undefined, sortType: SortType.Ascending });
2305
2360
  }
2306
- case ActionType$1.ToggleColumnVisibility: {
2361
+ case ActionType$1.UpdateColumnVisibility: {
2307
2362
  const { columnVisibilityMap } = state;
2308
- columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
2363
+ columnVisibilityMap[action.param] = action.visible;
2309
2364
  return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2310
2365
  }
2311
2366
  case ActionType$1.ToggleColVisCusModalVisibility: {
2312
2367
  return Object.assign(Object.assign({}, state), { columnVisibilityCustomizationModalVisible: !state.columnVisibilityCustomizationModalVisible });
2313
2368
  }
2369
+ case ActionType$1.ShowAllColumns: {
2370
+ const { columnVisibilityMap } = state;
2371
+ Object.keys(columnVisibilityMap).forEach((param) => {
2372
+ columnVisibilityMap[param] = true;
2373
+ });
2374
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2375
+ }
2376
+ case ActionType$1.HideAllColumns: {
2377
+ const { columnVisibilityMap } = state;
2378
+ Object.keys(columnVisibilityMap).forEach((param) => {
2379
+ columnVisibilityMap[param] = false;
2380
+ });
2381
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2382
+ }
2314
2383
  default: {
2315
2384
  return state;
2316
2385
  }
@@ -2326,7 +2395,18 @@ const IntelliTable = (props) => {
2326
2395
  var _a;
2327
2396
  /* -------------- Props ------------- */
2328
2397
  // Destructure all props
2329
- const { title, id, data, columns, } = props;
2398
+ const { title, id, columns, } = props;
2399
+ // Get data, show empty row if none
2400
+ const data = ((props.data && props.data.length > 0)
2401
+ ? props.data
2402
+ : [{ id: 'empty-row' }]);
2403
+ // Get CSV filename
2404
+ let filename = `${title}.csv`;
2405
+ if (props.csvName) {
2406
+ filename = (props.csvName.endsWith('.csv')
2407
+ ? props.csvName
2408
+ : `${props.csvName}.csv`);
2409
+ }
2330
2410
  /* -------------- State ------------- */
2331
2411
  // Initial state
2332
2412
  const initColumnVisibilityMap = {};
@@ -2343,21 +2423,49 @@ const IntelliTable = (props) => {
2343
2423
  const [state, dispatch] = React.useReducer(reducer$1, initialState);
2344
2424
  // Destructure common state
2345
2425
  const { sortColumnParam, sortType, columnVisibilityMap, columnVisibilityCustomizationModalVisible, } = state;
2426
+ /*------------------------------------------------------------------------*/
2427
+ /* Render */
2428
+ /*------------------------------------------------------------------------*/
2429
+ /*----------------------------------------*/
2430
+ /* Modal */
2431
+ /*----------------------------------------*/
2432
+ // Modal that may be defined
2433
+ let modal;
2346
2434
  /* ------- Col Vis Customization Modal ------ */
2347
2435
  if (columnVisibilityCustomizationModalVisible) {
2348
2436
  // Create modal
2349
- (React__default["default"].createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2437
+ modal = (React__default["default"].createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2438
+ const noColumnsSelected = (Object.values(columnVisibilityMap)
2439
+ .every((isSelected) => {
2440
+ return !isSelected;
2441
+ }));
2442
+ if (noColumnsSelected) {
2443
+ return alert$1('Choose at least one column', 'To continue, you have to choose at least one column to show');
2444
+ }
2350
2445
  dispatch({
2351
2446
  type: ActionType$1.ToggleColVisCusModalVisibility,
2352
2447
  });
2353
- } }, columns.map((column) => {
2354
- return (React__default["default"].createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, text: column.title, onChanged: () => {
2448
+ }, okayVariant: Variant$1.Light },
2449
+ columns.map((column) => {
2450
+ return (React__default["default"].createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, className: "mb-2", text: column.title, onChanged: (checked) => {
2451
+ dispatch({
2452
+ type: ActionType$1.UpdateColumnVisibility,
2453
+ param: column.param,
2454
+ visible: checked,
2455
+ });
2456
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column in the ${title} table`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Secondary }));
2457
+ }),
2458
+ React__default["default"].createElement("div", { className: "mt-3" }, "Or you can:"),
2459
+ React__default["default"].createElement("button", { type: "button", id: `IntelliTable-${id}-select-all-columns`, className: "btn btn-secondary me-2", "aria-label": `show all columns in the ${title} table`, onClick: () => {
2355
2460
  dispatch({
2356
- type: ActionType$1.ToggleColumnVisibility,
2357
- param: column.param,
2461
+ type: ActionType$1.ShowAllColumns,
2358
2462
  });
2359
- }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column` }));
2360
- })));
2463
+ } }, "Select All"),
2464
+ React__default["default"].createElement("button", { type: "button", id: `IntelliTable-${id}-select-none-columns`, className: "btn btn-secondary", "aria-label": `hide all columns in the ${title} table`, onClick: () => {
2465
+ dispatch({
2466
+ type: ActionType$1.HideAllColumns,
2467
+ });
2468
+ } }, "Deselect All")));
2361
2469
  }
2362
2470
  /*----------------------------------------*/
2363
2471
  /* Main UI */
@@ -2371,6 +2479,7 @@ const IntelliTable = (props) => {
2371
2479
  // Custom info based on current sort type
2372
2480
  let sortButtonAriaLabel;
2373
2481
  let sortIcon = freeSolidSvgIcons.faSort;
2482
+ let sortingByThisColumn = false;
2374
2483
  if (!sortColumnParam) {
2375
2484
  // Not being sorted yet
2376
2485
  sortButtonAriaLabel = `sort ascending by ${column.title}`;
@@ -2378,6 +2487,7 @@ const IntelliTable = (props) => {
2378
2487
  }
2379
2488
  else if (column.param === sortColumnParam) {
2380
2489
  // Already sorted by this column
2490
+ sortingByThisColumn = true;
2381
2491
  if (sortType === SortType.Ascending) {
2382
2492
  // Sorted ascending
2383
2493
  sortButtonAriaLabel = `sort descending by ${column.title}`;
@@ -2395,11 +2505,14 @@ const IntelliTable = (props) => {
2395
2505
  sortIcon = freeSolidSvgIcons.faSort;
2396
2506
  }
2397
2507
  // Create the cell UI
2398
- return (React__default["default"].createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}` },
2399
- React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-center flex-row h-100" },
2400
- React__default["default"].createElement("h4", { className: "m-0" }, column.title),
2508
+ return (React__default["default"].createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}`, className: "text-start", style: {
2509
+ borderRight: '0.05rem solid #555',
2510
+ borderLeft: '0.05rem solid #555',
2511
+ } },
2512
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-start flex-row h-100" },
2513
+ React__default["default"].createElement("span", { className: "text-nowrap" }, column.title),
2401
2514
  React__default["default"].createElement("div", null,
2402
- React__default["default"].createElement("button", { type: "button", className: "btn btn-light", "aria-label": sortButtonAriaLabel, onClick: () => {
2515
+ React__default["default"].createElement("button", { type: "button", id: `IntelliTable-${id}-sort-by-${column.param}-button`, className: `btn btn-${sortingByThisColumn ? 'light' : 'secondary'} btn-sm ms-1 ps-1 pe-1 pt-0 pb-0`, "aria-label": sortButtonAriaLabel, onClick: () => {
2403
2516
  dispatch({
2404
2517
  type: ActionType$1.ToggleSortColumn,
2405
2518
  param: column.param,
@@ -2417,8 +2530,28 @@ const IntelliTable = (props) => {
2417
2530
  const descending = (sortType === SortType.Descending);
2418
2531
  if (sortColumnParam) {
2419
2532
  sortedData.sort((a, b) => {
2533
+ var _a, _b;
2420
2534
  const aVal = a[sortColumnParam];
2421
2535
  const bVal = b[sortColumnParam];
2536
+ // Tiebreaker sort by timestamp, most recent first (used if tied)
2537
+ const tiebreaker = (((_a = b.timestamp) !== null && _a !== void 0 ? _a : 0)
2538
+ - ((_b = a.timestamp) !== null && _b !== void 0 ? _b : 0));
2539
+ // Auto-sort undefined and null to end of list
2540
+ if ((aVal === undefined || aVal === null)
2541
+ || (bVal === undefined || bVal === null)) {
2542
+ // At least one was undefined
2543
+ if ((aVal === undefined || aVal === null)
2544
+ && (bVal === undefined || bVal === null)) {
2545
+ // Both undefined
2546
+ return tiebreaker;
2547
+ }
2548
+ if (aVal === undefined || aVal === null) {
2549
+ // First was undefined
2550
+ return 1;
2551
+ }
2552
+ // Second was undefined
2553
+ return -1;
2554
+ }
2422
2555
  // Sort differently based on the data type
2423
2556
  // > Boolean
2424
2557
  if (paramType === ParamType$1.Boolean) {
@@ -2428,7 +2561,7 @@ const IntelliTable = (props) => {
2428
2561
  if (!aVal && bVal) {
2429
2562
  return (descending ? 1 : -1);
2430
2563
  }
2431
- return 0;
2564
+ return tiebreaker;
2432
2565
  }
2433
2566
  // > Number
2434
2567
  if (paramType === ParamType$1.Int
@@ -2440,12 +2573,12 @@ const IntelliTable = (props) => {
2440
2573
  // > String
2441
2574
  if (paramType === ParamType$1.String) {
2442
2575
  if (aVal < bVal) {
2443
- return (descending ? -1 : 1);
2576
+ return (descending ? 1 : -1);
2444
2577
  }
2445
2578
  if (aVal > bVal) {
2446
- return (descending ? 1 : -1);
2579
+ return (descending ? -1 : 1);
2447
2580
  }
2448
- return 0;
2581
+ return tiebreaker;
2449
2582
  }
2450
2583
  // > JSON
2451
2584
  if (paramType === ParamType$1.JSON) {
@@ -2460,7 +2593,7 @@ const IntelliTable = (props) => {
2460
2593
  : (aSize - bSize));
2461
2594
  }
2462
2595
  // No sort
2463
- return 0;
2596
+ return tiebreaker;
2464
2597
  });
2465
2598
  }
2466
2599
  // Table body
@@ -2485,9 +2618,12 @@ const IntelliTable = (props) => {
2485
2618
  const noValue = (value === undefined
2486
2619
  || value === null);
2487
2620
  visibleValue = (noValue
2488
- ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCheckCircle }))
2489
- : (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faXmarkCircle })));
2621
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2622
+ : (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: fullValue ? freeSolidSvgIcons.faCheckCircle : freeSolidSvgIcons.faXmarkCircle })));
2490
2623
  title = (fullValue ? 'True' : 'False');
2624
+ if (noValue) {
2625
+ title = 'Empty Cell';
2626
+ }
2491
2627
  }
2492
2628
  else if (column.type === ParamType$1.Int) {
2493
2629
  fullValue = Number.parseInt(value, 10);
@@ -2496,6 +2632,9 @@ const IntelliTable = (props) => {
2496
2632
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2497
2633
  : fullValue);
2498
2634
  title = String(fullValue);
2635
+ if (noValue) {
2636
+ title = 'Empty Cell';
2637
+ }
2499
2638
  }
2500
2639
  else if (column.type === ParamType$1.Float) {
2501
2640
  fullValue = Number.parseFloat(value);
@@ -2504,14 +2643,22 @@ const IntelliTable = (props) => {
2504
2643
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2505
2644
  : roundToNumDecimals(fullValue, 2));
2506
2645
  title = String(fullValue);
2646
+ if (noValue) {
2647
+ title = 'Empty Cell';
2648
+ }
2507
2649
  }
2508
2650
  else if (column.type === ParamType$1.String) {
2509
2651
  fullValue = String(value).trim();
2510
- const noValue = (value.trim().length) === 0;
2652
+ const noValue = (value === undefined
2653
+ || value === null
2654
+ || String(fullValue).trim().length === 0);
2511
2655
  visibleValue = (noValue
2512
2656
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2513
2657
  : fullValue);
2514
2658
  title = `"${value}"`;
2659
+ if (noValue) {
2660
+ title = 'Empty Cell';
2661
+ }
2515
2662
  }
2516
2663
  else if (column.type === ParamType$1.JSON) {
2517
2664
  fullValue = JSON.stringify(value);
@@ -2521,9 +2668,13 @@ const IntelliTable = (props) => {
2521
2668
  visibleValue = (noValue
2522
2669
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2523
2670
  : fullValue);
2671
+ title = "JSON Object";
2524
2672
  }
2525
2673
  // Create UI
2526
- return (React__default["default"].createElement("td", { key: `${datum.id}-${column.param}`, title: title }, visibleValue));
2674
+ return (React__default["default"].createElement("td", { key: `${datum.id}-${column.param}`, title: title, style: {
2675
+ borderRight: '0.05rem solid #555',
2676
+ borderLeft: '0.05rem solid #555',
2677
+ } }, visibleValue));
2527
2678
  }));
2528
2679
  // Add cells to a row
2529
2680
  return (React__default["default"].createElement("tr", { key: datum.id }, cells));
@@ -2540,14 +2691,17 @@ const IntelliTable = (props) => {
2540
2691
  })
2541
2692
  .length);
2542
2693
  // Build CSV
2543
- const csv = genCSV(data, columns);
2694
+ const csv = genCSV(data, columns.filter((column) => {
2695
+ return columnVisibilityMap[column.param];
2696
+ }));
2544
2697
  // Build main UI
2545
2698
  return (React__default["default"].createElement("div", { className: `IntelliTable-container-${id}` },
2546
- React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-center" },
2699
+ modal,
2700
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-start" },
2547
2701
  React__default["default"].createElement("h3", { className: "m-0" }, title),
2548
2702
  React__default["default"].createElement("div", { className: "flex-grow-1 text-end" },
2549
- React__default["default"].createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: `${title}.csv`, csv: csv }),
2550
- React__default["default"].createElement("button", { type: "button", className: "btn btn-secondary", "aria-label": `show panel for customizing which columns show in table ${title}`, id: `IntelliTable-${id}-show-column-customization-modal`, onClick: () => {
2703
+ React__default["default"].createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: filename, csv: csv }),
2704
+ React__default["default"].createElement("button", { type: "button", className: "btn btn-secondary ms-2", "aria-label": `show panel for customizing which columns show in table ${title}`, id: `IntelliTable-${id}-show-column-customization-modal`, onClick: () => {
2551
2705
  dispatch({
2552
2706
  type: ActionType$1.ToggleColVisCusModalVisibility,
2553
2707
  });
@@ -2559,7 +2713,7 @@ const IntelliTable = (props) => {
2559
2713
  numHiddenCols,
2560
2714
  ' ',
2561
2715
  "hidden)"))))),
2562
- React__default["default"].createElement("div", { className: `IntelliTable-table-${id}`, style: {
2716
+ React__default["default"].createElement("div", { className: `IntelliTable-table-${id} mt-2`, style: {
2563
2717
  overflowX: 'auto',
2564
2718
  } }, table)));
2565
2719
  };
@@ -2611,6 +2765,11 @@ const style = `
2611
2765
  border: 0.05rem solid black;
2612
2766
  border-radius: 0.5rem;
2613
2767
  overflow: hidden;
2768
+ padding: 0.7rem;
2769
+
2770
+ /* Solid background */
2771
+ background-color: white;
2772
+ color: black;
2614
2773
 
2615
2774
  /* Place contents in flex column */
2616
2775
  flex-direction: column;
@@ -2637,8 +2796,218 @@ const style = `
2637
2796
  /* Vertical scroll */
2638
2797
  overflow-y: auto;
2639
2798
  }
2799
+
2800
+ .LogReviewer-header-close-button {
2801
+ border: 0 !important;
2802
+ background-color: transparent !important;
2803
+ padding-top: 0 !important;
2804
+ padding-bottom: 0 !important;
2805
+ padding-right: 1em !important;
2806
+ margin: 0 !important;
2807
+ color: #444 !important;
2808
+
2809
+ right: 0 !important;
2810
+ position: absolute !important;
2811
+ }
2812
+ .LogReviewer-header-close-button:hover {
2813
+ border: 0 !important;
2814
+ background-color: transparent !important;
2815
+ padding-top: 0 !important;
2816
+ padding-bottom: 0 !important;
2817
+ margin: 0 !important;
2818
+ color: #000 !important;
2819
+ }
2640
2820
  `;
2641
2821
  /*------------------------------------------------------------------------*/
2822
+ /* Constants */
2823
+ /*------------------------------------------------------------------------*/
2824
+ const columns = [
2825
+ {
2826
+ title: 'First Name',
2827
+ param: 'userFirstName',
2828
+ type: ParamType$1.String,
2829
+ },
2830
+ {
2831
+ title: 'Last Name',
2832
+ param: 'userLastName',
2833
+ type: ParamType$1.String,
2834
+ },
2835
+ {
2836
+ title: 'Email',
2837
+ param: 'userEmail',
2838
+ type: ParamType$1.String,
2839
+ },
2840
+ {
2841
+ title: 'Canvas Id',
2842
+ param: 'userId',
2843
+ type: ParamType$1.Int,
2844
+ },
2845
+ {
2846
+ title: 'Student?',
2847
+ param: 'isLearner',
2848
+ type: ParamType$1.Boolean,
2849
+ },
2850
+ {
2851
+ title: 'Teaching Staff?',
2852
+ param: 'isTTM',
2853
+ type: ParamType$1.Boolean,
2854
+ startsHidden: true,
2855
+ },
2856
+ {
2857
+ title: 'Admin?',
2858
+ param: 'isAdmin',
2859
+ type: ParamType$1.Boolean,
2860
+ startsHidden: true,
2861
+ },
2862
+ {
2863
+ title: 'Course Canvas Id',
2864
+ param: 'courseId',
2865
+ type: ParamType$1.Int,
2866
+ startsHidden: true,
2867
+ },
2868
+ {
2869
+ title: 'Course Name',
2870
+ param: 'courseName',
2871
+ type: ParamType$1.String,
2872
+ },
2873
+ {
2874
+ title: 'Browser Name',
2875
+ param: 'browser.name',
2876
+ type: ParamType$1.String,
2877
+ startsHidden: true,
2878
+ },
2879
+ {
2880
+ title: 'Browser Version',
2881
+ param: 'browser.version',
2882
+ type: ParamType$1.String,
2883
+ startsHidden: true,
2884
+ },
2885
+ {
2886
+ title: 'OS',
2887
+ param: 'device.os',
2888
+ type: ParamType$1.String,
2889
+ startsHidden: true,
2890
+ },
2891
+ {
2892
+ title: 'Mobile?',
2893
+ param: 'device.isMobile',
2894
+ type: ParamType$1.Boolean,
2895
+ startsHidden: true,
2896
+ },
2897
+ {
2898
+ title: 'Year',
2899
+ param: 'year',
2900
+ type: ParamType$1.Int,
2901
+ },
2902
+ {
2903
+ title: 'Month',
2904
+ param: 'month',
2905
+ type: ParamType$1.Int,
2906
+ },
2907
+ {
2908
+ title: 'Day',
2909
+ param: 'day',
2910
+ type: ParamType$1.Int,
2911
+ },
2912
+ {
2913
+ title: 'Hour',
2914
+ param: 'hour',
2915
+ type: ParamType$1.Int,
2916
+ },
2917
+ {
2918
+ title: 'Minute',
2919
+ param: 'minute',
2920
+ type: ParamType$1.Int,
2921
+ startsHidden: true,
2922
+ },
2923
+ {
2924
+ title: 'Timestamp',
2925
+ param: 'timestamp',
2926
+ type: ParamType$1.Int,
2927
+ startsHidden: true,
2928
+ },
2929
+ {
2930
+ title: 'Context',
2931
+ param: 'context',
2932
+ type: ParamType$1.String,
2933
+ },
2934
+ {
2935
+ title: 'Subcontext',
2936
+ param: 'subcontext',
2937
+ type: ParamType$1.String,
2938
+ },
2939
+ {
2940
+ title: 'Tags',
2941
+ param: 'tags',
2942
+ type: ParamType$1.JSON,
2943
+ startsHidden: true,
2944
+ },
2945
+ {
2946
+ title: 'Log Level',
2947
+ param: 'level',
2948
+ type: ParamType$1.String,
2949
+ startsHidden: true,
2950
+ },
2951
+ {
2952
+ title: 'Metadata',
2953
+ param: 'metadata',
2954
+ type: ParamType$1.JSON,
2955
+ startsHidden: true,
2956
+ },
2957
+ {
2958
+ title: 'Source',
2959
+ param: 'source',
2960
+ type: ParamType$1.String,
2961
+ },
2962
+ {
2963
+ title: 'Server Route Path',
2964
+ param: 'routePath',
2965
+ type: ParamType$1.String,
2966
+ startsHidden: true,
2967
+ },
2968
+ {
2969
+ title: 'Server Route Template',
2970
+ param: 'routeTemplate',
2971
+ type: ParamType$1.String,
2972
+ startsHidden: true,
2973
+ },
2974
+ {
2975
+ title: 'Type',
2976
+ param: 'type',
2977
+ type: ParamType$1.String,
2978
+ },
2979
+ {
2980
+ title: 'Error Message',
2981
+ param: 'errorMessage',
2982
+ type: ParamType$1.String,
2983
+ startsHidden: true,
2984
+ },
2985
+ {
2986
+ title: 'Error Code',
2987
+ param: 'errorCode',
2988
+ type: ParamType$1.String,
2989
+ startsHidden: true,
2990
+ },
2991
+ {
2992
+ title: 'Error Stack',
2993
+ param: 'errorStack',
2994
+ type: ParamType$1.String,
2995
+ startsHidden: true,
2996
+ },
2997
+ {
2998
+ title: 'Action Target',
2999
+ param: 'target',
3000
+ type: ParamType$1.String,
3001
+ startsHidden: true,
3002
+ },
3003
+ {
3004
+ title: 'Action Type',
3005
+ param: 'action',
3006
+ type: ParamType$1.String,
3007
+ startsHidden: true,
3008
+ },
3009
+ ];
3010
+ /*------------------------------------------------------------------------*/
2642
3011
  /* Static Functions */
2643
3012
  /*------------------------------------------------------------------------*/
2644
3013
  /**
@@ -2656,7 +3025,7 @@ const genHumanReadableName = (machineReadableName) => {
2656
3025
  // Uppercase! Add a space before
2657
3026
  humanReadableName += ' ';
2658
3027
  }
2659
- humanReadableName += chars;
3028
+ humanReadableName += char;
2660
3029
  });
2661
3030
  // Trim and return
2662
3031
  return humanReadableName.trim();
@@ -2718,7 +3087,19 @@ const reducer = (state, action) => {
2718
3087
  return Object.assign(Object.assign({}, state), { contextFilterState: action.contextFilterState });
2719
3088
  }
2720
3089
  case ActionType.UpdateTagFilterState: {
2721
- return Object.assign(Object.assign({}, state), { tagFilterState: action.tagFilterState });
3090
+ const { tagFilterState } = action;
3091
+ // Select all if every tag is deselected
3092
+ const numTagsSelected = (Object.values(tagFilterState)
3093
+ .filter((isSelected) => {
3094
+ return isSelected;
3095
+ })
3096
+ .length);
3097
+ if (numTagsSelected === 0) {
3098
+ Object.keys(tagFilterState).forEach((tag) => {
3099
+ tagFilterState[tag] = true;
3100
+ });
3101
+ }
3102
+ return Object.assign(Object.assign({}, state), { tagFilterState });
2722
3103
  }
2723
3104
  case ActionType.UpdateActionErrorFilterState: {
2724
3105
  return Object.assign(Object.assign({}, state), { actionErrorFilterState: action.actionErrorFilterState });
@@ -2738,10 +3119,35 @@ const LogReviewer = (props) => {
2738
3119
  /*------------------------------------------------------------------------*/
2739
3120
  /* Setup */
2740
3121
  /*------------------------------------------------------------------------*/
2741
- var _a, _b, _c, _d, _e;
3122
+ var _a, _b, _c, _d, _e, _f;
2742
3123
  /* -------------- Props ------------- */
2743
3124
  // Destructure props
2744
3125
  const { LogMetadata, onClose, } = props;
3126
+ // Create complete map of contexts
3127
+ const contextMap = {};
3128
+ [
3129
+ ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}),
3130
+ LogBuiltInMetadata.Context,
3131
+ ].forEach((partialContextMap) => {
3132
+ Object.keys(partialContextMap).forEach((context) => {
3133
+ // Add context
3134
+ contextMap[context] = context;
3135
+ // If context has children, add an "uncategorized" subcontext
3136
+ if (typeof contextMap[context] !== 'string') {
3137
+ contextMap[context][LogBuiltInMetadata.Context.Uncategorized] = (LogBuiltInMetadata.Context.Uncategorized);
3138
+ }
3139
+ });
3140
+ });
3141
+ // Create complete map of targets
3142
+ const targetMap = {};
3143
+ [
3144
+ ((_b = LogMetadata.Target) !== null && _b !== void 0 ? _b : {}),
3145
+ LogBuiltInMetadata.Target,
3146
+ ].forEach((partialTargetMap) => {
3147
+ Object.keys(partialTargetMap).forEach((target) => {
3148
+ targetMap[target] = target;
3149
+ });
3150
+ });
2745
3151
  /* -------------- State ------------- */
2746
3152
  // Create initial date filter state
2747
3153
  const today = getTimeInfoInET();
@@ -2761,26 +3167,30 @@ const LogReviewer = (props) => {
2761
3167
  };
2762
3168
  // Create initial context filter state
2763
3169
  const initContextFilterState = {};
2764
- Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
3170
+ Object.keys((_c = LogMetadata.Context) !== null && _c !== void 0 ? _c : {}).forEach((context) => {
2765
3171
  var _a, _b;
2766
3172
  const contextValue = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
2767
3173
  if (typeof contextValue === 'string') {
2768
- // Case: no subcontexts
3174
+ // Case: no subcontexts, init as checked
2769
3175
  initContextFilterState[contextValue] = true;
2770
3176
  }
2771
3177
  else {
2772
3178
  // Case: subcontexts exist
2773
- initContextFilterState[contextValue._] = {};
3179
+ initContextFilterState[context] = {};
2774
3180
  Object.values(((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {})[context]).forEach((subcontext) => {
2775
- const subcontextValue = contextValue[subcontext];
2776
- initContextFilterState[contextValue._][subcontextValue] = true;
3181
+ // Skip self ("_")
3182
+ if (subcontext === '_') {
3183
+ return;
3184
+ }
3185
+ // Initialize as checked
3186
+ initContextFilterState[context][subcontext] = true;
2777
3187
  });
2778
3188
  }
2779
3189
  });
2780
3190
  // Create initial tag filter state
2781
3191
  const initTagFilterState = {};
2782
- Object.values((_b = LogMetadata.Tag) !== null && _b !== void 0 ? _b : {}).forEach((tagValue) => {
2783
- initTagFilterState[tagValue] = true;
3192
+ Object.keys((_d = LogMetadata.Tag) !== null && _d !== void 0 ? _d : {}).forEach((tag) => {
3193
+ initTagFilterState[tag] = false;
2784
3194
  });
2785
3195
  // Create advanced filter state
2786
3196
  const initAdvancedFilterState = {
@@ -2806,12 +3216,16 @@ const LogReviewer = (props) => {
2806
3216
  target: {},
2807
3217
  action: {},
2808
3218
  };
2809
- Object.values((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {}).forEach((target) => {
3219
+ Object.values((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {}).forEach((target) => {
2810
3220
  initActionErrorFilterState.target[target] = true;
2811
3221
  });
2812
3222
  Object.values(LogAction$1).forEach((action) => {
2813
3223
  initActionErrorFilterState.action[action] = true;
2814
3224
  });
3225
+ // Add built-in targets
3226
+ Object.values(LogBuiltInMetadata.Target).forEach((target) => {
3227
+ initActionErrorFilterState.target[target] = true;
3228
+ });
2815
3229
  // Initial state
2816
3230
  const initialState = {
2817
3231
  loading: true,
@@ -2845,15 +3259,18 @@ const LogReviewer = (props) => {
2845
3259
  let month = newDateFilterState.startDate.month;
2846
3260
  while (
2847
3261
  // Earlier year
2848
- (year <= newDateFilterState.endDate.year)
3262
+ (year < newDateFilterState.endDate.year)
2849
3263
  // Current year but included month
2850
3264
  || (year === newDateFilterState.endDate.year
2851
3265
  && month <= newDateFilterState.endDate.month)) {
2852
- // Add to list
2853
- toLoad.push({
2854
- year,
2855
- month,
2856
- });
3266
+ // Add to list if not already loaded
3267
+ if (!logMap[year]
3268
+ || !logMap[year][month]) {
3269
+ toLoad.push({
3270
+ year,
3271
+ month,
3272
+ });
3273
+ }
2857
3274
  // Increment
2858
3275
  month += 1;
2859
3276
  if (month > 12) {
@@ -2941,10 +3358,10 @@ const LogReviewer = (props) => {
2941
3358
  /* Filters */
2942
3359
  /*----------------------------------------*/
2943
3360
  // Filter toggle
2944
- const filterToggles = (React__default["default"].createElement("div", { className: "LogReviewer-filter-toggles d-flex align-items-center justify-content-center" },
3361
+ const filterToggles = (React__default["default"].createElement("div", { className: "LogReviewer-filter-toggles" },
2945
3362
  React__default["default"].createElement("h3", { className: "m-0" }, "Filters:"),
2946
- React__default["default"].createElement("div", { className: "LogReviewer-filter-toggle-buttons" },
2947
- React__default["default"].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: () => {
3363
+ React__default["default"].createElement("div", { className: "LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0" },
3364
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-date-filter-drawer", className: `btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle date filter drawer", onClick: () => {
2948
3365
  dispatch({
2949
3366
  type: ActionType.ToggleFilterDrawer,
2950
3367
  filterDrawer: FilterDrawer.Date,
@@ -2952,7 +3369,7 @@ const LogReviewer = (props) => {
2952
3369
  } },
2953
3370
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCalendar, className: "me-2" }),
2954
3371
  "Date"),
2955
- React__default["default"].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: () => {
3372
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-context-filter-drawer", className: `btn btn-${FilterDrawer.Context === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle context filter drawer", onClick: () => {
2956
3373
  dispatch({
2957
3374
  type: ActionType.ToggleFilterDrawer,
2958
3375
  filterDrawer: FilterDrawer.Context,
@@ -2960,15 +3377,15 @@ const LogReviewer = (props) => {
2960
3377
  } },
2961
3378
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "me-2" }),
2962
3379
  "Context"),
2963
- React__default["default"].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: () => {
3380
+ (LogMetadata.Tag && Object.keys(LogMetadata.Tag).length > 0) && (React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-tag-filter-drawer", className: `btn btn-${FilterDrawer.Tag === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle tag filter drawer", onClick: () => {
2964
3381
  dispatch({
2965
3382
  type: ActionType.ToggleFilterDrawer,
2966
3383
  filterDrawer: FilterDrawer.Tag,
2967
3384
  });
2968
3385
  } },
2969
3386
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTag, className: "me-2" }),
2970
- "Tag"),
2971
- React__default["default"].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: () => {
3387
+ "Tag")),
3388
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-action-filter-drawer", className: `btn btn-${FilterDrawer.Action === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle action and error filter drawer", onClick: () => {
2972
3389
  dispatch({
2973
3390
  type: ActionType.ToggleFilterDrawer,
2974
3391
  filterDrawer: FilterDrawer.Action,
@@ -2976,41 +3393,64 @@ const LogReviewer = (props) => {
2976
3393
  } },
2977
3394
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faHammer, className: "me-2" }),
2978
3395
  "Action"),
2979
- React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-advanced-filter-drawer", className: `btn btn-${FilterDrawer.Advanced === expandedFilterDrawer}`, "aria-label": "toggle advanced filter drawer", onClick: () => {
3396
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-advanced-filter-drawer", className: `btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle advanced filter drawer", onClick: () => {
2980
3397
  dispatch({
2981
3398
  type: ActionType.ToggleFilterDrawer,
2982
3399
  filterDrawer: FilterDrawer.Advanced,
2983
3400
  });
2984
3401
  } },
2985
3402
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faList, className: "me-2" }),
2986
- "Advanced"))));
3403
+ "Advanced"),
3404
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-reset-filters-button", className: "btn btn-light", "aria-label": "reset filters", onClick: () => {
3405
+ dispatch({
3406
+ type: ActionType.ResetFilters,
3407
+ initActionErrorFilterState,
3408
+ initAdvancedFilterState,
3409
+ initContextFilterState,
3410
+ initDateFilterState,
3411
+ initTagFilterState,
3412
+ });
3413
+ } },
3414
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes }),
3415
+ ' ',
3416
+ "Reset"))));
2987
3417
  // Filter drawer
2988
3418
  let filterDrawer;
2989
3419
  if (expandedFilterDrawer) {
2990
3420
  if (expandedFilterDrawer === FilterDrawer.Date) {
2991
3421
  filterDrawer = (React__default["default"].createElement(TabBox, { title: "Date" },
2992
- React__default["default"].createElement(SimpleDateChooser, { ariaLabel: "filter start date", name: "filter-start-date", year: dateFilterState.startDate.year, month: dateFilterState.startDate.month, day: dateFilterState.startDate.day, onChange: (month, day, year) => {
2993
- dispatch({
2994
- type: ActionType.UpdateDateFilterState,
2995
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { startDate: { month, day, year } }),
2996
- });
3422
+ React__default["default"].createElement(SimpleDateChooser, { ariaLabel: "filter start date", name: "filter-start-date", year: dateFilterState.startDate.year, month: dateFilterState.startDate.month, day: dateFilterState.startDate.day, chooseFromPast: true, numMonthsToShow: 36, onChange: (month, day, year) => {
3423
+ dateFilterState.startDate = { month, day, year };
3424
+ handleDateRangeUpdated(dateFilterState);
2997
3425
  } }),
2998
3426
  ' ',
2999
3427
  "to",
3000
3428
  ' ',
3001
- React__default["default"].createElement(SimpleDateChooser, { ariaLabel: "filter end date", name: "filter-end-date", year: dateFilterState.endDate.year, month: dateFilterState.endDate.month, day: dateFilterState.endDate.day, onChange: (month, day, year) => {
3002
- dispatch({
3003
- type: ActionType.UpdateDateFilterState,
3004
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { endDate: { month, day, year } }),
3005
- });
3429
+ React__default["default"].createElement(SimpleDateChooser, { ariaLabel: "filter end date", name: "filter-end-date", year: dateFilterState.endDate.year, month: dateFilterState.endDate.month, day: dateFilterState.endDate.day, chooseFromPast: true, numMonthsToShow: 12, onChange: (month, day, year) => {
3430
+ if (year < dateFilterState.startDate.year
3431
+ || (year === dateFilterState.startDate.year
3432
+ && month < dateFilterState.startDate.month)
3433
+ || (year === dateFilterState.startDate.year
3434
+ && month === dateFilterState.startDate.month
3435
+ && day < dateFilterState.startDate.day)) {
3436
+ return alert$1('Invalid Start Date', 'The start date cannot be before the end date.');
3437
+ }
3438
+ dateFilterState.endDate = { month, day, year };
3439
+ handleDateRangeUpdated(dateFilterState);
3006
3440
  } })));
3007
3441
  }
3008
3442
  else if (expandedFilterDrawer === FilterDrawer.Context) {
3009
3443
  // Create item picker items
3010
- const pickableItems = (Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {})
3011
- .map((context) => {
3012
- var _a;
3013
- const value = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
3444
+ const builtInPickableItem = {
3445
+ id: '_',
3446
+ name: 'Auto-logged',
3447
+ isGroup: true,
3448
+ children: [],
3449
+ };
3450
+ const pickableItems = [];
3451
+ Object.keys(contextMap)
3452
+ .forEach((context) => {
3453
+ const value = (contextMap)[context];
3014
3454
  if (typeof value === 'string') {
3015
3455
  // No subcategories
3016
3456
  const item = {
@@ -3019,38 +3459,64 @@ const LogReviewer = (props) => {
3019
3459
  isGroup: false,
3020
3460
  checked: !!contextFilterState[context],
3021
3461
  };
3022
- return item;
3462
+ // Add built-in items to its own folder
3463
+ const isBuiltIn = context in LogBuiltInMetadata.Context;
3464
+ if (isBuiltIn) {
3465
+ // Add to built-in pickable item
3466
+ builtInPickableItem.children.push(item);
3467
+ }
3468
+ else {
3469
+ // Add to pickable items list
3470
+ pickableItems.push(item);
3471
+ }
3023
3472
  }
3024
3473
  // Has subcategories
3025
3474
  const children = (Object.keys(value)
3475
+ // Remove parent name
3026
3476
  .filter((subcontext) => {
3027
3477
  return subcontext !== '_';
3028
3478
  })
3479
+ // Create child pickable items
3029
3480
  .map((subcontext) => {
3030
3481
  return {
3031
- id: `${context}-${subcontext}`,
3482
+ id: subcontext,
3032
3483
  name: genHumanReadableName(subcontext),
3033
3484
  isGroup: false,
3034
- checked: !!value[subcontext],
3485
+ checked: contextFilterState[context][subcontext],
3035
3486
  };
3036
3487
  }));
3037
3488
  const item = {
3038
3489
  id: context,
3039
- name: context,
3490
+ name: genHumanReadableName(context),
3040
3491
  isGroup: true,
3041
3492
  children,
3042
3493
  };
3043
- return item;
3044
- }));
3494
+ pickableItems.push(item);
3495
+ });
3496
+ // Add built-in contexts to end ofl ist
3497
+ pickableItems.push(builtInPickableItem);
3045
3498
  // Create filter UI
3046
3499
  filterDrawer = (React__default["default"].createElement(ItemPicker, { title: "Context", items: pickableItems, onChanged: (updatedItems) => {
3047
3500
  // Update our state
3048
3501
  updatedItems.forEach((pickableItem) => {
3049
3502
  if (pickableItem.isGroup) {
3050
3503
  // Has subcontexts
3051
- pickableItem.children.forEach((subcontextItem) => {
3052
- contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3053
- });
3504
+ if (pickableItem.id === '_') {
3505
+ // Built-in
3506
+ // Treat as if these were top-level contexts
3507
+ pickableItem.children.forEach((subcontextItem) => {
3508
+ contextFilterState[subcontextItem.id] = ('checked' in subcontextItem
3509
+ && subcontextItem.checked);
3510
+ });
3511
+ }
3512
+ else {
3513
+ // Not built-in
3514
+ pickableItem.children.forEach((subcontextItem) => {
3515
+ if (!subcontextItem.isGroup) {
3516
+ contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3517
+ }
3518
+ });
3519
+ }
3054
3520
  }
3055
3521
  else {
3056
3522
  // No subcontexts
@@ -3065,13 +3531,19 @@ const LogReviewer = (props) => {
3065
3531
  }
3066
3532
  else if (expandedFilterDrawer === FilterDrawer.Tag) {
3067
3533
  // Create filter UI
3068
- filterDrawer = (React__default["default"].createElement(TabBox, { title: "Tags" }, Object.keys(tagFilterState)
3069
- .map((tag, i) => {
3070
- const description = genHumanReadableName(tag);
3071
- return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-tag-${tag}-checkbox`, text: description, ariaLabel: `include logs tagged with "${description}" in results`, noMarginOnRight: i === Object.keys(tagFilterState).length - 1, onChanged: (checked) => {
3072
- tagFilterState[tag] = checked;
3073
- } }));
3074
- })));
3534
+ filterDrawer = (React__default["default"].createElement(TabBox, { title: "Tags" },
3535
+ React__default["default"].createElement("div", null, "If any are selected, logs must contain at least one but not necessarily all of the selected tags."),
3536
+ Object.keys((_f = LogMetadata.Tag) !== null && _f !== void 0 ? _f : {})
3537
+ .map((tag, i) => {
3538
+ const description = genHumanReadableName(tag);
3539
+ return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-tag-${tag}-checkbox`, text: description, ariaLabel: `require that logs be tagged with "${description}" or any other selected tag`, noMarginOnRight: i === Object.keys(tagFilterState).length - 1, checked: tagFilterState[tag], onChanged: (checked) => {
3540
+ tagFilterState[tag] = checked;
3541
+ dispatch({
3542
+ type: ActionType.UpdateTagFilterState,
3543
+ tagFilterState,
3544
+ });
3545
+ } }));
3546
+ })));
3075
3547
  }
3076
3548
  else if (expandedFilterDrawer === FilterDrawer.Action) {
3077
3549
  // Create filter UI
@@ -3100,34 +3572,37 @@ const LogReviewer = (props) => {
3100
3572
  }, ariaLabel: "only show error logs", selected: actionErrorFilterState.type === LogType$1.Error, noMarginOnRight: true })),
3101
3573
  (actionErrorFilterState.type === undefined
3102
3574
  || actionErrorFilterState.type === LogType$1.Action) && (React__default["default"].createElement(TabBox, { title: "Action Log Details" },
3103
- React__default["default"].createElement(ButtonInputGroup, { label: "Action" }, Object.keys(LogAction$1)
3104
- .map((action, i) => {
3105
- const description = genHumanReadableName(action);
3106
- return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-action-${action}-checkbox`, text: description, ariaLabel: `include logs with action type "${description}" in results`, noMarginOnRight: i === Object.keys(LogAction$1).length - 1, onChanged: (checked) => {
3107
- actionErrorFilterState.action[action] = checked;
3108
- dispatch({
3109
- type: ActionType.UpdateActionErrorFilterState,
3110
- actionErrorFilterState,
3111
- });
3112
- } }));
3113
- })),
3114
- React__default["default"].createElement(ButtonInputGroup, { label: "Target" }, Object.keys((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {})
3115
- .map((target, i) => {
3116
- var _a;
3117
- const description = genHumanReadableName(target);
3118
- return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, onChanged: (checked) => {
3119
- actionErrorFilterState.target[target] = checked;
3120
- dispatch({
3121
- type: ActionType.UpdateActionErrorFilterState,
3122
- actionErrorFilterState,
3123
- });
3124
- }, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
3125
- })))),
3575
+ React__default["default"].createElement(ButtonInputGroup, { label: "Action", className: "mb-2" },
3576
+ React__default["default"].createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys(LogAction$1)
3577
+ .map((action) => {
3578
+ const description = genHumanReadableName(action);
3579
+ return (React__default["default"].createElement(CheckboxButton, { key: action, id: `LogReviewer-action-${action}-checkbox`, text: description, ariaLabel: `include logs with action type "${description}" in results`, noMarginOnRight: true, checked: actionErrorFilterState.action[action], onChanged: (checked) => {
3580
+ actionErrorFilterState.action[action] = checked;
3581
+ console.log(actionErrorFilterState, action, checked);
3582
+ dispatch({
3583
+ type: ActionType.UpdateActionErrorFilterState,
3584
+ actionErrorFilterState,
3585
+ });
3586
+ } }));
3587
+ }))),
3588
+ React__default["default"].createElement(ButtonInputGroup, { label: "Target" },
3589
+ React__default["default"].createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys(targetMap)
3590
+ .map((target) => {
3591
+ const description = genHumanReadableName(target);
3592
+ return (React__default["default"].createElement(CheckboxButton, { key: target, id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, checked: actionErrorFilterState.target[target], noMarginOnRight: true, onChanged: (checked) => {
3593
+ actionErrorFilterState.target[target] = checked;
3594
+ console.log(actionErrorFilterState, target, checked);
3595
+ dispatch({
3596
+ type: ActionType.UpdateActionErrorFilterState,
3597
+ actionErrorFilterState,
3598
+ });
3599
+ } }));
3600
+ }))))),
3126
3601
  (actionErrorFilterState.type === undefined
3127
3602
  || actionErrorFilterState.type === LogType$1.Error) && (React__default["default"].createElement(TabBox, { title: "Error Log Details" },
3128
3603
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3129
3604
  React__default["default"].createElement("span", { className: "input-group-text" }, "Error Message"),
3130
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error message", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3605
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error message", value: actionErrorFilterState.errorMessage, placeholder: "e.g. undefined is not a function", onChange: (e) => {
3131
3606
  actionErrorFilterState.errorMessage = e.target.value;
3132
3607
  dispatch({
3133
3608
  type: ActionType.UpdateActionErrorFilterState,
@@ -3136,7 +3611,7 @@ const LogReviewer = (props) => {
3136
3611
  } })),
3137
3612
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3138
3613
  React__default["default"].createElement("span", { className: "input-group-text" }, "Error Code"),
3139
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3614
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorCode, placeholder: "e.g. GC22", onChange: (e) => {
3140
3615
  actionErrorFilterState.errorCode = ((e.target.value)
3141
3616
  .trim()
3142
3617
  .toUpperCase());
@@ -3152,7 +3627,7 @@ const LogReviewer = (props) => {
3152
3627
  React__default["default"].createElement(TabBox, { title: "User Info" },
3153
3628
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3154
3629
  React__default["default"].createElement("span", { className: "input-group-text" }, "User First Name"),
3155
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, onChange: (e) => {
3630
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, placeholder: "e.g. Divardo", onChange: (e) => {
3156
3631
  advancedFilterState.userFirstName = e.target.value;
3157
3632
  dispatch({
3158
3633
  type: ActionType.UpdateAdvancedFilterState,
@@ -3161,7 +3636,7 @@ const LogReviewer = (props) => {
3161
3636
  } })),
3162
3637
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3163
3638
  React__default["default"].createElement("span", { className: "input-group-text" }, "User Last Name"),
3164
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, onChange: (e) => {
3639
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, placeholder: "e.g. Calicci", onChange: (e) => {
3165
3640
  advancedFilterState.userLastName = e.target.value;
3166
3641
  dispatch({
3167
3642
  type: ActionType.UpdateAdvancedFilterState,
@@ -3170,7 +3645,7 @@ const LogReviewer = (props) => {
3170
3645
  } })),
3171
3646
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3172
3647
  React__default["default"].createElement("span", { className: "input-group-text" }, "User Email"),
3173
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user email", value: advancedFilterState.userEmail, onChange: (e) => {
3648
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user email", value: advancedFilterState.userEmail, placeholder: "e.g. calicci@fas.harvard.edu", onChange: (e) => {
3174
3649
  advancedFilterState.userEmail = ((e.target.value)
3175
3650
  .trim());
3176
3651
  dispatch({
@@ -3180,7 +3655,7 @@ const LogReviewer = (props) => {
3180
3655
  } })),
3181
3656
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3182
3657
  React__default["default"].createElement("span", { className: "input-group-text" }, "User Canvas Id"),
3183
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, onChange: (e) => {
3658
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, placeholder: "e.g. 104985", onChange: (e) => {
3184
3659
  const { value } = e.target;
3185
3660
  // Only update if value contains only numbers
3186
3661
  if (/^\d+$/.test(value)) {
@@ -3217,7 +3692,7 @@ const LogReviewer = (props) => {
3217
3692
  React__default["default"].createElement(TabBox, { title: "Course Info" },
3218
3693
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3219
3694
  React__default["default"].createElement("span", { className: "input-group-text" }, "Course Name"),
3220
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, onChange: (e) => {
3695
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, placeholder: "e.g. GLC 200", onChange: (e) => {
3221
3696
  advancedFilterState.courseName = e.target.value;
3222
3697
  dispatch({
3223
3698
  type: ActionType.UpdateAdvancedFilterState,
@@ -3226,7 +3701,7 @@ const LogReviewer = (props) => {
3226
3701
  } })),
3227
3702
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3228
3703
  React__default["default"].createElement("span", { className: "input-group-text" }, "Course Canvas Id"),
3229
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, onChange: (e) => {
3704
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, placeholder: "e.g. 15948", onChange: (e) => {
3230
3705
  const { value } = e.target;
3231
3706
  // Only update if value contains only numbers
3232
3707
  if (/^\d+$/.test(value)) {
@@ -3284,37 +3759,38 @@ const LogReviewer = (props) => {
3284
3759
  advancedFilterState,
3285
3760
  });
3286
3761
  }, noMarginOnRight: true })),
3287
- React__default["default"].createElement("div", { className: "input-group mb-2" },
3288
- React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Path"),
3289
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route path", placeholder: "e.g. /api/ttm/courses/12345", value: advancedFilterState.routePath, onChange: (e) => {
3290
- advancedFilterState.courseName = ((e.target.value)
3291
- .trim());
3292
- dispatch({
3293
- type: ActionType.UpdateAdvancedFilterState,
3294
- advancedFilterState,
3295
- });
3296
- } })),
3297
- React__default["default"].createElement("div", { className: "input-group mb-2" },
3298
- React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Template"),
3299
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route template", value: advancedFilterState.routeTemplate, placeholder: "e.g. /api/ttm/courses/:courseId", onChange: (e) => {
3300
- advancedFilterState.courseName = ((e.target.value)
3301
- .trim());
3302
- dispatch({
3303
- type: ActionType.UpdateAdvancedFilterState,
3304
- advancedFilterState,
3305
- });
3306
- } })))));
3762
+ advancedFilterState.source !== LogSource$1.Client && (React__default["default"].createElement("div", { className: "mt-2" },
3763
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3764
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Path"),
3765
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route path", value: advancedFilterState.routePath, placeholder: "e.g. /api/ttm/courses/12345", onChange: (e) => {
3766
+ advancedFilterState.courseName = ((e.target.value)
3767
+ .trim());
3768
+ dispatch({
3769
+ type: ActionType.UpdateAdvancedFilterState,
3770
+ advancedFilterState,
3771
+ });
3772
+ } })),
3773
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3774
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Template"),
3775
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route template", value: advancedFilterState.routeTemplate, placeholder: "e.g. /api/ttm/courses/:courseId", onChange: (e) => {
3776
+ advancedFilterState.courseName = ((e.target.value)
3777
+ .trim());
3778
+ dispatch({
3779
+ type: ActionType.UpdateAdvancedFilterState,
3780
+ advancedFilterState,
3781
+ });
3782
+ } })))))));
3307
3783
  }
3308
3784
  }
3309
3785
  // Filters UI
3310
3786
  const filters = (React__default["default"].createElement(React__default["default"].Fragment, null,
3311
3787
  filterToggles,
3312
- filterDrawer && (React__default["default"].createElement(Drawer, null, filterDrawer))));
3788
+ filterDrawer && (React__default["default"].createElement(Drawer, { customBackgroundColor: "#eee" }, filterDrawer))));
3313
3789
  // Actually filter the logs
3314
3790
  // > Perform filters
3315
3791
  const logs = [];
3316
3792
  Object.keys(logMap).forEach((year) => {
3317
- Object.keys(logMap).forEach((month) => {
3793
+ Object.keys(logMap[year]).forEach((month) => {
3318
3794
  logMap[year][month].forEach((log) => {
3319
3795
  /* ----------- Date Filter ---------- */
3320
3796
  var _a;
@@ -3350,16 +3826,23 @@ const LogReviewer = (props) => {
3350
3826
  // Whole context is deselected
3351
3827
  contextFilterState[log.context] === false
3352
3828
  // None of the subcontexts are selected
3353
- || (Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3354
- .every((isSelected) => {
3355
- return !isSelected;
3356
- }))) {
3829
+ || (
3830
+ // Has subcontexts
3831
+ typeof contextFilterState[log.context] !== 'boolean'
3832
+ // None of the subcontexts are selected
3833
+ && Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3834
+ .every((isSelected) => {
3835
+ return !isSelected;
3836
+ }))) {
3837
+ console.log(log.context, contextFilterState);
3357
3838
  return;
3358
3839
  }
3359
3840
  // Subcontext doesn't match
3360
3841
  if (
3361
- // Log has a subcontext
3362
- log.subcontext
3842
+ // Log context is not "uncategorized" (no point in further filters)
3843
+ log.context !== LogBuiltInMetadata.Context.Uncategorized
3844
+ // Log has a subcontext
3845
+ && log.subcontext
3363
3846
  // Context has subcontexts
3364
3847
  && (contextFilterState[log.context]
3365
3848
  && contextFilterState[log.context] !== false
@@ -3371,12 +3854,16 @@ const LogReviewer = (props) => {
3371
3854
  /* -------------- Tags -------------- */
3372
3855
  // No tags match
3373
3856
  if (
3374
- // Log has at least one tag
3375
- log.tags.length > 0
3857
+ // At least one tag is required
3858
+ Object.values(tagFilterState)
3859
+ .filter((isSelected) => {
3860
+ return isSelected;
3861
+ })
3862
+ .length > 0
3376
3863
  // No tags match
3377
- && (log.tags.every((tag) => {
3864
+ && log.tags.every((tag) => {
3378
3865
  return !tagFilterState[tag];
3379
- }))) {
3866
+ })) {
3380
3867
  return;
3381
3868
  }
3382
3869
  /* ------- Actions and Errors ------- */
@@ -3510,7 +3997,7 @@ const LogReviewer = (props) => {
3510
3997
  // Device info exists
3511
3998
  && log.device
3512
3999
  // Mobile filter doesn't match
3513
- && (advancedFilterState.isMobile === log.device.isMobile)) {
4000
+ && (advancedFilterState.isMobile !== log.device.isMobile)) {
3514
4001
  return;
3515
4002
  }
3516
4003
  // Log source doesn't match
@@ -3552,199 +4039,20 @@ const LogReviewer = (props) => {
3552
4039
  /*----------------------------------------*/
3553
4040
  /* Data */
3554
4041
  /*----------------------------------------*/
3555
- // Create data table
3556
- const columns = [
3557
- {
3558
- title: 'First Name',
3559
- param: 'userFirstName',
3560
- type: ParamType$1.String,
3561
- },
3562
- {
3563
- title: 'Last Name',
3564
- param: 'userLastName',
3565
- type: ParamType$1.String,
3566
- },
3567
- {
3568
- title: 'Email',
3569
- param: 'userEmail',
3570
- type: ParamType$1.String,
3571
- },
3572
- {
3573
- title: 'Canvas Id',
3574
- param: 'userId',
3575
- type: ParamType$1.Int,
3576
- },
3577
- {
3578
- title: 'Student',
3579
- param: 'isLearner',
3580
- type: ParamType$1.Boolean,
3581
- },
3582
- {
3583
- title: 'Teaching Staff',
3584
- param: 'isTTM',
3585
- type: ParamType$1.Boolean,
3586
- startsHidden: true,
3587
- },
3588
- {
3589
- title: 'Admin',
3590
- param: 'isAdmin',
3591
- type: ParamType$1.Boolean,
3592
- startsHidden: true,
3593
- },
3594
- {
3595
- title: 'Course Canvas Id',
3596
- param: 'courseId',
3597
- type: ParamType$1.Int,
3598
- startsHidden: true,
3599
- },
3600
- {
3601
- title: 'Course Name',
3602
- param: 'courseName',
3603
- type: ParamType$1.String,
3604
- },
3605
- {
3606
- title: 'Browser Name',
3607
- param: 'browser.name',
3608
- type: ParamType$1.String,
3609
- startsHidden: true,
3610
- },
3611
- {
3612
- title: 'Browser Version',
3613
- param: 'browser.version',
3614
- type: ParamType$1.String,
3615
- startsHidden: true,
3616
- },
3617
- {
3618
- title: 'OS',
3619
- param: 'device.os',
3620
- type: ParamType$1.String,
3621
- startsHidden: true,
3622
- },
3623
- {
3624
- title: 'Mobile',
3625
- param: 'device.isMobile',
3626
- type: ParamType$1.Boolean,
3627
- startsHidden: true,
3628
- },
3629
- {
3630
- title: 'Year',
3631
- param: 'year',
3632
- type: ParamType$1.Int,
3633
- },
3634
- {
3635
- title: 'Month',
3636
- param: 'month',
3637
- type: ParamType$1.Int,
3638
- },
3639
- {
3640
- title: 'Day',
3641
- param: 'day',
3642
- type: ParamType$1.Int,
3643
- },
3644
- {
3645
- title: 'Hour',
3646
- param: 'hour',
3647
- type: ParamType$1.Int,
3648
- },
3649
- {
3650
- title: 'Minute',
3651
- param: 'minute',
3652
- type: ParamType$1.Int,
3653
- startsHidden: true,
3654
- },
3655
- {
3656
- title: 'Timestamp',
3657
- param: 'timestamp',
3658
- type: ParamType$1.Int,
3659
- startsHidden: true,
3660
- },
3661
- {
3662
- title: 'Context',
3663
- param: 'context',
3664
- type: ParamType$1.String,
3665
- },
3666
- {
3667
- title: 'Subcontext',
3668
- param: 'subcontext',
3669
- type: ParamType$1.String,
3670
- },
3671
- {
3672
- title: 'Tags',
3673
- param: 'tags',
3674
- type: ParamType$1.JSON,
3675
- startsHidden: true,
3676
- },
3677
- {
3678
- title: 'Log Level',
3679
- param: 'level',
3680
- type: ParamType$1.String,
3681
- startsHidden: true,
3682
- },
3683
- {
3684
- title: 'Metadata',
3685
- param: 'metadata',
3686
- type: ParamType$1.JSON,
3687
- startsHidden: true,
3688
- },
3689
- {
3690
- title: 'Source',
3691
- param: 'source',
3692
- type: ParamType$1.String,
3693
- },
3694
- {
3695
- title: 'Server Route Path',
3696
- param: 'routePath',
3697
- type: ParamType$1.String,
3698
- startsHidden: true,
3699
- },
3700
- {
3701
- title: 'Server Route Template',
3702
- param: 'routeTemplate',
3703
- type: ParamType$1.String,
3704
- startsHidden: true,
3705
- },
3706
- {
3707
- title: 'Type',
3708
- param: 'type',
3709
- type: ParamType$1.String,
3710
- },
3711
- {
3712
- title: 'Error Message',
3713
- param: 'errorMessage',
3714
- type: ParamType$1.String,
3715
- startsHidden: true,
3716
- },
3717
- {
3718
- title: 'Error Code',
3719
- param: 'errorCode',
3720
- type: ParamType$1.String,
3721
- startsHidden: true,
3722
- },
3723
- {
3724
- title: 'Error Stack',
3725
- param: 'errorStack',
3726
- type: ParamType$1.String,
3727
- startsHidden: true,
3728
- },
3729
- {
3730
- title: 'Action Target',
3731
- param: 'target',
3732
- type: ParamType$1.String,
3733
- startsHidden: true,
3734
- },
3735
- {
3736
- title: 'Action Type',
3737
- param: 'action',
3738
- type: ParamType$1.String,
3739
- startsHidden: true,
3740
- },
3741
- ];
4042
+ // Nothing to show notice
4043
+ const noLogsNotice = (logs.length === 0
4044
+ ? (React__default["default"].createElement("div", { className: "alert alert-warning text-center mt-2" },
4045
+ React__default["default"].createElement("h4", { className: "m-1" }, "No Logs to Show"),
4046
+ React__default["default"].createElement("div", null, "Either your filters are too strict or no matching logs have been created yet.")))
4047
+ : undefined);
3742
4048
  // Create intelliTable
3743
- const dataTable = (React__default["default"].createElement(IntelliTable, { title: "Matching Logs", id: "logs", data: logs, columns: columns }));
4049
+ const dataTable = (React__default["default"].createElement(IntelliTable, { title: "Matching Logs:", csvName: `Logs from ${getHumanReadableDate()}`, id: "logs", data: logs, columns: columns }));
3744
4050
  // Main body
3745
4051
  body = (React__default["default"].createElement(React__default["default"].Fragment, null,
3746
4052
  filters,
3747
- dataTable));
4053
+ React__default["default"].createElement("div", { className: "mt-2" },
4054
+ dataTable,
4055
+ noLogsNotice)));
3748
4056
  }
3749
4057
  /* ---------- Wrap in Modal --------- */
3750
4058
  return (React__default["default"].createElement("div", { className: "LogReviewer-outer-container" },
@@ -3752,14 +4060,10 @@ const LogReviewer = (props) => {
3752
4060
  React__default["default"].createElement("div", { className: "LogReviewer-inner-container" },
3753
4061
  React__default["default"].createElement("div", { className: "LogReviewer-header" },
3754
4062
  React__default["default"].createElement("div", { className: "LogReviewer-header-title" },
3755
- React__default["default"].createElement("h1", { className: "m-0" }, "Log Review Dashboard")),
3756
- React__default["default"].createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-lg", "aria-label": "close log reviewer panel", onClick: onClose, style: {
3757
- border: 0,
3758
- backgroundColor: 'transparent',
3759
- padding: 0,
3760
- margin: 0,
3761
- } },
3762
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes }))),
4063
+ React__default["default"].createElement("h3", { className: "text-center m-0" }, "Log Review Dashboard")),
4064
+ React__default["default"].createElement("div", { style: { width: 0 } },
4065
+ React__default["default"].createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-dark btn-lg pe-0", "aria-label": "close log reviewer panel", onClick: onClose },
4066
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes })))),
3763
4067
  React__default["default"].createElement("div", { className: "LogReviewer-contents" }, body))));
3764
4068
  };
3765
4069
 
@@ -3855,6 +4159,18 @@ const floorToNumDecimals = (num, numDecimals) => {
3855
4159
  return (Math.floor(num * rounder) / rounder);
3856
4160
  };
3857
4161
 
4162
+ /**
4163
+ * Force a number to stay within specific bounds
4164
+ * @author Gabe Abrams
4165
+ * @param num the number to move into the bounds
4166
+ * @param min the minimum number in the bound
4167
+ * @param max the maximum number in the bound
4168
+ * @returns bounded number
4169
+ */
4170
+ const forceNumIntoBounds = (num, min, max) => {
4171
+ return Math.max(min, Math.min(max, num));
4172
+ };
4173
+
3858
4174
  /**
3859
4175
  * Pad a number's decimal with zeros on the right
3860
4176
  * (e.g. 5.2 becomes 5.20 with 2 digit padding)
@@ -4090,15 +4406,17 @@ const initServer = (opts) => {
4090
4406
  },
4091
4407
  handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
4092
4408
  // Get user info
4093
- const { userId, isAdmin } = params;
4409
+ const { year, month, userId, isAdmin, } = params;
4094
4410
  // Validate user
4095
- // isAdmin is already checked because path starts with '/admin'
4096
4411
  const canReview = yield canReviewLogs(userId, isAdmin);
4097
4412
  if (!canReview) {
4098
4413
  throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
4099
4414
  }
4100
4415
  // Query for logs
4101
- const logs = yield _logCollection.find({ userId });
4416
+ const logs = yield _logCollection.find({
4417
+ year,
4418
+ month,
4419
+ });
4102
4420
  // Return logs
4103
4421
  return logs;
4104
4422
  }),
@@ -4747,7 +5065,7 @@ const genRouteHandler = (opts) => {
4747
5065
  isAdmin: !!launchInfo.isAdmin,
4748
5066
  isTTM: !!launchInfo.isTTM,
4749
5067
  courseId: launchInfo.courseId,
4750
- courseName: launchInfo.courseName,
5068
+ courseName: launchInfo.contextLabel,
4751
5069
  browser,
4752
5070
  device,
4753
5071
  year,
@@ -4774,7 +5092,7 @@ const genRouteHandler = (opts) => {
4774
5092
  }
4775
5093
  : {
4776
5094
  type: LogType$1.Action,
4777
- target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoSpecificTarget),
5095
+ target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoTarget),
4778
5096
  action: ((_o = opts.action) !== null && _o !== void 0 ? _o : LogAction$1.Unknown),
4779
5097
  });
4780
5098
  // Source-specific info
@@ -4987,29 +5305,6 @@ const startMinWait = (minWaitMs) => {
4987
5305
  });
4988
5306
  };
4989
5307
 
4990
- // Import shared helpers
4991
- /**
4992
- * Get a human-readable description of a date (all in ET)
4993
- * @author Gabe Abrams
4994
- * @param [dateOrTimestamp=today] the date or timestamp for the date to describe
4995
- * @returns human-readable description of the date
4996
- */
4997
- const getHumanReadableDate = (dateOrTimestamp) => {
4998
- // Get the date info
4999
- const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
5000
- const currYear = getTimeInfoInET().year;
5001
- // Get the short month description
5002
- const monthName = getMonthName(month).short;
5003
- // Create start of description
5004
- let description = `${monthName} ${day}${getOrdinal(day)}`;
5005
- // Add on year if it's different
5006
- if (year !== currYear) {
5007
- description += ` ${year}`;
5008
- }
5009
- // Return description
5010
- return description;
5011
- };
5012
-
5013
5308
  /**
5014
5309
  * Get the current part of day (morning, evening, etc.)
5015
5310
  * @author Gabe Abrams