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