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

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 +728 -433
  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 +729 -434
  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 +27 -18
  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/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, faCloudDownloadAlt, faCheckCircle, faXmarkCircle, faMinus, faSort, faSortDown, faSortUp, faCalendar, faTag, faHammer, faList, faTimes } from '@fortawesome/free-solid-svg-icons';
3
+ import { faExclamationTriangle, faCircle, faDotCircle, faCheckSquare, faHourglass, faClipboard, faChevronDown, faChevronRight, faCloudDownloadAlt, faMinus, faCheckCircle, faXmarkCircle, 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
  /******************************************************************************
@@ -511,14 +511,14 @@ const LOG_ROUTE_PATH = `${ROUTE_PATH_PREFIX}/log`;
511
511
  const LogBuiltInMetadata = {
512
512
  // Contexts
513
513
  Context: {
514
- Uncategorized: 'n/a',
515
- ServerRenderedErrorPage: '_server-rendered-error-page',
516
- ServerEndpointError: '_server-endpoint-error',
517
- ClientFatalError: '_client-fatal-error',
514
+ Uncategorized: 'Uncategorized',
515
+ ServerRenderedErrorPage: 'ServerRenderedErrorPage',
516
+ ServerEndpointError: 'ServerEndpointError',
517
+ ClientFatalError: 'ClientFatalError',
518
518
  },
519
519
  // Targets
520
520
  Target: {
521
- NoSpecificTarget: 'n/a',
521
+ NoTarget: 'NoTarget',
522
522
  },
523
523
  };
524
524
 
@@ -674,7 +674,7 @@ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* ()
674
674
  ? opts.error.stack
675
675
  : undefined),
676
676
  target: (opts.action
677
- ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoSpecificTarget)
677
+ ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoTarget)
678
678
  : undefined),
679
679
  action: (opts.action
680
680
  ? opts.action
@@ -1180,21 +1180,21 @@ const ButtonInputGroup = (props) => {
1180
1180
  /*------------------------------------------------------------------------*/
1181
1181
  /* -------------- Props ------------- */
1182
1182
  // Destructure all props
1183
- const { label, minLabelWidth, children, } = props;
1183
+ const { label, minLabelWidth, children, className, } = props;
1184
1184
  /*------------------------------------------------------------------------*/
1185
1185
  /* Render */
1186
1186
  /*------------------------------------------------------------------------*/
1187
1187
  /*----------------------------------------*/
1188
1188
  /* Main UI */
1189
1189
  /*----------------------------------------*/
1190
- return (React.createElement("div", { className: "input-group" },
1190
+ return (React.createElement("div", { className: `input-group ${className !== null && className !== void 0 ? className : ''}` },
1191
1191
  React.createElement("div", { className: "input-group-prepend d-flex w-100" },
1192
1192
  React.createElement("span", { className: "input-group-text", style: {
1193
1193
  minWidth: (minLabelWidth !== null && minLabelWidth !== void 0 ? minLabelWidth : undefined),
1194
1194
  borderTopRightRadius: 0,
1195
1195
  borderBottomRightRadius: 0,
1196
1196
  } }, label),
1197
- React.createElement("span", { className: "input-group-text flex-grow-1 rounded-right", style: {
1197
+ React.createElement("span", { className: "input-group-text flex-grow-1 rounded-right d-flex flex-wrap", style: {
1198
1198
  backgroundColor: 'white',
1199
1199
  borderTopLeftRadius: 0,
1200
1200
  borderBottomLeftRadius: 0,
@@ -1339,7 +1339,7 @@ const SimpleDateChooser = (props) => {
1339
1339
  let startMonth = today.month;
1340
1340
  if (chooseFromPast) {
1341
1341
  startMonth -= Math.max(0, numMonthsToShow - 1);
1342
- if (startMonth <= 0) {
1342
+ while (startMonth <= 0) {
1343
1343
  startMonth += 12;
1344
1344
  startYear -= 1;
1345
1345
  }
@@ -1347,22 +1347,35 @@ const SimpleDateChooser = (props) => {
1347
1347
  for (let i = 0; i < numMonthsToShow; i++) {
1348
1348
  // Get month and year info
1349
1349
  const unmoddedMonth = (startMonth + i);
1350
- const month = (unmoddedMonth > 12
1351
- ? unmoddedMonth - 12
1352
- : unmoddedMonth);
1350
+ let month = unmoddedMonth;
1351
+ while (month > 12) {
1352
+ month -= 12;
1353
+ }
1353
1354
  const monthName = getMonthName(month).full;
1354
- const year = (unmoddedMonth > 12
1355
- ? startYear + 1
1356
- : startYear);
1355
+ // Year is start year +1 for each 12 months
1356
+ const year = (startYear + (Math.floor(unmoddedMonth / 12)));
1357
1357
  // Figure out which days are allowed
1358
1358
  const days = [];
1359
1359
  const numDaysInMonth = (new Date(year, month, 0)).getDate();
1360
- const firstDay = (month === today.month
1361
- ? today.day // Current month: start at current date
1362
- : 1 // Future month: start at beginning of month
1363
- );
1364
- for (let day = firstDay; day <= numDaysInMonth; day++) {
1365
- days.push(day);
1360
+ if (chooseFromPast) {
1361
+ // Past selection
1362
+ const numDaysToAdd = ((month === today.month)
1363
+ ? today.day // Current month, only add up to today
1364
+ : numDaysInMonth // Past month, add all days
1365
+ );
1366
+ for (let day = 1; day <= numDaysToAdd; day++) {
1367
+ days.push(day);
1368
+ }
1369
+ }
1370
+ else {
1371
+ // Future selection: add all remaining days of the month
1372
+ const firstDay = (month === today.month
1373
+ ? today.day // Current month: start at current date
1374
+ : 1 // Future month: start at beginning of month
1375
+ );
1376
+ for (let day = firstDay; day <= numDaysInMonth; day++) {
1377
+ days.push(day);
1378
+ }
1366
1379
  }
1367
1380
  choices.push({
1368
1381
  choiceName: `${monthName} ${year}`,
@@ -1433,14 +1446,16 @@ const Drawer = (props) => {
1433
1446
  /*------------------------------------------------------------------------*/
1434
1447
  /* -------------- Props ------------- */
1435
1448
  // Destructure all props
1436
- const { children, } = props;
1449
+ const { customBackgroundColor, children, } = props;
1437
1450
  /*------------------------------------------------------------------------*/
1438
1451
  /* Render */
1439
1452
  /*------------------------------------------------------------------------*/
1440
1453
  /*----------------------------------------*/
1441
1454
  /* Main UI */
1442
1455
  /*----------------------------------------*/
1443
- return (React.createElement("div", { className: "Drawer-container" },
1456
+ return (React.createElement("div", { className: "Drawer-container", style: {
1457
+ backgroundColor: (customBackgroundColor !== null && customBackgroundColor !== void 0 ? customBackgroundColor : undefined),
1458
+ } },
1444
1459
  React.createElement("style", null, style$4),
1445
1460
  children));
1446
1461
  };
@@ -1903,24 +1918,25 @@ const CopiableBox = (props) => {
1903
1918
  /**
1904
1919
  * Reusable nested item picker
1905
1920
  * @author Yuen Ler Chow
1921
+ * @author Gabe Abrams
1906
1922
  */
1907
1923
  /* ------------- Actions ------------ */
1908
1924
  // Types of actions
1909
1925
  var ActionType$2;
1910
1926
  (function (ActionType) {
1911
- // Toggle whether the children are being shown
1912
- ActionType["ToggleItems"] = "toggle-items";
1927
+ // Toggle whether a child are being shown
1928
+ ActionType["ToggleChild"] = "toggle-child";
1913
1929
  })(ActionType$2 || (ActionType$2 = {}));
1914
1930
  /**
1915
1931
  * Reducer that executes actions
1916
- * @author Yuen Ler Chow
1932
+ * @author Gabe Abrams
1917
1933
  * @param state current state
1918
1934
  * @param action action to execute
1919
1935
  */
1920
1936
  const reducer$2 = (state, action) => {
1921
1937
  switch (action.type) {
1922
- case ActionType$2.ToggleItems: {
1923
- return { isShowingItems: !state.isShowingItems };
1938
+ case ActionType$2.ToggleChild: {
1939
+ return Object.assign(Object.assign({}, state), { childExpanded: Object.assign(Object.assign({}, state.childExpanded), { [String(action.id)]: !state.childExpanded[String(action.id)] }) });
1924
1940
  }
1925
1941
  default: {
1926
1942
  return state;
@@ -1938,14 +1954,19 @@ const NestableItemList = (props) => {
1938
1954
  // Destructure all props
1939
1955
  const { items, onChanged, } = props;
1940
1956
  /* -------------- State ------------- */
1957
+ // Create initial map of child expanded booleans
1958
+ const initChildExpanded = {};
1959
+ items.forEach((item) => {
1960
+ initChildExpanded[String(item.id)] = false;
1961
+ });
1941
1962
  // Initial state
1942
1963
  const initialState = {
1943
- isShowingItems: false,
1964
+ childExpanded: initChildExpanded,
1944
1965
  };
1945
1966
  // Initialize state
1946
1967
  const [state, dispatch] = useReducer(reducer$2, initialState);
1947
1968
  // Destructure common state
1948
- const { isShowingItems, } = state;
1969
+ const { childExpanded, } = state;
1949
1970
  /*------------------------------------------------------------------------*/
1950
1971
  /* Component Functions */
1951
1972
  /*------------------------------------------------------------------------*/
@@ -2033,14 +2054,15 @@ const NestableItemList = (props) => {
2033
2054
  backgroundColor: 'transparent',
2034
2055
  }, type: "button", onClick: () => {
2035
2056
  dispatch({
2036
- type: ActionType$2.ToggleItems,
2057
+ type: ActionType$2.ToggleChild,
2058
+ id: item.id,
2037
2059
  });
2038
- }, "aria-label": `${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}` },
2039
- React.createElement(FontAwesomeIcon, { icon: isShowingItems ? faChevronDown : faChevronRight })))),
2060
+ }, "aria-label": `${childExpanded[item.id] ? 'Hide' : 'Show'} items in ${item.name}` },
2061
+ React.createElement(FontAwesomeIcon, { icon: childExpanded[item.id] ? faChevronDown : faChevronRight })))),
2040
2062
  React.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) => {
2041
2063
  onChanged(changeChecked(item.id, checked, items));
2042
2064
  }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light }),
2043
- item.isGroup && isShowingItems && (React.createElement("div", { className: "NestableItemList-children-container", style: {
2065
+ (item.isGroup && childExpanded[item.id]) && (React.createElement("div", { className: "NestableItemList-children-container", style: {
2044
2066
  paddingLeft: '2.2rem',
2045
2067
  } },
2046
2068
  React.createElement(NestableItemList, { items: item.children, onChanged: (updatedItems) => {
@@ -2062,7 +2084,7 @@ const ItemPicker = (props) => {
2062
2084
  /*------------------------------------------------------------------------*/
2063
2085
  /* -------------- Props ------------- */
2064
2086
  // Destructure all props
2065
- const { title, items, onChanged, } = props;
2087
+ const { title, items, onChanged, noBottomMargin, } = props;
2066
2088
  /*------------------------------------------------------------------------*/
2067
2089
  /* Component Functions */
2068
2090
  /*------------------------------------------------------------------------*/
@@ -2072,11 +2094,34 @@ const ItemPicker = (props) => {
2072
2094
  /*----------------------------------------*/
2073
2095
  /* Main UI */
2074
2096
  /*----------------------------------------*/
2075
- return (React.createElement(TabBox, { title: title },
2097
+ return (React.createElement(TabBox, { title: title, noBottomMargin: noBottomMargin },
2076
2098
  React.createElement("div", { style: { overflowX: 'auto' } },
2077
2099
  React.createElement(NestableItemList, { items: items, onChanged: onChanged }))));
2078
2100
  };
2079
2101
 
2102
+ // Import shared helpers
2103
+ /**
2104
+ * Get a human-readable description of a date (all in ET)
2105
+ * @author Gabe Abrams
2106
+ * @param [dateOrTimestamp=today] the date or timestamp for the date to describe
2107
+ * @returns human-readable description of the date
2108
+ */
2109
+ const getHumanReadableDate = (dateOrTimestamp) => {
2110
+ // Get the date info
2111
+ const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
2112
+ const currYear = getTimeInfoInET().year;
2113
+ // Get the short month description
2114
+ const monthName = getMonthName(month).short;
2115
+ // Create start of description
2116
+ let description = `${monthName} ${day}${getOrdinal(day)}`;
2117
+ // Add on year if it's different
2118
+ if (year !== currYear) {
2119
+ description += ` ${year}`;
2120
+ }
2121
+ // Return description
2122
+ return description;
2123
+ };
2124
+
2080
2125
  /**
2081
2126
  * Path of the route for storing client-side logs
2082
2127
  * @author Gabe Abrams
@@ -2116,37 +2161,37 @@ var LogType$1 = LogType;
2116
2161
  var LogAction;
2117
2162
  (function (LogAction) {
2118
2163
  // Target was opened by the user (it was not on screen, but now it is)
2119
- LogAction["Open"] = "open";
2164
+ LogAction["Open"] = "Open";
2120
2165
  // Target was closed by the user (it was on screen, but now it is not)
2121
- LogAction["Close"] = "close";
2166
+ LogAction["Close"] = "Close";
2122
2167
  // Target was cancelled by the user (it was on closed without saving)
2123
- LogAction["Cancel"] = "cancel";
2168
+ LogAction["Cancel"] = "Cancel";
2124
2169
  // Target was expanded by the user (it always remains on screen, but size was changed)
2125
- LogAction["Expand"] = "expand";
2170
+ LogAction["Expand"] = "Expand";
2126
2171
  // Target was collapsed by the user (it always remains on screen, but size was changed)
2127
- LogAction["Collapse"] = "collapse";
2172
+ LogAction["Collapse"] = "Collapse";
2128
2173
  // Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
2129
- LogAction["View"] = "view";
2174
+ LogAction["View"] = "View";
2130
2175
  // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2131
- LogAction["Interrupt"] = "interrupt";
2176
+ LogAction["Interrupt"] = "Interrupt";
2132
2177
  // Target was created by the user (it did not exist before)
2133
- LogAction["Create"] = "create";
2178
+ LogAction["Create"] = "Create";
2134
2179
  // Target was edited by the user (it existed and was changed)
2135
- LogAction["Edit"] = "edit";
2180
+ LogAction["Edit"] = "Edit";
2136
2181
  // Target was deleted by the user (it existed and now it doesn't)
2137
- LogAction["Delete"] = "delete";
2182
+ LogAction["Delete"] = "Delete";
2138
2183
  // Target was added by the user (it already existed and was added to another place)
2139
- LogAction["Add"] = "add";
2184
+ LogAction["Add"] = "Add";
2140
2185
  // Target was removed by the user (it was removed from something but still exists)
2141
- LogAction["Remove"] = "remove";
2186
+ LogAction["Remove"] = "Remove";
2142
2187
  // Target was activated by the user (click, check, tap, keypress, etc.)
2143
- LogAction["Activate"] = "activate";
2188
+ LogAction["Activate"] = "Activate";
2144
2189
  // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2145
- LogAction["Deactivate"] = "deactivate";
2190
+ LogAction["Deactivate"] = "Deactivate";
2146
2191
  // User showed interest in a target (hover, peek, etc.)
2147
- LogAction["Peek"] = "peek";
2192
+ LogAction["Peek"] = "Peek";
2148
2193
  // Unknown action
2149
- LogAction["Unknown"] = "unknown";
2194
+ LogAction["Unknown"] = "Unknown";
2150
2195
  })(LogAction || (LogAction = {}));
2151
2196
  var LogAction$1 = LogAction;
2152
2197
 
@@ -2188,12 +2233,12 @@ const roundToNumDecimals = (num, numDecimals) => {
2188
2233
  * @returns escaped cell text
2189
2234
  */
2190
2235
  const escapeCellText = (text) => {
2191
- if (!text.includes(',')) {
2236
+ if (!String(text).includes(',')) {
2192
2237
  // No need to escape
2193
- return text;
2238
+ return String(text);
2194
2239
  }
2195
2240
  // Perform escape
2196
- return `"${text.replace(/"/g, '""')}`;
2241
+ return `"${String(text).replace(/"/g, '""')}`;
2197
2242
  };
2198
2243
  /**
2199
2244
  * Generate a CSV file
@@ -2212,9 +2257,26 @@ const genCSV = (data, columns) => {
2212
2257
  .join(','));
2213
2258
  // Add each row
2214
2259
  data.forEach((datum) => {
2260
+ csv += '\n';
2215
2261
  csv += (columns
2216
2262
  .map((column) => {
2217
- return escapeCellText(datum[column.param]);
2263
+ let contents;
2264
+ const cell = datum[column.param];
2265
+ if (typeof cell === 'string'
2266
+ || typeof cell === 'number') {
2267
+ contents = String(cell);
2268
+ }
2269
+ else if (typeof cell === 'undefined'
2270
+ || cell === null) {
2271
+ contents = '';
2272
+ }
2273
+ else if (typeof cell === 'object') {
2274
+ contents = JSON.stringify(cell);
2275
+ }
2276
+ else {
2277
+ contents = '';
2278
+ }
2279
+ return escapeCellText(contents);
2218
2280
  })
2219
2281
  .join(','));
2220
2282
  });
@@ -2243,11 +2305,12 @@ const CSVDownloadButton = (props) => {
2243
2305
  /* Main UI */
2244
2306
  /*----------------------------------------*/
2245
2307
  // Render the button
2246
- return (React.createElement("a", { id: id, download: filename, href: `data:application/octet-stream,${csv}`, className: `CSVDownloadButton-button ${className !== null && className !== void 0 ? className : 'btn btn-secondary'}`, "aria-label": (ariaLabel
2308
+ return (React.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
2247
2309
  ? `Click to download ${filename}`
2248
2310
  : ariaLabel), style: style, onClick: onClick },
2249
2311
  !children && (React.createElement(React.Fragment, null,
2250
- React.createElement(FontAwesomeIcon, { icon: faCloudDownloadAlt, className: "mr-2" }),
2312
+ React.createElement(FontAwesomeIcon, { icon: faCloudDownloadAlt }),
2313
+ ' ',
2251
2314
  "Download CSV")),
2252
2315
  children));
2253
2316
  };
@@ -2271,9 +2334,13 @@ var ActionType$1;
2271
2334
  // Toggle sort column param
2272
2335
  ActionType["ToggleSortColumn"] = "toggle-sort-column";
2273
2336
  // Toggle the visibility of a column
2274
- ActionType["ToggleColumnVisibility"] = "toggle-column-visibility";
2337
+ ActionType["UpdateColumnVisibility"] = "update-column-visibility";
2275
2338
  // Toggle the column visibility customization modal
2276
2339
  ActionType["ToggleColVisCusModalVisibility"] = "toggle-col-vis-cus-modal-visibility";
2340
+ // Show all columns
2341
+ ActionType["ShowAllColumns"] = "show-all-columns";
2342
+ // Hide all columns
2343
+ ActionType["HideAllColumns"] = "hide-all-columns";
2277
2344
  })(ActionType$1 || (ActionType$1 = {}));
2278
2345
  /**
2279
2346
  * Reducer that executes actions
@@ -2295,14 +2362,28 @@ const reducer$1 = (state, action) => {
2295
2362
  // Stop sorting by column
2296
2363
  return Object.assign(Object.assign({}, state), { sortColumnParam: undefined, sortType: SortType.Ascending });
2297
2364
  }
2298
- case ActionType$1.ToggleColumnVisibility: {
2365
+ case ActionType$1.UpdateColumnVisibility: {
2299
2366
  const { columnVisibilityMap } = state;
2300
- columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
2367
+ columnVisibilityMap[action.param] = action.visible;
2301
2368
  return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2302
2369
  }
2303
2370
  case ActionType$1.ToggleColVisCusModalVisibility: {
2304
2371
  return Object.assign(Object.assign({}, state), { columnVisibilityCustomizationModalVisible: !state.columnVisibilityCustomizationModalVisible });
2305
2372
  }
2373
+ case ActionType$1.ShowAllColumns: {
2374
+ const { columnVisibilityMap } = state;
2375
+ Object.keys(columnVisibilityMap).forEach((param) => {
2376
+ columnVisibilityMap[param] = true;
2377
+ });
2378
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2379
+ }
2380
+ case ActionType$1.HideAllColumns: {
2381
+ const { columnVisibilityMap } = state;
2382
+ Object.keys(columnVisibilityMap).forEach((param) => {
2383
+ columnVisibilityMap[param] = false;
2384
+ });
2385
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2386
+ }
2306
2387
  default: {
2307
2388
  return state;
2308
2389
  }
@@ -2318,7 +2399,18 @@ const IntelliTable = (props) => {
2318
2399
  var _a;
2319
2400
  /* -------------- Props ------------- */
2320
2401
  // Destructure all props
2321
- const { title, id, data, columns, } = props;
2402
+ const { title, id, columns, } = props;
2403
+ // Get data, show empty row if none
2404
+ const data = ((props.data && props.data.length > 0)
2405
+ ? props.data
2406
+ : [{ id: 'empty-row' }]);
2407
+ // Get CSV filename
2408
+ let filename = `${title}.csv`;
2409
+ if (props.csvName) {
2410
+ filename = (props.csvName.endsWith('.csv')
2411
+ ? props.csvName
2412
+ : `${props.csvName}.csv`);
2413
+ }
2322
2414
  /* -------------- State ------------- */
2323
2415
  // Initial state
2324
2416
  const initColumnVisibilityMap = {};
@@ -2335,21 +2427,49 @@ const IntelliTable = (props) => {
2335
2427
  const [state, dispatch] = useReducer(reducer$1, initialState);
2336
2428
  // Destructure common state
2337
2429
  const { sortColumnParam, sortType, columnVisibilityMap, columnVisibilityCustomizationModalVisible, } = state;
2430
+ /*------------------------------------------------------------------------*/
2431
+ /* Render */
2432
+ /*------------------------------------------------------------------------*/
2433
+ /*----------------------------------------*/
2434
+ /* Modal */
2435
+ /*----------------------------------------*/
2436
+ // Modal that may be defined
2437
+ let modal;
2338
2438
  /* ------- Col Vis Customization Modal ------ */
2339
2439
  if (columnVisibilityCustomizationModalVisible) {
2340
2440
  // Create modal
2341
- (React.createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2441
+ modal = (React.createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2442
+ const noColumnsSelected = (Object.values(columnVisibilityMap)
2443
+ .every((isSelected) => {
2444
+ return !isSelected;
2445
+ }));
2446
+ if (noColumnsSelected) {
2447
+ return alert$1('Choose at least one column', 'To continue, you have to choose at least one column to show');
2448
+ }
2342
2449
  dispatch({
2343
2450
  type: ActionType$1.ToggleColVisCusModalVisibility,
2344
2451
  });
2345
- } }, columns.map((column) => {
2346
- return (React.createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, text: column.title, onChanged: () => {
2452
+ }, okayVariant: Variant$1.Light },
2453
+ columns.map((column) => {
2454
+ return (React.createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, className: "mb-2", text: column.title, onChanged: (checked) => {
2455
+ dispatch({
2456
+ type: ActionType$1.UpdateColumnVisibility,
2457
+ param: column.param,
2458
+ visible: checked,
2459
+ });
2460
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column in the ${title} table`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Secondary }));
2461
+ }),
2462
+ React.createElement("div", { className: "mt-3" }, "Or you can:"),
2463
+ React.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: () => {
2464
+ dispatch({
2465
+ type: ActionType$1.ShowAllColumns,
2466
+ });
2467
+ } }, "Select All"),
2468
+ React.createElement("button", { type: "button", id: `IntelliTable-${id}-select-none-columns`, className: "btn btn-secondary", "aria-label": `hide all columns in the ${title} table`, onClick: () => {
2347
2469
  dispatch({
2348
- type: ActionType$1.ToggleColumnVisibility,
2349
- param: column.param,
2470
+ type: ActionType$1.HideAllColumns,
2350
2471
  });
2351
- }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column` }));
2352
- })));
2472
+ } }, "Deselect All")));
2353
2473
  }
2354
2474
  /*----------------------------------------*/
2355
2475
  /* Main UI */
@@ -2363,6 +2483,7 @@ const IntelliTable = (props) => {
2363
2483
  // Custom info based on current sort type
2364
2484
  let sortButtonAriaLabel;
2365
2485
  let sortIcon = faSort;
2486
+ let sortingByThisColumn = false;
2366
2487
  if (!sortColumnParam) {
2367
2488
  // Not being sorted yet
2368
2489
  sortButtonAriaLabel = `sort ascending by ${column.title}`;
@@ -2370,6 +2491,7 @@ const IntelliTable = (props) => {
2370
2491
  }
2371
2492
  else if (column.param === sortColumnParam) {
2372
2493
  // Already sorted by this column
2494
+ sortingByThisColumn = true;
2373
2495
  if (sortType === SortType.Ascending) {
2374
2496
  // Sorted ascending
2375
2497
  sortButtonAriaLabel = `sort descending by ${column.title}`;
@@ -2387,11 +2509,14 @@ const IntelliTable = (props) => {
2387
2509
  sortIcon = faSort;
2388
2510
  }
2389
2511
  // Create the cell UI
2390
- return (React.createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}` },
2391
- React.createElement("div", { className: "d-flex align-items-center justify-content-center flex-row h-100" },
2392
- React.createElement("h4", { className: "m-0" }, column.title),
2512
+ return (React.createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}`, className: "text-start", style: {
2513
+ borderRight: '0.05rem solid #555',
2514
+ borderLeft: '0.05rem solid #555',
2515
+ } },
2516
+ React.createElement("div", { className: "d-flex align-items-center justify-content-start flex-row h-100" },
2517
+ React.createElement("span", { className: "text-nowrap" }, column.title),
2393
2518
  React.createElement("div", null,
2394
- React.createElement("button", { type: "button", className: "btn btn-light", "aria-label": sortButtonAriaLabel, onClick: () => {
2519
+ React.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: () => {
2395
2520
  dispatch({
2396
2521
  type: ActionType$1.ToggleSortColumn,
2397
2522
  param: column.param,
@@ -2409,8 +2534,28 @@ const IntelliTable = (props) => {
2409
2534
  const descending = (sortType === SortType.Descending);
2410
2535
  if (sortColumnParam) {
2411
2536
  sortedData.sort((a, b) => {
2537
+ var _a, _b;
2412
2538
  const aVal = a[sortColumnParam];
2413
2539
  const bVal = b[sortColumnParam];
2540
+ // Tiebreaker sort by timestamp, most recent first (used if tied)
2541
+ const tiebreaker = (((_a = b.timestamp) !== null && _a !== void 0 ? _a : 0)
2542
+ - ((_b = a.timestamp) !== null && _b !== void 0 ? _b : 0));
2543
+ // Auto-sort undefined and null to end of list
2544
+ if ((aVal === undefined || aVal === null)
2545
+ || (bVal === undefined || bVal === null)) {
2546
+ // At least one was undefined
2547
+ if ((aVal === undefined || aVal === null)
2548
+ && (bVal === undefined || bVal === null)) {
2549
+ // Both undefined
2550
+ return tiebreaker;
2551
+ }
2552
+ if (aVal === undefined || aVal === null) {
2553
+ // First was undefined
2554
+ return 1;
2555
+ }
2556
+ // Second was undefined
2557
+ return -1;
2558
+ }
2414
2559
  // Sort differently based on the data type
2415
2560
  // > Boolean
2416
2561
  if (paramType === ParamType$1.Boolean) {
@@ -2420,7 +2565,7 @@ const IntelliTable = (props) => {
2420
2565
  if (!aVal && bVal) {
2421
2566
  return (descending ? 1 : -1);
2422
2567
  }
2423
- return 0;
2568
+ return tiebreaker;
2424
2569
  }
2425
2570
  // > Number
2426
2571
  if (paramType === ParamType$1.Int
@@ -2432,12 +2577,12 @@ const IntelliTable = (props) => {
2432
2577
  // > String
2433
2578
  if (paramType === ParamType$1.String) {
2434
2579
  if (aVal < bVal) {
2435
- return (descending ? -1 : 1);
2580
+ return (descending ? 1 : -1);
2436
2581
  }
2437
2582
  if (aVal > bVal) {
2438
- return (descending ? 1 : -1);
2583
+ return (descending ? -1 : 1);
2439
2584
  }
2440
- return 0;
2585
+ return tiebreaker;
2441
2586
  }
2442
2587
  // > JSON
2443
2588
  if (paramType === ParamType$1.JSON) {
@@ -2452,7 +2597,7 @@ const IntelliTable = (props) => {
2452
2597
  : (aSize - bSize));
2453
2598
  }
2454
2599
  // No sort
2455
- return 0;
2600
+ return tiebreaker;
2456
2601
  });
2457
2602
  }
2458
2603
  // Table body
@@ -2477,9 +2622,12 @@ const IntelliTable = (props) => {
2477
2622
  const noValue = (value === undefined
2478
2623
  || value === null);
2479
2624
  visibleValue = (noValue
2480
- ? (React.createElement(FontAwesomeIcon, { icon: faCheckCircle }))
2481
- : (React.createElement(FontAwesomeIcon, { icon: faXmarkCircle })));
2625
+ ? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
2626
+ : (React.createElement(FontAwesomeIcon, { icon: fullValue ? faCheckCircle : faXmarkCircle })));
2482
2627
  title = (fullValue ? 'True' : 'False');
2628
+ if (noValue) {
2629
+ title = 'Empty Cell';
2630
+ }
2483
2631
  }
2484
2632
  else if (column.type === ParamType$1.Int) {
2485
2633
  fullValue = Number.parseInt(value, 10);
@@ -2488,6 +2636,9 @@ const IntelliTable = (props) => {
2488
2636
  ? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
2489
2637
  : fullValue);
2490
2638
  title = String(fullValue);
2639
+ if (noValue) {
2640
+ title = 'Empty Cell';
2641
+ }
2491
2642
  }
2492
2643
  else if (column.type === ParamType$1.Float) {
2493
2644
  fullValue = Number.parseFloat(value);
@@ -2496,14 +2647,22 @@ const IntelliTable = (props) => {
2496
2647
  ? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
2497
2648
  : roundToNumDecimals(fullValue, 2));
2498
2649
  title = String(fullValue);
2650
+ if (noValue) {
2651
+ title = 'Empty Cell';
2652
+ }
2499
2653
  }
2500
2654
  else if (column.type === ParamType$1.String) {
2501
2655
  fullValue = String(value).trim();
2502
- const noValue = (value.trim().length) === 0;
2656
+ const noValue = (value === undefined
2657
+ || value === null
2658
+ || String(fullValue).trim().length === 0);
2503
2659
  visibleValue = (noValue
2504
2660
  ? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
2505
2661
  : fullValue);
2506
2662
  title = `"${value}"`;
2663
+ if (noValue) {
2664
+ title = 'Empty Cell';
2665
+ }
2507
2666
  }
2508
2667
  else if (column.type === ParamType$1.JSON) {
2509
2668
  fullValue = JSON.stringify(value);
@@ -2513,9 +2672,13 @@ const IntelliTable = (props) => {
2513
2672
  visibleValue = (noValue
2514
2673
  ? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
2515
2674
  : fullValue);
2675
+ title = "JSON Object";
2516
2676
  }
2517
2677
  // Create UI
2518
- return (React.createElement("td", { key: `${datum.id}-${column.param}`, title: title }, visibleValue));
2678
+ return (React.createElement("td", { key: `${datum.id}-${column.param}`, title: title, style: {
2679
+ borderRight: '0.05rem solid #555',
2680
+ borderLeft: '0.05rem solid #555',
2681
+ } }, visibleValue));
2519
2682
  }));
2520
2683
  // Add cells to a row
2521
2684
  return (React.createElement("tr", { key: datum.id }, cells));
@@ -2532,14 +2695,17 @@ const IntelliTable = (props) => {
2532
2695
  })
2533
2696
  .length);
2534
2697
  // Build CSV
2535
- const csv = genCSV(data, columns);
2698
+ const csv = genCSV(data, columns.filter((column) => {
2699
+ return columnVisibilityMap[column.param];
2700
+ }));
2536
2701
  // Build main UI
2537
2702
  return (React.createElement("div", { className: `IntelliTable-container-${id}` },
2538
- React.createElement("div", { className: "d-flex align-items-center justify-content-center" },
2703
+ modal,
2704
+ React.createElement("div", { className: "d-flex align-items-center justify-content-start" },
2539
2705
  React.createElement("h3", { className: "m-0" }, title),
2540
2706
  React.createElement("div", { className: "flex-grow-1 text-end" },
2541
- React.createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: `${title}.csv`, csv: csv }),
2542
- React.createElement("button", { type: "button", className: "btn btn-secondary", "aria-label": `show panel for customizing which columns show in table ${title}`, id: `IntelliTable-${id}-show-column-customization-modal`, onClick: () => {
2707
+ React.createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: filename, csv: csv }),
2708
+ React.createElement("button", { type: "button", className: "btn btn-secondary ms-2", "aria-label": `show panel for customizing which columns show in table ${title}`, id: `IntelliTable-${id}-show-column-customization-modal`, onClick: () => {
2543
2709
  dispatch({
2544
2710
  type: ActionType$1.ToggleColVisCusModalVisibility,
2545
2711
  });
@@ -2551,7 +2717,7 @@ const IntelliTable = (props) => {
2551
2717
  numHiddenCols,
2552
2718
  ' ',
2553
2719
  "hidden)"))))),
2554
- React.createElement("div", { className: `IntelliTable-table-${id}`, style: {
2720
+ React.createElement("div", { className: `IntelliTable-table-${id} mt-2`, style: {
2555
2721
  overflowX: 'auto',
2556
2722
  } }, table)));
2557
2723
  };
@@ -2603,6 +2769,11 @@ const style = `
2603
2769
  border: 0.05rem solid black;
2604
2770
  border-radius: 0.5rem;
2605
2771
  overflow: hidden;
2772
+ padding: 0.7rem;
2773
+
2774
+ /* Solid background */
2775
+ background-color: white;
2776
+ color: black;
2606
2777
 
2607
2778
  /* Place contents in flex column */
2608
2779
  flex-direction: column;
@@ -2629,8 +2800,218 @@ const style = `
2629
2800
  /* Vertical scroll */
2630
2801
  overflow-y: auto;
2631
2802
  }
2803
+
2804
+ .LogReviewer-header-close-button {
2805
+ border: 0 !important;
2806
+ background-color: transparent !important;
2807
+ padding-top: 0 !important;
2808
+ padding-bottom: 0 !important;
2809
+ padding-right: 1em !important;
2810
+ margin: 0 !important;
2811
+ color: #444 !important;
2812
+
2813
+ right: 0 !important;
2814
+ position: absolute !important;
2815
+ }
2816
+ .LogReviewer-header-close-button:hover {
2817
+ border: 0 !important;
2818
+ background-color: transparent !important;
2819
+ padding-top: 0 !important;
2820
+ padding-bottom: 0 !important;
2821
+ margin: 0 !important;
2822
+ color: #000 !important;
2823
+ }
2632
2824
  `;
2633
2825
  /*------------------------------------------------------------------------*/
2826
+ /* Constants */
2827
+ /*------------------------------------------------------------------------*/
2828
+ const columns = [
2829
+ {
2830
+ title: 'First Name',
2831
+ param: 'userFirstName',
2832
+ type: ParamType$1.String,
2833
+ },
2834
+ {
2835
+ title: 'Last Name',
2836
+ param: 'userLastName',
2837
+ type: ParamType$1.String,
2838
+ },
2839
+ {
2840
+ title: 'Email',
2841
+ param: 'userEmail',
2842
+ type: ParamType$1.String,
2843
+ },
2844
+ {
2845
+ title: 'Canvas Id',
2846
+ param: 'userId',
2847
+ type: ParamType$1.Int,
2848
+ },
2849
+ {
2850
+ title: 'Student?',
2851
+ param: 'isLearner',
2852
+ type: ParamType$1.Boolean,
2853
+ },
2854
+ {
2855
+ title: 'Teaching Staff?',
2856
+ param: 'isTTM',
2857
+ type: ParamType$1.Boolean,
2858
+ startsHidden: true,
2859
+ },
2860
+ {
2861
+ title: 'Admin?',
2862
+ param: 'isAdmin',
2863
+ type: ParamType$1.Boolean,
2864
+ startsHidden: true,
2865
+ },
2866
+ {
2867
+ title: 'Course Canvas Id',
2868
+ param: 'courseId',
2869
+ type: ParamType$1.Int,
2870
+ startsHidden: true,
2871
+ },
2872
+ {
2873
+ title: 'Course Name',
2874
+ param: 'courseName',
2875
+ type: ParamType$1.String,
2876
+ },
2877
+ {
2878
+ title: 'Browser Name',
2879
+ param: 'browser.name',
2880
+ type: ParamType$1.String,
2881
+ startsHidden: true,
2882
+ },
2883
+ {
2884
+ title: 'Browser Version',
2885
+ param: 'browser.version',
2886
+ type: ParamType$1.String,
2887
+ startsHidden: true,
2888
+ },
2889
+ {
2890
+ title: 'OS',
2891
+ param: 'device.os',
2892
+ type: ParamType$1.String,
2893
+ startsHidden: true,
2894
+ },
2895
+ {
2896
+ title: 'Mobile?',
2897
+ param: 'device.isMobile',
2898
+ type: ParamType$1.Boolean,
2899
+ startsHidden: true,
2900
+ },
2901
+ {
2902
+ title: 'Year',
2903
+ param: 'year',
2904
+ type: ParamType$1.Int,
2905
+ },
2906
+ {
2907
+ title: 'Month',
2908
+ param: 'month',
2909
+ type: ParamType$1.Int,
2910
+ },
2911
+ {
2912
+ title: 'Day',
2913
+ param: 'day',
2914
+ type: ParamType$1.Int,
2915
+ },
2916
+ {
2917
+ title: 'Hour',
2918
+ param: 'hour',
2919
+ type: ParamType$1.Int,
2920
+ },
2921
+ {
2922
+ title: 'Minute',
2923
+ param: 'minute',
2924
+ type: ParamType$1.Int,
2925
+ startsHidden: true,
2926
+ },
2927
+ {
2928
+ title: 'Timestamp',
2929
+ param: 'timestamp',
2930
+ type: ParamType$1.Int,
2931
+ startsHidden: true,
2932
+ },
2933
+ {
2934
+ title: 'Context',
2935
+ param: 'context',
2936
+ type: ParamType$1.String,
2937
+ },
2938
+ {
2939
+ title: 'Subcontext',
2940
+ param: 'subcontext',
2941
+ type: ParamType$1.String,
2942
+ },
2943
+ {
2944
+ title: 'Tags',
2945
+ param: 'tags',
2946
+ type: ParamType$1.JSON,
2947
+ startsHidden: true,
2948
+ },
2949
+ {
2950
+ title: 'Log Level',
2951
+ param: 'level',
2952
+ type: ParamType$1.String,
2953
+ startsHidden: true,
2954
+ },
2955
+ {
2956
+ title: 'Metadata',
2957
+ param: 'metadata',
2958
+ type: ParamType$1.JSON,
2959
+ startsHidden: true,
2960
+ },
2961
+ {
2962
+ title: 'Source',
2963
+ param: 'source',
2964
+ type: ParamType$1.String,
2965
+ },
2966
+ {
2967
+ title: 'Server Route Path',
2968
+ param: 'routePath',
2969
+ type: ParamType$1.String,
2970
+ startsHidden: true,
2971
+ },
2972
+ {
2973
+ title: 'Server Route Template',
2974
+ param: 'routeTemplate',
2975
+ type: ParamType$1.String,
2976
+ startsHidden: true,
2977
+ },
2978
+ {
2979
+ title: 'Type',
2980
+ param: 'type',
2981
+ type: ParamType$1.String,
2982
+ },
2983
+ {
2984
+ title: 'Error Message',
2985
+ param: 'errorMessage',
2986
+ type: ParamType$1.String,
2987
+ startsHidden: true,
2988
+ },
2989
+ {
2990
+ title: 'Error Code',
2991
+ param: 'errorCode',
2992
+ type: ParamType$1.String,
2993
+ startsHidden: true,
2994
+ },
2995
+ {
2996
+ title: 'Error Stack',
2997
+ param: 'errorStack',
2998
+ type: ParamType$1.String,
2999
+ startsHidden: true,
3000
+ },
3001
+ {
3002
+ title: 'Action Target',
3003
+ param: 'target',
3004
+ type: ParamType$1.String,
3005
+ startsHidden: true,
3006
+ },
3007
+ {
3008
+ title: 'Action Type',
3009
+ param: 'action',
3010
+ type: ParamType$1.String,
3011
+ startsHidden: true,
3012
+ },
3013
+ ];
3014
+ /*------------------------------------------------------------------------*/
2634
3015
  /* Static Functions */
2635
3016
  /*------------------------------------------------------------------------*/
2636
3017
  /**
@@ -2648,7 +3029,7 @@ const genHumanReadableName = (machineReadableName) => {
2648
3029
  // Uppercase! Add a space before
2649
3030
  humanReadableName += ' ';
2650
3031
  }
2651
- humanReadableName += chars;
3032
+ humanReadableName += char;
2652
3033
  });
2653
3034
  // Trim and return
2654
3035
  return humanReadableName.trim();
@@ -2710,7 +3091,19 @@ const reducer = (state, action) => {
2710
3091
  return Object.assign(Object.assign({}, state), { contextFilterState: action.contextFilterState });
2711
3092
  }
2712
3093
  case ActionType.UpdateTagFilterState: {
2713
- return Object.assign(Object.assign({}, state), { tagFilterState: action.tagFilterState });
3094
+ const { tagFilterState } = action;
3095
+ // Select all if every tag is deselected
3096
+ const numTagsSelected = (Object.values(tagFilterState)
3097
+ .filter((isSelected) => {
3098
+ return isSelected;
3099
+ })
3100
+ .length);
3101
+ if (numTagsSelected === 0) {
3102
+ Object.keys(tagFilterState).forEach((tag) => {
3103
+ tagFilterState[tag] = true;
3104
+ });
3105
+ }
3106
+ return Object.assign(Object.assign({}, state), { tagFilterState });
2714
3107
  }
2715
3108
  case ActionType.UpdateActionErrorFilterState: {
2716
3109
  return Object.assign(Object.assign({}, state), { actionErrorFilterState: action.actionErrorFilterState });
@@ -2730,10 +3123,35 @@ const LogReviewer = (props) => {
2730
3123
  /*------------------------------------------------------------------------*/
2731
3124
  /* Setup */
2732
3125
  /*------------------------------------------------------------------------*/
2733
- var _a, _b, _c, _d, _e;
3126
+ var _a, _b, _c, _d, _e, _f;
2734
3127
  /* -------------- Props ------------- */
2735
3128
  // Destructure props
2736
3129
  const { LogMetadata, onClose, } = props;
3130
+ // Create complete map of contexts
3131
+ const contextMap = {};
3132
+ [
3133
+ ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}),
3134
+ LogBuiltInMetadata.Context,
3135
+ ].forEach((partialContextMap) => {
3136
+ Object.keys(partialContextMap).forEach((context) => {
3137
+ // Add context
3138
+ contextMap[context] = context;
3139
+ // If context has children, add an "uncategorized" subcontext
3140
+ if (typeof contextMap[context] !== 'string') {
3141
+ contextMap[context][LogBuiltInMetadata.Context.Uncategorized] = (LogBuiltInMetadata.Context.Uncategorized);
3142
+ }
3143
+ });
3144
+ });
3145
+ // Create complete map of targets
3146
+ const targetMap = {};
3147
+ [
3148
+ ((_b = LogMetadata.Target) !== null && _b !== void 0 ? _b : {}),
3149
+ LogBuiltInMetadata.Target,
3150
+ ].forEach((partialTargetMap) => {
3151
+ Object.keys(partialTargetMap).forEach((target) => {
3152
+ targetMap[target] = target;
3153
+ });
3154
+ });
2737
3155
  /* -------------- State ------------- */
2738
3156
  // Create initial date filter state
2739
3157
  const today = getTimeInfoInET();
@@ -2753,26 +3171,30 @@ const LogReviewer = (props) => {
2753
3171
  };
2754
3172
  // Create initial context filter state
2755
3173
  const initContextFilterState = {};
2756
- Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
3174
+ Object.keys((_c = LogMetadata.Context) !== null && _c !== void 0 ? _c : {}).forEach((context) => {
2757
3175
  var _a, _b;
2758
3176
  const contextValue = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
2759
3177
  if (typeof contextValue === 'string') {
2760
- // Case: no subcontexts
3178
+ // Case: no subcontexts, init as checked
2761
3179
  initContextFilterState[contextValue] = true;
2762
3180
  }
2763
3181
  else {
2764
3182
  // Case: subcontexts exist
2765
- initContextFilterState[contextValue._] = {};
3183
+ initContextFilterState[context] = {};
2766
3184
  Object.values(((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {})[context]).forEach((subcontext) => {
2767
- const subcontextValue = contextValue[subcontext];
2768
- initContextFilterState[contextValue._][subcontextValue] = true;
3185
+ // Skip self ("_")
3186
+ if (subcontext === '_') {
3187
+ return;
3188
+ }
3189
+ // Initialize as checked
3190
+ initContextFilterState[context][subcontext] = true;
2769
3191
  });
2770
3192
  }
2771
3193
  });
2772
3194
  // Create initial tag filter state
2773
3195
  const initTagFilterState = {};
2774
- Object.values((_b = LogMetadata.Tag) !== null && _b !== void 0 ? _b : {}).forEach((tagValue) => {
2775
- initTagFilterState[tagValue] = true;
3196
+ Object.keys((_d = LogMetadata.Tag) !== null && _d !== void 0 ? _d : {}).forEach((tag) => {
3197
+ initTagFilterState[tag] = false;
2776
3198
  });
2777
3199
  // Create advanced filter state
2778
3200
  const initAdvancedFilterState = {
@@ -2798,12 +3220,16 @@ const LogReviewer = (props) => {
2798
3220
  target: {},
2799
3221
  action: {},
2800
3222
  };
2801
- Object.values((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {}).forEach((target) => {
3223
+ Object.values((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {}).forEach((target) => {
2802
3224
  initActionErrorFilterState.target[target] = true;
2803
3225
  });
2804
3226
  Object.values(LogAction$1).forEach((action) => {
2805
3227
  initActionErrorFilterState.action[action] = true;
2806
3228
  });
3229
+ // Add built-in targets
3230
+ Object.values(LogBuiltInMetadata.Target).forEach((target) => {
3231
+ initActionErrorFilterState.target[target] = true;
3232
+ });
2807
3233
  // Initial state
2808
3234
  const initialState = {
2809
3235
  loading: true,
@@ -2837,15 +3263,18 @@ const LogReviewer = (props) => {
2837
3263
  let month = newDateFilterState.startDate.month;
2838
3264
  while (
2839
3265
  // Earlier year
2840
- (year <= newDateFilterState.endDate.year)
3266
+ (year < newDateFilterState.endDate.year)
2841
3267
  // Current year but included month
2842
3268
  || (year === newDateFilterState.endDate.year
2843
3269
  && month <= newDateFilterState.endDate.month)) {
2844
- // Add to list
2845
- toLoad.push({
2846
- year,
2847
- month,
2848
- });
3270
+ // Add to list if not already loaded
3271
+ if (!logMap[year]
3272
+ || !logMap[year][month]) {
3273
+ toLoad.push({
3274
+ year,
3275
+ month,
3276
+ });
3277
+ }
2849
3278
  // Increment
2850
3279
  month += 1;
2851
3280
  if (month > 12) {
@@ -2933,10 +3362,10 @@ const LogReviewer = (props) => {
2933
3362
  /* Filters */
2934
3363
  /*----------------------------------------*/
2935
3364
  // Filter toggle
2936
- const filterToggles = (React.createElement("div", { className: "LogReviewer-filter-toggles d-flex align-items-center justify-content-center" },
3365
+ const filterToggles = (React.createElement("div", { className: "LogReviewer-filter-toggles" },
2937
3366
  React.createElement("h3", { className: "m-0" }, "Filters:"),
2938
- React.createElement("div", { className: "LogReviewer-filter-toggle-buttons" },
2939
- 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: () => {
3367
+ React.createElement("div", { className: "LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0" },
3368
+ React.createElement("button", { type: "button", id: "LogReviewer-toggle-date-filter-drawer", className: `btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle date filter drawer", onClick: () => {
2940
3369
  dispatch({
2941
3370
  type: ActionType.ToggleFilterDrawer,
2942
3371
  filterDrawer: FilterDrawer.Date,
@@ -2944,7 +3373,7 @@ const LogReviewer = (props) => {
2944
3373
  } },
2945
3374
  React.createElement(FontAwesomeIcon, { icon: faCalendar, className: "me-2" }),
2946
3375
  "Date"),
2947
- 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: () => {
3376
+ React.createElement("button", { type: "button", id: "LogReviewer-toggle-context-filter-drawer", className: `btn btn-${FilterDrawer.Context === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle context filter drawer", onClick: () => {
2948
3377
  dispatch({
2949
3378
  type: ActionType.ToggleFilterDrawer,
2950
3379
  filterDrawer: FilterDrawer.Context,
@@ -2952,15 +3381,15 @@ const LogReviewer = (props) => {
2952
3381
  } },
2953
3382
  React.createElement(FontAwesomeIcon, { icon: faCircle, className: "me-2" }),
2954
3383
  "Context"),
2955
- 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: () => {
3384
+ (LogMetadata.Tag && Object.keys(LogMetadata.Tag).length > 0) && (React.createElement("button", { type: "button", id: "LogReviewer-toggle-tag-filter-drawer", className: `btn btn-${FilterDrawer.Tag === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle tag filter drawer", onClick: () => {
2956
3385
  dispatch({
2957
3386
  type: ActionType.ToggleFilterDrawer,
2958
3387
  filterDrawer: FilterDrawer.Tag,
2959
3388
  });
2960
3389
  } },
2961
3390
  React.createElement(FontAwesomeIcon, { icon: faTag, className: "me-2" }),
2962
- "Tag"),
2963
- 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: () => {
3391
+ "Tag")),
3392
+ React.createElement("button", { type: "button", id: "LogReviewer-toggle-action-filter-drawer", className: `btn btn-${FilterDrawer.Action === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle action and error filter drawer", onClick: () => {
2964
3393
  dispatch({
2965
3394
  type: ActionType.ToggleFilterDrawer,
2966
3395
  filterDrawer: FilterDrawer.Action,
@@ -2968,41 +3397,64 @@ const LogReviewer = (props) => {
2968
3397
  } },
2969
3398
  React.createElement(FontAwesomeIcon, { icon: faHammer, className: "me-2" }),
2970
3399
  "Action"),
2971
- React.createElement("button", { type: "button", id: "LogReviewer-toggle-advanced-filter-drawer", className: `btn btn-${FilterDrawer.Advanced === expandedFilterDrawer}`, "aria-label": "toggle advanced filter drawer", onClick: () => {
3400
+ React.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: () => {
2972
3401
  dispatch({
2973
3402
  type: ActionType.ToggleFilterDrawer,
2974
3403
  filterDrawer: FilterDrawer.Advanced,
2975
3404
  });
2976
3405
  } },
2977
3406
  React.createElement(FontAwesomeIcon, { icon: faList, className: "me-2" }),
2978
- "Advanced"))));
3407
+ "Advanced"),
3408
+ React.createElement("button", { type: "button", id: "LogReviewer-reset-filters-button", className: "btn btn-light", "aria-label": "reset filters", onClick: () => {
3409
+ dispatch({
3410
+ type: ActionType.ResetFilters,
3411
+ initActionErrorFilterState,
3412
+ initAdvancedFilterState,
3413
+ initContextFilterState,
3414
+ initDateFilterState,
3415
+ initTagFilterState,
3416
+ });
3417
+ } },
3418
+ React.createElement(FontAwesomeIcon, { icon: faTimes }),
3419
+ ' ',
3420
+ "Reset"))));
2979
3421
  // Filter drawer
2980
3422
  let filterDrawer;
2981
3423
  if (expandedFilterDrawer) {
2982
3424
  if (expandedFilterDrawer === FilterDrawer.Date) {
2983
3425
  filterDrawer = (React.createElement(TabBox, { title: "Date" },
2984
- 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) => {
2985
- dispatch({
2986
- type: ActionType.UpdateDateFilterState,
2987
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { startDate: { month, day, year } }),
2988
- });
3426
+ React.createElement(SimpleDateChooser, { ariaLabel: "filter start date", name: "filter-start-date", year: dateFilterState.startDate.year, month: dateFilterState.startDate.month, day: dateFilterState.startDate.day, chooseFromPast: true, numMonthsToShow: 36, onChange: (month, day, year) => {
3427
+ dateFilterState.startDate = { month, day, year };
3428
+ handleDateRangeUpdated(dateFilterState);
2989
3429
  } }),
2990
3430
  ' ',
2991
3431
  "to",
2992
3432
  ' ',
2993
- 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) => {
2994
- dispatch({
2995
- type: ActionType.UpdateDateFilterState,
2996
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { endDate: { month, day, year } }),
2997
- });
3433
+ React.createElement(SimpleDateChooser, { ariaLabel: "filter end date", name: "filter-end-date", year: dateFilterState.endDate.year, month: dateFilterState.endDate.month, day: dateFilterState.endDate.day, chooseFromPast: true, numMonthsToShow: 12, onChange: (month, day, year) => {
3434
+ if (year < dateFilterState.startDate.year
3435
+ || (year === dateFilterState.startDate.year
3436
+ && month < dateFilterState.startDate.month)
3437
+ || (year === dateFilterState.startDate.year
3438
+ && month === dateFilterState.startDate.month
3439
+ && day < dateFilterState.startDate.day)) {
3440
+ return alert$1('Invalid Start Date', 'The start date cannot be before the end date.');
3441
+ }
3442
+ dateFilterState.endDate = { month, day, year };
3443
+ handleDateRangeUpdated(dateFilterState);
2998
3444
  } })));
2999
3445
  }
3000
3446
  else if (expandedFilterDrawer === FilterDrawer.Context) {
3001
3447
  // Create item picker items
3002
- const pickableItems = (Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {})
3003
- .map((context) => {
3004
- var _a;
3005
- const value = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
3448
+ const builtInPickableItem = {
3449
+ id: '_',
3450
+ name: 'Auto-logged',
3451
+ isGroup: true,
3452
+ children: [],
3453
+ };
3454
+ const pickableItems = [];
3455
+ Object.keys(contextMap)
3456
+ .forEach((context) => {
3457
+ const value = (contextMap)[context];
3006
3458
  if (typeof value === 'string') {
3007
3459
  // No subcategories
3008
3460
  const item = {
@@ -3011,38 +3463,64 @@ const LogReviewer = (props) => {
3011
3463
  isGroup: false,
3012
3464
  checked: !!contextFilterState[context],
3013
3465
  };
3014
- return item;
3466
+ // Add built-in items to its own folder
3467
+ const isBuiltIn = context in LogBuiltInMetadata.Context;
3468
+ if (isBuiltIn) {
3469
+ // Add to built-in pickable item
3470
+ builtInPickableItem.children.push(item);
3471
+ }
3472
+ else {
3473
+ // Add to pickable items list
3474
+ pickableItems.push(item);
3475
+ }
3015
3476
  }
3016
3477
  // Has subcategories
3017
3478
  const children = (Object.keys(value)
3479
+ // Remove parent name
3018
3480
  .filter((subcontext) => {
3019
3481
  return subcontext !== '_';
3020
3482
  })
3483
+ // Create child pickable items
3021
3484
  .map((subcontext) => {
3022
3485
  return {
3023
- id: `${context}-${subcontext}`,
3486
+ id: subcontext,
3024
3487
  name: genHumanReadableName(subcontext),
3025
3488
  isGroup: false,
3026
- checked: !!value[subcontext],
3489
+ checked: contextFilterState[context][subcontext],
3027
3490
  };
3028
3491
  }));
3029
3492
  const item = {
3030
3493
  id: context,
3031
- name: context,
3494
+ name: genHumanReadableName(context),
3032
3495
  isGroup: true,
3033
3496
  children,
3034
3497
  };
3035
- return item;
3036
- }));
3498
+ pickableItems.push(item);
3499
+ });
3500
+ // Add built-in contexts to end ofl ist
3501
+ pickableItems.push(builtInPickableItem);
3037
3502
  // Create filter UI
3038
3503
  filterDrawer = (React.createElement(ItemPicker, { title: "Context", items: pickableItems, onChanged: (updatedItems) => {
3039
3504
  // Update our state
3040
3505
  updatedItems.forEach((pickableItem) => {
3041
3506
  if (pickableItem.isGroup) {
3042
3507
  // Has subcontexts
3043
- pickableItem.children.forEach((subcontextItem) => {
3044
- contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3045
- });
3508
+ if (pickableItem.id === '_') {
3509
+ // Built-in
3510
+ // Treat as if these were top-level contexts
3511
+ pickableItem.children.forEach((subcontextItem) => {
3512
+ contextFilterState[subcontextItem.id] = ('checked' in subcontextItem
3513
+ && subcontextItem.checked);
3514
+ });
3515
+ }
3516
+ else {
3517
+ // Not built-in
3518
+ pickableItem.children.forEach((subcontextItem) => {
3519
+ if (!subcontextItem.isGroup) {
3520
+ contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3521
+ }
3522
+ });
3523
+ }
3046
3524
  }
3047
3525
  else {
3048
3526
  // No subcontexts
@@ -3057,13 +3535,19 @@ const LogReviewer = (props) => {
3057
3535
  }
3058
3536
  else if (expandedFilterDrawer === FilterDrawer.Tag) {
3059
3537
  // Create filter UI
3060
- filterDrawer = (React.createElement(TabBox, { title: "Tags" }, Object.keys(tagFilterState)
3061
- .map((tag, i) => {
3062
- const description = genHumanReadableName(tag);
3063
- 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) => {
3064
- tagFilterState[tag] = checked;
3065
- } }));
3066
- })));
3538
+ filterDrawer = (React.createElement(TabBox, { title: "Tags" },
3539
+ React.createElement("div", null, "If any are selected, logs must contain at least one but not necessarily all of the selected tags."),
3540
+ Object.keys((_f = LogMetadata.Tag) !== null && _f !== void 0 ? _f : {})
3541
+ .map((tag, i) => {
3542
+ const description = genHumanReadableName(tag);
3543
+ return (React.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) => {
3544
+ tagFilterState[tag] = checked;
3545
+ dispatch({
3546
+ type: ActionType.UpdateTagFilterState,
3547
+ tagFilterState,
3548
+ });
3549
+ } }));
3550
+ })));
3067
3551
  }
3068
3552
  else if (expandedFilterDrawer === FilterDrawer.Action) {
3069
3553
  // Create filter UI
@@ -3092,34 +3576,37 @@ const LogReviewer = (props) => {
3092
3576
  }, ariaLabel: "only show error logs", selected: actionErrorFilterState.type === LogType$1.Error, noMarginOnRight: true })),
3093
3577
  (actionErrorFilterState.type === undefined
3094
3578
  || actionErrorFilterState.type === LogType$1.Action) && (React.createElement(TabBox, { title: "Action Log Details" },
3095
- React.createElement(ButtonInputGroup, { label: "Action" }, Object.keys(LogAction$1)
3096
- .map((action, i) => {
3097
- const description = genHumanReadableName(action);
3098
- 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) => {
3099
- actionErrorFilterState.action[action] = checked;
3100
- dispatch({
3101
- type: ActionType.UpdateActionErrorFilterState,
3102
- actionErrorFilterState,
3103
- });
3104
- } }));
3105
- })),
3106
- React.createElement(ButtonInputGroup, { label: "Target" }, Object.keys((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {})
3107
- .map((target, i) => {
3108
- var _a;
3109
- const description = genHumanReadableName(target);
3110
- return (React.createElement(CheckboxButton, { id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, onChanged: (checked) => {
3111
- actionErrorFilterState.target[target] = checked;
3112
- dispatch({
3113
- type: ActionType.UpdateActionErrorFilterState,
3114
- actionErrorFilterState,
3115
- });
3116
- }, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
3117
- })))),
3579
+ React.createElement(ButtonInputGroup, { label: "Action", className: "mb-2" },
3580
+ React.createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys(LogAction$1)
3581
+ .map((action) => {
3582
+ const description = genHumanReadableName(action);
3583
+ return (React.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) => {
3584
+ actionErrorFilterState.action[action] = checked;
3585
+ console.log(actionErrorFilterState, action, checked);
3586
+ dispatch({
3587
+ type: ActionType.UpdateActionErrorFilterState,
3588
+ actionErrorFilterState,
3589
+ });
3590
+ } }));
3591
+ }))),
3592
+ React.createElement(ButtonInputGroup, { label: "Target" },
3593
+ React.createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys(targetMap)
3594
+ .map((target) => {
3595
+ const description = genHumanReadableName(target);
3596
+ return (React.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) => {
3597
+ actionErrorFilterState.target[target] = checked;
3598
+ console.log(actionErrorFilterState, target, checked);
3599
+ dispatch({
3600
+ type: ActionType.UpdateActionErrorFilterState,
3601
+ actionErrorFilterState,
3602
+ });
3603
+ } }));
3604
+ }))))),
3118
3605
  (actionErrorFilterState.type === undefined
3119
3606
  || actionErrorFilterState.type === LogType$1.Error) && (React.createElement(TabBox, { title: "Error Log Details" },
3120
3607
  React.createElement("div", { className: "input-group mb-2" },
3121
3608
  React.createElement("span", { className: "input-group-text" }, "Error Message"),
3122
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for error message", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3609
+ React.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) => {
3123
3610
  actionErrorFilterState.errorMessage = e.target.value;
3124
3611
  dispatch({
3125
3612
  type: ActionType.UpdateActionErrorFilterState,
@@ -3128,7 +3615,7 @@ const LogReviewer = (props) => {
3128
3615
  } })),
3129
3616
  React.createElement("div", { className: "input-group mb-2" },
3130
3617
  React.createElement("span", { className: "input-group-text" }, "Error Code"),
3131
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3618
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorCode, placeholder: "e.g. GC22", onChange: (e) => {
3132
3619
  actionErrorFilterState.errorCode = ((e.target.value)
3133
3620
  .trim()
3134
3621
  .toUpperCase());
@@ -3144,7 +3631,7 @@ const LogReviewer = (props) => {
3144
3631
  React.createElement(TabBox, { title: "User Info" },
3145
3632
  React.createElement("div", { className: "input-group mb-2" },
3146
3633
  React.createElement("span", { className: "input-group-text" }, "User First Name"),
3147
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, onChange: (e) => {
3634
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, placeholder: "e.g. Divardo", onChange: (e) => {
3148
3635
  advancedFilterState.userFirstName = e.target.value;
3149
3636
  dispatch({
3150
3637
  type: ActionType.UpdateAdvancedFilterState,
@@ -3153,7 +3640,7 @@ const LogReviewer = (props) => {
3153
3640
  } })),
3154
3641
  React.createElement("div", { className: "input-group mb-2" },
3155
3642
  React.createElement("span", { className: "input-group-text" }, "User Last Name"),
3156
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, onChange: (e) => {
3643
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, placeholder: "e.g. Calicci", onChange: (e) => {
3157
3644
  advancedFilterState.userLastName = e.target.value;
3158
3645
  dispatch({
3159
3646
  type: ActionType.UpdateAdvancedFilterState,
@@ -3162,7 +3649,7 @@ const LogReviewer = (props) => {
3162
3649
  } })),
3163
3650
  React.createElement("div", { className: "input-group mb-2" },
3164
3651
  React.createElement("span", { className: "input-group-text" }, "User Email"),
3165
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user email", value: advancedFilterState.userEmail, onChange: (e) => {
3652
+ React.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) => {
3166
3653
  advancedFilterState.userEmail = ((e.target.value)
3167
3654
  .trim());
3168
3655
  dispatch({
@@ -3172,7 +3659,7 @@ const LogReviewer = (props) => {
3172
3659
  } })),
3173
3660
  React.createElement("div", { className: "input-group mb-2" },
3174
3661
  React.createElement("span", { className: "input-group-text" }, "User Canvas Id"),
3175
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, onChange: (e) => {
3662
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, placeholder: "e.g. 104985", onChange: (e) => {
3176
3663
  const { value } = e.target;
3177
3664
  // Only update if value contains only numbers
3178
3665
  if (/^\d+$/.test(value)) {
@@ -3209,7 +3696,7 @@ const LogReviewer = (props) => {
3209
3696
  React.createElement(TabBox, { title: "Course Info" },
3210
3697
  React.createElement("div", { className: "input-group mb-2" },
3211
3698
  React.createElement("span", { className: "input-group-text" }, "Course Name"),
3212
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, onChange: (e) => {
3699
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, placeholder: "e.g. GLC 200", onChange: (e) => {
3213
3700
  advancedFilterState.courseName = e.target.value;
3214
3701
  dispatch({
3215
3702
  type: ActionType.UpdateAdvancedFilterState,
@@ -3218,7 +3705,7 @@ const LogReviewer = (props) => {
3218
3705
  } })),
3219
3706
  React.createElement("div", { className: "input-group mb-2" },
3220
3707
  React.createElement("span", { className: "input-group-text" }, "Course Canvas Id"),
3221
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, onChange: (e) => {
3708
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, placeholder: "e.g. 15948", onChange: (e) => {
3222
3709
  const { value } = e.target;
3223
3710
  // Only update if value contains only numbers
3224
3711
  if (/^\d+$/.test(value)) {
@@ -3276,37 +3763,38 @@ const LogReviewer = (props) => {
3276
3763
  advancedFilterState,
3277
3764
  });
3278
3765
  }, noMarginOnRight: true })),
3279
- React.createElement("div", { className: "input-group mb-2" },
3280
- React.createElement("span", { className: "input-group-text" }, "Server Route Path"),
3281
- 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) => {
3282
- advancedFilterState.courseName = ((e.target.value)
3283
- .trim());
3284
- dispatch({
3285
- type: ActionType.UpdateAdvancedFilterState,
3286
- advancedFilterState,
3287
- });
3288
- } })),
3289
- React.createElement("div", { className: "input-group mb-2" },
3290
- React.createElement("span", { className: "input-group-text" }, "Server Route Template"),
3291
- 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) => {
3292
- advancedFilterState.courseName = ((e.target.value)
3293
- .trim());
3294
- dispatch({
3295
- type: ActionType.UpdateAdvancedFilterState,
3296
- advancedFilterState,
3297
- });
3298
- } })))));
3766
+ advancedFilterState.source !== LogSource$1.Client && (React.createElement("div", { className: "mt-2" },
3767
+ React.createElement("div", { className: "input-group mb-2" },
3768
+ React.createElement("span", { className: "input-group-text" }, "Server Route Path"),
3769
+ React.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) => {
3770
+ advancedFilterState.courseName = ((e.target.value)
3771
+ .trim());
3772
+ dispatch({
3773
+ type: ActionType.UpdateAdvancedFilterState,
3774
+ advancedFilterState,
3775
+ });
3776
+ } })),
3777
+ React.createElement("div", { className: "input-group mb-2" },
3778
+ React.createElement("span", { className: "input-group-text" }, "Server Route Template"),
3779
+ 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) => {
3780
+ advancedFilterState.courseName = ((e.target.value)
3781
+ .trim());
3782
+ dispatch({
3783
+ type: ActionType.UpdateAdvancedFilterState,
3784
+ advancedFilterState,
3785
+ });
3786
+ } })))))));
3299
3787
  }
3300
3788
  }
3301
3789
  // Filters UI
3302
3790
  const filters = (React.createElement(React.Fragment, null,
3303
3791
  filterToggles,
3304
- filterDrawer && (React.createElement(Drawer, null, filterDrawer))));
3792
+ filterDrawer && (React.createElement(Drawer, { customBackgroundColor: "#eee" }, filterDrawer))));
3305
3793
  // Actually filter the logs
3306
3794
  // > Perform filters
3307
3795
  const logs = [];
3308
3796
  Object.keys(logMap).forEach((year) => {
3309
- Object.keys(logMap).forEach((month) => {
3797
+ Object.keys(logMap[year]).forEach((month) => {
3310
3798
  logMap[year][month].forEach((log) => {
3311
3799
  /* ----------- Date Filter ---------- */
3312
3800
  var _a;
@@ -3342,16 +3830,23 @@ const LogReviewer = (props) => {
3342
3830
  // Whole context is deselected
3343
3831
  contextFilterState[log.context] === false
3344
3832
  // None of the subcontexts are selected
3345
- || (Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3346
- .every((isSelected) => {
3347
- return !isSelected;
3348
- }))) {
3833
+ || (
3834
+ // Has subcontexts
3835
+ typeof contextFilterState[log.context] !== 'boolean'
3836
+ // None of the subcontexts are selected
3837
+ && Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3838
+ .every((isSelected) => {
3839
+ return !isSelected;
3840
+ }))) {
3841
+ console.log(log.context, contextFilterState);
3349
3842
  return;
3350
3843
  }
3351
3844
  // Subcontext doesn't match
3352
3845
  if (
3353
- // Log has a subcontext
3354
- log.subcontext
3846
+ // Log context is not "uncategorized" (no point in further filters)
3847
+ log.context !== LogBuiltInMetadata.Context.Uncategorized
3848
+ // Log has a subcontext
3849
+ && log.subcontext
3355
3850
  // Context has subcontexts
3356
3851
  && (contextFilterState[log.context]
3357
3852
  && contextFilterState[log.context] !== false
@@ -3363,12 +3858,16 @@ const LogReviewer = (props) => {
3363
3858
  /* -------------- Tags -------------- */
3364
3859
  // No tags match
3365
3860
  if (
3366
- // Log has at least one tag
3367
- log.tags.length > 0
3861
+ // At least one tag is required
3862
+ Object.values(tagFilterState)
3863
+ .filter((isSelected) => {
3864
+ return isSelected;
3865
+ })
3866
+ .length > 0
3368
3867
  // No tags match
3369
- && (log.tags.every((tag) => {
3868
+ && log.tags.every((tag) => {
3370
3869
  return !tagFilterState[tag];
3371
- }))) {
3870
+ })) {
3372
3871
  return;
3373
3872
  }
3374
3873
  /* ------- Actions and Errors ------- */
@@ -3502,7 +4001,7 @@ const LogReviewer = (props) => {
3502
4001
  // Device info exists
3503
4002
  && log.device
3504
4003
  // Mobile filter doesn't match
3505
- && (advancedFilterState.isMobile === log.device.isMobile)) {
4004
+ && (advancedFilterState.isMobile !== log.device.isMobile)) {
3506
4005
  return;
3507
4006
  }
3508
4007
  // Log source doesn't match
@@ -3544,199 +4043,20 @@ const LogReviewer = (props) => {
3544
4043
  /*----------------------------------------*/
3545
4044
  /* Data */
3546
4045
  /*----------------------------------------*/
3547
- // Create data table
3548
- const columns = [
3549
- {
3550
- title: 'First Name',
3551
- param: 'userFirstName',
3552
- type: ParamType$1.String,
3553
- },
3554
- {
3555
- title: 'Last Name',
3556
- param: 'userLastName',
3557
- type: ParamType$1.String,
3558
- },
3559
- {
3560
- title: 'Email',
3561
- param: 'userEmail',
3562
- type: ParamType$1.String,
3563
- },
3564
- {
3565
- title: 'Canvas Id',
3566
- param: 'userId',
3567
- type: ParamType$1.Int,
3568
- },
3569
- {
3570
- title: 'Student',
3571
- param: 'isLearner',
3572
- type: ParamType$1.Boolean,
3573
- },
3574
- {
3575
- title: 'Teaching Staff',
3576
- param: 'isTTM',
3577
- type: ParamType$1.Boolean,
3578
- startsHidden: true,
3579
- },
3580
- {
3581
- title: 'Admin',
3582
- param: 'isAdmin',
3583
- type: ParamType$1.Boolean,
3584
- startsHidden: true,
3585
- },
3586
- {
3587
- title: 'Course Canvas Id',
3588
- param: 'courseId',
3589
- type: ParamType$1.Int,
3590
- startsHidden: true,
3591
- },
3592
- {
3593
- title: 'Course Name',
3594
- param: 'courseName',
3595
- type: ParamType$1.String,
3596
- },
3597
- {
3598
- title: 'Browser Name',
3599
- param: 'browser.name',
3600
- type: ParamType$1.String,
3601
- startsHidden: true,
3602
- },
3603
- {
3604
- title: 'Browser Version',
3605
- param: 'browser.version',
3606
- type: ParamType$1.String,
3607
- startsHidden: true,
3608
- },
3609
- {
3610
- title: 'OS',
3611
- param: 'device.os',
3612
- type: ParamType$1.String,
3613
- startsHidden: true,
3614
- },
3615
- {
3616
- title: 'Mobile',
3617
- param: 'device.isMobile',
3618
- type: ParamType$1.Boolean,
3619
- startsHidden: true,
3620
- },
3621
- {
3622
- title: 'Year',
3623
- param: 'year',
3624
- type: ParamType$1.Int,
3625
- },
3626
- {
3627
- title: 'Month',
3628
- param: 'month',
3629
- type: ParamType$1.Int,
3630
- },
3631
- {
3632
- title: 'Day',
3633
- param: 'day',
3634
- type: ParamType$1.Int,
3635
- },
3636
- {
3637
- title: 'Hour',
3638
- param: 'hour',
3639
- type: ParamType$1.Int,
3640
- },
3641
- {
3642
- title: 'Minute',
3643
- param: 'minute',
3644
- type: ParamType$1.Int,
3645
- startsHidden: true,
3646
- },
3647
- {
3648
- title: 'Timestamp',
3649
- param: 'timestamp',
3650
- type: ParamType$1.Int,
3651
- startsHidden: true,
3652
- },
3653
- {
3654
- title: 'Context',
3655
- param: 'context',
3656
- type: ParamType$1.String,
3657
- },
3658
- {
3659
- title: 'Subcontext',
3660
- param: 'subcontext',
3661
- type: ParamType$1.String,
3662
- },
3663
- {
3664
- title: 'Tags',
3665
- param: 'tags',
3666
- type: ParamType$1.JSON,
3667
- startsHidden: true,
3668
- },
3669
- {
3670
- title: 'Log Level',
3671
- param: 'level',
3672
- type: ParamType$1.String,
3673
- startsHidden: true,
3674
- },
3675
- {
3676
- title: 'Metadata',
3677
- param: 'metadata',
3678
- type: ParamType$1.JSON,
3679
- startsHidden: true,
3680
- },
3681
- {
3682
- title: 'Source',
3683
- param: 'source',
3684
- type: ParamType$1.String,
3685
- },
3686
- {
3687
- title: 'Server Route Path',
3688
- param: 'routePath',
3689
- type: ParamType$1.String,
3690
- startsHidden: true,
3691
- },
3692
- {
3693
- title: 'Server Route Template',
3694
- param: 'routeTemplate',
3695
- type: ParamType$1.String,
3696
- startsHidden: true,
3697
- },
3698
- {
3699
- title: 'Type',
3700
- param: 'type',
3701
- type: ParamType$1.String,
3702
- },
3703
- {
3704
- title: 'Error Message',
3705
- param: 'errorMessage',
3706
- type: ParamType$1.String,
3707
- startsHidden: true,
3708
- },
3709
- {
3710
- title: 'Error Code',
3711
- param: 'errorCode',
3712
- type: ParamType$1.String,
3713
- startsHidden: true,
3714
- },
3715
- {
3716
- title: 'Error Stack',
3717
- param: 'errorStack',
3718
- type: ParamType$1.String,
3719
- startsHidden: true,
3720
- },
3721
- {
3722
- title: 'Action Target',
3723
- param: 'target',
3724
- type: ParamType$1.String,
3725
- startsHidden: true,
3726
- },
3727
- {
3728
- title: 'Action Type',
3729
- param: 'action',
3730
- type: ParamType$1.String,
3731
- startsHidden: true,
3732
- },
3733
- ];
4046
+ // Nothing to show notice
4047
+ const noLogsNotice = (logs.length === 0
4048
+ ? (React.createElement("div", { className: "alert alert-warning text-center mt-2" },
4049
+ React.createElement("h4", { className: "m-1" }, "No Logs to Show"),
4050
+ React.createElement("div", null, "Either your filters are too strict or no matching logs have been created yet.")))
4051
+ : undefined);
3734
4052
  // Create intelliTable
3735
- const dataTable = (React.createElement(IntelliTable, { title: "Matching Logs", id: "logs", data: logs, columns: columns }));
4053
+ const dataTable = (React.createElement(IntelliTable, { title: "Matching Logs:", csvName: `Logs from ${getHumanReadableDate()}`, id: "logs", data: logs, columns: columns }));
3736
4054
  // Main body
3737
4055
  body = (React.createElement(React.Fragment, null,
3738
4056
  filters,
3739
- dataTable));
4057
+ React.createElement("div", { className: "mt-2" },
4058
+ dataTable,
4059
+ noLogsNotice)));
3740
4060
  }
3741
4061
  /* ---------- Wrap in Modal --------- */
3742
4062
  return (React.createElement("div", { className: "LogReviewer-outer-container" },
@@ -3744,14 +4064,10 @@ const LogReviewer = (props) => {
3744
4064
  React.createElement("div", { className: "LogReviewer-inner-container" },
3745
4065
  React.createElement("div", { className: "LogReviewer-header" },
3746
4066
  React.createElement("div", { className: "LogReviewer-header-title" },
3747
- React.createElement("h1", { className: "m-0" }, "Log Review Dashboard")),
3748
- React.createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-lg", "aria-label": "close log reviewer panel", onClick: onClose, style: {
3749
- border: 0,
3750
- backgroundColor: 'transparent',
3751
- padding: 0,
3752
- margin: 0,
3753
- } },
3754
- React.createElement(FontAwesomeIcon, { icon: faTimes }))),
4067
+ React.createElement("h3", { className: "text-center m-0" }, "Log Review Dashboard")),
4068
+ React.createElement("div", { style: { width: 0 } },
4069
+ React.createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-dark btn-lg pe-0", "aria-label": "close log reviewer panel", onClick: onClose },
4070
+ React.createElement(FontAwesomeIcon, { icon: faTimes })))),
3755
4071
  React.createElement("div", { className: "LogReviewer-contents" }, body))));
3756
4072
  };
3757
4073
 
@@ -4082,15 +4398,17 @@ const initServer = (opts) => {
4082
4398
  },
4083
4399
  handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
4084
4400
  // Get user info
4085
- const { userId, isAdmin } = params;
4401
+ const { year, month, userId, isAdmin, } = params;
4086
4402
  // Validate user
4087
- // isAdmin is already checked because path starts with '/admin'
4088
4403
  const canReview = yield canReviewLogs(userId, isAdmin);
4089
4404
  if (!canReview) {
4090
4405
  throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
4091
4406
  }
4092
4407
  // Query for logs
4093
- const logs = yield _logCollection.find({ userId });
4408
+ const logs = yield _logCollection.find({
4409
+ year,
4410
+ month,
4411
+ });
4094
4412
  // Return logs
4095
4413
  return logs;
4096
4414
  }),
@@ -4739,7 +5057,7 @@ const genRouteHandler = (opts) => {
4739
5057
  isAdmin: !!launchInfo.isAdmin,
4740
5058
  isTTM: !!launchInfo.isTTM,
4741
5059
  courseId: launchInfo.courseId,
4742
- courseName: launchInfo.courseName,
5060
+ courseName: launchInfo.contextLabel,
4743
5061
  browser,
4744
5062
  device,
4745
5063
  year,
@@ -4766,7 +5084,7 @@ const genRouteHandler = (opts) => {
4766
5084
  }
4767
5085
  : {
4768
5086
  type: LogType$1.Action,
4769
- target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoSpecificTarget),
5087
+ target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoTarget),
4770
5088
  action: ((_o = opts.action) !== null && _o !== void 0 ? _o : LogAction$1.Unknown),
4771
5089
  });
4772
5090
  // Source-specific info
@@ -4979,29 +5297,6 @@ const startMinWait = (minWaitMs) => {
4979
5297
  });
4980
5298
  };
4981
5299
 
4982
- // Import shared helpers
4983
- /**
4984
- * Get a human-readable description of a date (all in ET)
4985
- * @author Gabe Abrams
4986
- * @param [dateOrTimestamp=today] the date or timestamp for the date to describe
4987
- * @returns human-readable description of the date
4988
- */
4989
- const getHumanReadableDate = (dateOrTimestamp) => {
4990
- // Get the date info
4991
- const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
4992
- const currYear = getTimeInfoInET().year;
4993
- // Get the short month description
4994
- const monthName = getMonthName(month).short;
4995
- // Create start of description
4996
- let description = `${monthName} ${day}${getOrdinal(day)}`;
4997
- // Add on year if it's different
4998
- if (year !== currYear) {
4999
- description += ` ${year}`;
5000
- }
5001
- // Return description
5002
- return description;
5003
- };
5004
-
5005
5300
  /**
5006
5301
  * Get the current part of day (morning, evening, etc.)
5007
5302
  * @author Gabe Abrams