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