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