dce-reactkit 3.2.1 → 3.2.2-beta.2
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 +2106 -416
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/components/CSVDownloadButton.d.ts +19 -0
- package/dist/cjs/types/components/IntelliTable.d.ts +17 -0
- package/dist/cjs/types/components/LogReviewer.d.ts +13 -0
- package/dist/cjs/types/components/SimpleDateChooser.d.ts +1 -0
- package/dist/cjs/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
- package/dist/cjs/types/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
- package/dist/cjs/types/helpers/canReviewLogs.d.ts +7 -0
- package/dist/cjs/types/helpers/genCSV.d.ts +14 -0
- package/dist/cjs/types/helpers/getMonthName.d.ts +14 -0
- package/dist/cjs/types/index.d.ts +8 -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/LogBuiltInMetadata.d.ts +1 -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 +2103 -418
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/components/CSVDownloadButton.d.ts +19 -0
- package/dist/esm/types/components/IntelliTable.d.ts +17 -0
- package/dist/esm/types/components/LogReviewer.d.ts +13 -0
- package/dist/esm/types/components/SimpleDateChooser.d.ts +1 -0
- package/dist/esm/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
- package/dist/esm/types/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
- package/dist/esm/types/helpers/canReviewLogs.d.ts +7 -0
- package/dist/esm/types/helpers/genCSV.d.ts +14 -0
- package/dist/esm/types/helpers/getMonthName.d.ts +14 -0
- package/dist/esm/types/index.d.ts +8 -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/LogBuiltInMetadata.d.ts +1 -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 +163 -47
- package/package.json +1 -1
- package/sandbox.js +78 -17
- package/src/components/AppWrapper.tsx +15 -0
- package/src/components/CSVDownloadButton.tsx +102 -0
- package/src/components/IntelliTable.tsx +610 -0
- package/src/components/LogReviewer.tsx +2038 -0
- package/src/components/SimpleDateChooser.tsx +26 -27
- package/src/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.ts +9 -0
- package/src/constants/LOG_REVIEW_STATUS_ROUTE.ts +10 -0
- package/src/helpers/canReviewLogs.ts +33 -0
- package/src/helpers/genCSV.ts +59 -0
- package/src/helpers/getHumanReadableDate.ts +6 -17
- package/src/helpers/getMonthName.ts +29 -0
- package/src/index.ts +14 -0
- package/src/server/initServer.ts +105 -3
- package/src/types/IntelliTableColumn.ts +24 -0
- package/src/types/LogBuiltInMetadata.ts +1 -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* () {
|
|
@@ -491,6 +492,184 @@ class ErrorWithCode extends Error {
|
|
|
491
492
|
}
|
|
492
493
|
}
|
|
493
494
|
|
|
495
|
+
/**
|
|
496
|
+
* Path that all routes start with
|
|
497
|
+
* @author Gabe Abrams
|
|
498
|
+
*/
|
|
499
|
+
const ROUTE_PATH_PREFIX = '/dce-reactkit';
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Path of the route for storing client-side logs
|
|
503
|
+
* @author Gabe Abrams
|
|
504
|
+
*/
|
|
505
|
+
const LOG_ROUTE_PATH = `${ROUTE_PATH_PREFIX}/log`;
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Built-in metadata for logs
|
|
509
|
+
* @author Gabe Abrams
|
|
510
|
+
*/
|
|
511
|
+
const LogBuiltInMetadata = {
|
|
512
|
+
// Contexts
|
|
513
|
+
Context: {
|
|
514
|
+
Uncategorized: 'n/a',
|
|
515
|
+
ServerRenderedErrorPage: '_server-rendered-error-page',
|
|
516
|
+
ServerEndpointError: '_server-endpoint-error',
|
|
517
|
+
ClientFatalError: '_client-fatal-error',
|
|
518
|
+
},
|
|
519
|
+
// Targets
|
|
520
|
+
Target: {
|
|
521
|
+
NoSpecificTarget: 'n/a',
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// Keep track of whether or not session expiry has already been handled
|
|
526
|
+
let sessionAlreadyExpired = false;
|
|
527
|
+
/*------------------------------------------------------------------------*/
|
|
528
|
+
/* Stub Logic */
|
|
529
|
+
/*------------------------------------------------------------------------*/
|
|
530
|
+
// Stored stub responses
|
|
531
|
+
const stubResponses = {};
|
|
532
|
+
/**
|
|
533
|
+
* Add a stub response
|
|
534
|
+
* @author Gabe Abrams
|
|
535
|
+
* @param opts object containing all arguments
|
|
536
|
+
* @param [opts.method=GET] http request method
|
|
537
|
+
* @param opts.path pathname of the request
|
|
538
|
+
* @param [opts.body] body of the response if successful
|
|
539
|
+
* @param [opts.errorMessage] error message if not successful
|
|
540
|
+
* @param [opts.errorCode] error code if not successful
|
|
541
|
+
*/
|
|
542
|
+
const _setStubResponse = (opts) => {
|
|
543
|
+
var _a, _b, _c;
|
|
544
|
+
const { path, body, } = opts;
|
|
545
|
+
const method = ((_a = opts.method) !== null && _a !== void 0 ? _a : 'GET').toUpperCase();
|
|
546
|
+
const errorMessage = ((_b = opts.errorMessage) !== null && _b !== void 0 ? _b : 'An unknown error has occurred.');
|
|
547
|
+
const errorCode = ((_c = opts.errorCode) !== null && _c !== void 0 ? _c : ReactKitErrorCode$1.NoCode);
|
|
548
|
+
// Store to stub responses
|
|
549
|
+
if (!stubResponses[method]) {
|
|
550
|
+
stubResponses[method] = {};
|
|
551
|
+
}
|
|
552
|
+
stubResponses[method][path] = ((opts.errorMessage || opts.errorCode)
|
|
553
|
+
? {
|
|
554
|
+
success: false,
|
|
555
|
+
errorMessage,
|
|
556
|
+
errorCode,
|
|
557
|
+
}
|
|
558
|
+
: {
|
|
559
|
+
success: true,
|
|
560
|
+
body: body !== null && body !== void 0 ? body : undefined,
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
/*------------------------------------------------------------------------*/
|
|
564
|
+
/* Main */
|
|
565
|
+
/*------------------------------------------------------------------------*/
|
|
566
|
+
/**
|
|
567
|
+
* Visit an endpoint on the server [for client only]
|
|
568
|
+
* @author Gabe Abrams
|
|
569
|
+
* @param opts object containing all arguments
|
|
570
|
+
* @param opts.path - the path of the server endpoint
|
|
571
|
+
* @param [opts.method=GET] - the method of the endpoint
|
|
572
|
+
* @param [opts.params] - query/body parameters to include
|
|
573
|
+
* @returns response from server
|
|
574
|
+
*/
|
|
575
|
+
const visitServerEndpoint = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
576
|
+
var _a, _b, _c;
|
|
577
|
+
const method = ((_a = opts.method) !== null && _a !== void 0 ? _a : 'GET');
|
|
578
|
+
// Handle stubs
|
|
579
|
+
const stubResponse = (_b = stubResponses[method]) === null || _b === void 0 ? void 0 : _b[opts.path];
|
|
580
|
+
if (stubResponse) {
|
|
581
|
+
// Remove from list
|
|
582
|
+
try {
|
|
583
|
+
stubResponses[method][opts.path] = undefined;
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
// Ignore
|
|
587
|
+
}
|
|
588
|
+
// Success
|
|
589
|
+
if (stubResponse.success) {
|
|
590
|
+
return stubResponse.body;
|
|
591
|
+
}
|
|
592
|
+
// Error
|
|
593
|
+
throw new ErrorWithCode(stubResponse.errorMessage, stubResponse.errorCode);
|
|
594
|
+
}
|
|
595
|
+
// Send the request
|
|
596
|
+
const response = yield cacclSendRequest({
|
|
597
|
+
path: opts.path,
|
|
598
|
+
method: (_c = opts.method) !== null && _c !== void 0 ? _c : 'GET',
|
|
599
|
+
params: opts.params,
|
|
600
|
+
});
|
|
601
|
+
// Check for failure
|
|
602
|
+
if (!response || !response.body) {
|
|
603
|
+
throw new ErrorWithCode('We didn\'t get a response from the server. Please check your internet connection.', ReactKitErrorCode$1.NoResponse);
|
|
604
|
+
}
|
|
605
|
+
if (!response.body.success) {
|
|
606
|
+
// Session expired
|
|
607
|
+
if (response.body.code === ReactKitErrorCode$1.SessionExpired) {
|
|
608
|
+
// Skip notice if session was already expired
|
|
609
|
+
if (sessionAlreadyExpired) {
|
|
610
|
+
// Never return (browser is already reloading)
|
|
611
|
+
yield new Promise(() => {
|
|
612
|
+
// Promise that never returns
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
sessionAlreadyExpired = true;
|
|
616
|
+
// Show session expiration message
|
|
617
|
+
{
|
|
618
|
+
// Fallback to alert
|
|
619
|
+
// eslint-disable-next-line no-alert
|
|
620
|
+
alert('Your session has expired. Please start over.');
|
|
621
|
+
}
|
|
622
|
+
// Never return (don't continue execution)
|
|
623
|
+
yield new Promise(() => {
|
|
624
|
+
// Promise that never returns
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
// Other errors
|
|
628
|
+
throw new ErrorWithCode((response.body.message
|
|
629
|
+
|| 'An unknown error occurred. Please contact an admin.'), (response.body.code
|
|
630
|
+
|| ReactKitErrorCode$1.NoCode));
|
|
631
|
+
}
|
|
632
|
+
// Success! Extract the body
|
|
633
|
+
const { body } = response.body;
|
|
634
|
+
// Return
|
|
635
|
+
return body;
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Log a user action on the client (cannot be used on the server)
|
|
640
|
+
* @author Gabe Abrams
|
|
641
|
+
*/
|
|
642
|
+
const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
643
|
+
var _a, _b, _c, _d, _e, _f;
|
|
644
|
+
return visitServerEndpoint({
|
|
645
|
+
path: LOG_ROUTE_PATH,
|
|
646
|
+
method: 'POST',
|
|
647
|
+
params: {
|
|
648
|
+
context: (typeof opts.context === 'string'
|
|
649
|
+
? opts.context
|
|
650
|
+
: ((_b = ((_a = opts.context) !== null && _a !== void 0 ? _a : {})._) !== null && _b !== void 0 ? _b : LogBuiltInMetadata.Context.Uncategorized)),
|
|
651
|
+
subcontext: ((_c = opts.subcontext) !== null && _c !== void 0 ? _c : LogBuiltInMetadata.Context.Uncategorized),
|
|
652
|
+
tags: JSON.stringify((_d = opts.tags) !== null && _d !== void 0 ? _d : []),
|
|
653
|
+
metadata: JSON.stringify((_e = opts.metadata) !== null && _e !== void 0 ? _e : {}),
|
|
654
|
+
errorMessage: (opts.error
|
|
655
|
+
? opts.error.message
|
|
656
|
+
: undefined),
|
|
657
|
+
errorCode: (opts.error
|
|
658
|
+
? opts.error.code
|
|
659
|
+
: undefined),
|
|
660
|
+
errorStack: (opts.error
|
|
661
|
+
? opts.error.stack
|
|
662
|
+
: undefined),
|
|
663
|
+
target: (opts.action
|
|
664
|
+
? ((_f = opts.target) !== null && _f !== void 0 ? _f : LogBuiltInMetadata.Target.NoSpecificTarget)
|
|
665
|
+
: undefined),
|
|
666
|
+
action: (opts.action
|
|
667
|
+
? opts.action
|
|
668
|
+
: undefined),
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
494
673
|
/**
|
|
495
674
|
* A wrapper for the entire React app that adds global functionality like
|
|
496
675
|
* handling for fatal error messages, adds bootstrap support
|
|
@@ -610,6 +789,18 @@ const showFatalError = (error, errorTitle = 'An Error Occurred') => {
|
|
|
610
789
|
const code = (typeof error === 'string'
|
|
611
790
|
? ReactKitErrorCode$1.NoCode
|
|
612
791
|
: String((_b = error.code) !== null && _b !== void 0 ? _b : ReactKitErrorCode$1.NoCode));
|
|
792
|
+
// Add log
|
|
793
|
+
logClientEvent({
|
|
794
|
+
context: LogBuiltInMetadata.Context.ClientFatalError,
|
|
795
|
+
error: {
|
|
796
|
+
message,
|
|
797
|
+
code,
|
|
798
|
+
stack: (error !== null && error !== void 0 ? error : {}).stack,
|
|
799
|
+
},
|
|
800
|
+
metadata: {
|
|
801
|
+
errorTitle,
|
|
802
|
+
},
|
|
803
|
+
});
|
|
613
804
|
// Handle case where app hasn't loaded
|
|
614
805
|
if (!setFatalErrorMessage || !setFatalErrorCode) {
|
|
615
806
|
alert$1(errorTitle, `${message} (code: ${code}). Please contact support.`);
|
|
@@ -719,7 +910,7 @@ const AppWrapper = (props) => {
|
|
|
719
910
|
/*------------------------------------------------------------------------*/
|
|
720
911
|
/* Style */
|
|
721
912
|
/*------------------------------------------------------------------------*/
|
|
722
|
-
const style$
|
|
913
|
+
const style$6 = `
|
|
723
914
|
/* Container fades in */
|
|
724
915
|
.LoadingSpinner-container {
|
|
725
916
|
animation-name: LoadingSpinner-container-fade-in;
|
|
@@ -797,7 +988,7 @@ const LoadingSpinner = () => {
|
|
|
797
988
|
/*------------------------------------------------------------------------*/
|
|
798
989
|
// Add all four blips to a container
|
|
799
990
|
return (React.createElement("div", { className: "text-center LoadingSpinner LoadingSpinner-container" },
|
|
800
|
-
React.createElement("style", null, style$
|
|
991
|
+
React.createElement("style", null, style$6),
|
|
801
992
|
React.createElement(FontAwesomeIcon, { icon: faCircle, className: "LoadingSpinner-blip-1 me-1" }),
|
|
802
993
|
React.createElement(FontAwesomeIcon, { icon: faCircle, className: "LoadingSpinner-blip-2 me-1" }),
|
|
803
994
|
React.createElement(FontAwesomeIcon, { icon: faCircle, className: "LoadingSpinner-blip-3 me-1" }),
|
|
@@ -811,7 +1002,7 @@ const LoadingSpinner = () => {
|
|
|
811
1002
|
/*------------------------------------------------------------------------*/
|
|
812
1003
|
/* Style */
|
|
813
1004
|
/*------------------------------------------------------------------------*/
|
|
814
|
-
const style$
|
|
1005
|
+
const style$5 = `
|
|
815
1006
|
/* Tab Box */
|
|
816
1007
|
.TabBox-box {
|
|
817
1008
|
/* Light Border */
|
|
@@ -891,7 +1082,7 @@ const TabBox = (props) => {
|
|
|
891
1082
|
/*----------------------------------------*/
|
|
892
1083
|
// Full UI
|
|
893
1084
|
return (React.createElement("div", { className: `TabBox-container ${noBottomMargin ? '' : 'mb-2'}` },
|
|
894
|
-
React.createElement("style", null, style$
|
|
1085
|
+
React.createElement("style", null, style$5),
|
|
895
1086
|
React.createElement("div", { className: "TabBox-title-container" },
|
|
896
1087
|
React.createElement("div", { className: "TabBox-title" }, title)),
|
|
897
1088
|
React.createElement("div", { className: `TabBox-box ps-2 pt-2 pe-2 ${noBottomPadding ? '' : 'pb-2'}` },
|
|
@@ -998,6 +1189,34 @@ const ButtonInputGroup = (props) => {
|
|
|
998
1189
|
} }, children))));
|
|
999
1190
|
};
|
|
1000
1191
|
|
|
1192
|
+
const monthNames = [
|
|
1193
|
+
{ short: 'Jan', full: 'January' },
|
|
1194
|
+
{ short: 'Feb', full: 'February' },
|
|
1195
|
+
{ short: 'Mar', full: 'March' },
|
|
1196
|
+
{ short: 'Apr', full: 'April' },
|
|
1197
|
+
{ short: 'May', full: 'May' },
|
|
1198
|
+
{ short: 'Jun', full: 'June' },
|
|
1199
|
+
{ short: 'Jul', full: 'July' },
|
|
1200
|
+
{ short: 'Aug', full: 'August' },
|
|
1201
|
+
{ short: 'Sep', full: 'September' },
|
|
1202
|
+
{ short: 'Oct', full: 'October' },
|
|
1203
|
+
{ short: 'Nov', full: 'November' },
|
|
1204
|
+
{ short: 'Dec', full: 'December' },
|
|
1205
|
+
];
|
|
1206
|
+
/**
|
|
1207
|
+
* Get the name of a month given the month number (1 = January, etc.)
|
|
1208
|
+
* If an invalid number is provided, we will treat it like January
|
|
1209
|
+
* @author Gabe Abrams
|
|
1210
|
+
* @param month the number of the month
|
|
1211
|
+
* @returns object containing multiple month name formats:
|
|
1212
|
+
* { short, full } where short will look like "Jan" and full will look like
|
|
1213
|
+
* "January"
|
|
1214
|
+
*/
|
|
1215
|
+
const getMonthName = (month) => {
|
|
1216
|
+
var _a;
|
|
1217
|
+
return ((_a = monthNames[month - 1]) !== null && _a !== void 0 ? _a : monthNames[0]);
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1001
1220
|
const ORDINALS = ['th', 'st', 'nd', 'rd'];
|
|
1002
1221
|
/**
|
|
1003
1222
|
* Get a number's ordinal
|
|
@@ -1067,29 +1286,23 @@ const getTimeInfoInET = (dateOrTimestamp) => {
|
|
|
1067
1286
|
};
|
|
1068
1287
|
};
|
|
1069
1288
|
|
|
1289
|
+
/**
|
|
1290
|
+
* Force a number to stay within specific bounds
|
|
1291
|
+
* @author Gabe Abrams
|
|
1292
|
+
* @param num the number to move into the bounds
|
|
1293
|
+
* @param min the minimum number in the bound
|
|
1294
|
+
* @param max the maximum number in the bound
|
|
1295
|
+
* @returns bounded number
|
|
1296
|
+
*/
|
|
1297
|
+
const forceNumIntoBounds = (num, min, max) => {
|
|
1298
|
+
return Math.max(min, Math.min(max, num));
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1070
1301
|
/**
|
|
1071
1302
|
* A very simple, lightweight date chooser
|
|
1072
1303
|
* @author Gabe Abrams
|
|
1073
1304
|
*/
|
|
1074
1305
|
/*------------------------------------------------------------------------*/
|
|
1075
|
-
/* Constants */
|
|
1076
|
-
/*------------------------------------------------------------------------*/
|
|
1077
|
-
// Constants
|
|
1078
|
-
const MONTH_NAMES = [
|
|
1079
|
-
'January',
|
|
1080
|
-
'February',
|
|
1081
|
-
'March',
|
|
1082
|
-
'April',
|
|
1083
|
-
'May',
|
|
1084
|
-
'June',
|
|
1085
|
-
'July',
|
|
1086
|
-
'August',
|
|
1087
|
-
'September',
|
|
1088
|
-
'October',
|
|
1089
|
-
'November',
|
|
1090
|
-
'December',
|
|
1091
|
-
];
|
|
1092
|
-
/*------------------------------------------------------------------------*/
|
|
1093
1306
|
/* Component */
|
|
1094
1307
|
/*------------------------------------------------------------------------*/
|
|
1095
1308
|
const SimpleDateChooser = (props) => {
|
|
@@ -1098,8 +1311,8 @@ const SimpleDateChooser = (props) => {
|
|
|
1098
1311
|
/*------------------------------------------------------------------------*/
|
|
1099
1312
|
var _a;
|
|
1100
1313
|
/* -------------- Props ------------- */
|
|
1101
|
-
const { ariaLabel, name, month, day, year, onChange, } = props;
|
|
1102
|
-
|
|
1314
|
+
const { ariaLabel, name, month, day, year, onChange, chooseFromPast, } = props;
|
|
1315
|
+
let numMonthsToShow = forceNumIntoBounds(((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6), 1, 12);
|
|
1103
1316
|
/*------------------------------------------------------------------------*/
|
|
1104
1317
|
/* Render */
|
|
1105
1318
|
/*------------------------------------------------------------------------*/
|
|
@@ -1109,16 +1322,25 @@ const SimpleDateChooser = (props) => {
|
|
|
1109
1322
|
// Determine the set of choices allowed
|
|
1110
1323
|
const today = getTimeInfoInET();
|
|
1111
1324
|
const choices = [];
|
|
1325
|
+
let startYear = today.year;
|
|
1326
|
+
let startMonth = today.month;
|
|
1327
|
+
if (chooseFromPast) {
|
|
1328
|
+
startMonth -= Math.max(0, numMonthsToShow - 1);
|
|
1329
|
+
if (startMonth <= 0) {
|
|
1330
|
+
startMonth += 12;
|
|
1331
|
+
startYear -= 1;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1112
1334
|
for (let i = 0; i < numMonthsToShow; i++) {
|
|
1113
1335
|
// Get month and year info
|
|
1114
|
-
const unmoddedMonth = (
|
|
1336
|
+
const unmoddedMonth = (startMonth + i);
|
|
1115
1337
|
const month = (unmoddedMonth > 12
|
|
1116
1338
|
? unmoddedMonth - 12
|
|
1117
1339
|
: unmoddedMonth);
|
|
1118
|
-
const monthName =
|
|
1340
|
+
const monthName = getMonthName(month).full;
|
|
1119
1341
|
const year = (unmoddedMonth > 12
|
|
1120
|
-
?
|
|
1121
|
-
:
|
|
1342
|
+
? startYear + 1
|
|
1343
|
+
: startYear);
|
|
1122
1344
|
// Figure out which days are allowed
|
|
1123
1345
|
const days = [];
|
|
1124
1346
|
const numDaysInMonth = (new Date(year, month, 0)).getDate();
|
|
@@ -1174,7 +1396,7 @@ const SimpleDateChooser = (props) => {
|
|
|
1174
1396
|
/*------------------------------------------------------------------------*/
|
|
1175
1397
|
/* Style */
|
|
1176
1398
|
/*------------------------------------------------------------------------*/
|
|
1177
|
-
const style$
|
|
1399
|
+
const style$4 = `
|
|
1178
1400
|
.Drawer-container {
|
|
1179
1401
|
margin-left: 1rem;
|
|
1180
1402
|
margin-right: 1rem;
|
|
@@ -1206,7 +1428,7 @@ const Drawer = (props) => {
|
|
|
1206
1428
|
/* Main UI */
|
|
1207
1429
|
/*----------------------------------------*/
|
|
1208
1430
|
return (React.createElement("div", { className: "Drawer-container" },
|
|
1209
|
-
React.createElement("style", null, style$
|
|
1431
|
+
React.createElement("style", null, style$4),
|
|
1210
1432
|
children));
|
|
1211
1433
|
};
|
|
1212
1434
|
|
|
@@ -1217,7 +1439,7 @@ const Drawer = (props) => {
|
|
|
1217
1439
|
/*------------------------------------------------------------------------*/
|
|
1218
1440
|
/* Style */
|
|
1219
1441
|
/*------------------------------------------------------------------------*/
|
|
1220
|
-
const style$
|
|
1442
|
+
const style$3 = `
|
|
1221
1443
|
.PopSuccessMark-outer-container {
|
|
1222
1444
|
position: relative;
|
|
1223
1445
|
display: inline-block;
|
|
@@ -1322,7 +1544,7 @@ const PopSuccessMark = (props) => {
|
|
|
1322
1544
|
width: `${sizeRem}rem`,
|
|
1323
1545
|
height: `${sizeRem}rem`,
|
|
1324
1546
|
}, "aria-label": "checkmark indicating success" },
|
|
1325
|
-
React.createElement("style", null, style$
|
|
1547
|
+
React.createElement("style", null, style$3),
|
|
1326
1548
|
React.createElement("div", { className: `PopSuccessMark-check-stroke-1 bg-${checkVariant}`, style: {
|
|
1327
1549
|
borderRadius: `${sizeRem / 5}rem`,
|
|
1328
1550
|
} }),
|
|
@@ -1338,7 +1560,7 @@ const PopSuccessMark = (props) => {
|
|
|
1338
1560
|
/*------------------------------------------------------------------------*/
|
|
1339
1561
|
/* Style */
|
|
1340
1562
|
/*------------------------------------------------------------------------*/
|
|
1341
|
-
const style$
|
|
1563
|
+
const style$2 = `
|
|
1342
1564
|
.PopFailureMark-outer-container {
|
|
1343
1565
|
position: relative;
|
|
1344
1566
|
display: inline-block;
|
|
@@ -1442,7 +1664,7 @@ const PopFailureMark = (props) => {
|
|
|
1442
1664
|
width: `${sizeRem}rem`,
|
|
1443
1665
|
height: `${sizeRem}rem`,
|
|
1444
1666
|
}, "aria-label": "mark indicating failure" },
|
|
1445
|
-
React.createElement("style", null, style$
|
|
1667
|
+
React.createElement("style", null, style$2),
|
|
1446
1668
|
React.createElement("div", { className: `PopFailureMark-x-stroke-1 bg-${xVariant}`, style: {
|
|
1447
1669
|
borderRadius: `${sizeRem / 5}rem`,
|
|
1448
1670
|
} }),
|
|
@@ -1458,7 +1680,7 @@ const PopFailureMark = (props) => {
|
|
|
1458
1680
|
/*------------------------------------------------------------------------*/
|
|
1459
1681
|
/* Style */
|
|
1460
1682
|
/*------------------------------------------------------------------------*/
|
|
1461
|
-
const style = `
|
|
1683
|
+
const style$1 = `
|
|
1462
1684
|
.PopPendingMark-outer-container {
|
|
1463
1685
|
position: relative;
|
|
1464
1686
|
display: inline-block;
|
|
@@ -1531,7 +1753,7 @@ const PopPendingMark = (props) => {
|
|
|
1531
1753
|
width: `${sizeRem}rem`,
|
|
1532
1754
|
height: `${sizeRem}rem`,
|
|
1533
1755
|
}, "aria-label": "mark indicating that the item is pending" },
|
|
1534
|
-
React.createElement("style", null, style),
|
|
1756
|
+
React.createElement("style", null, style$1),
|
|
1535
1757
|
React.createElement("div", null,
|
|
1536
1758
|
React.createElement(FontAwesomeIcon, { icon: faHourglass, className: `PopPendingMark-hourglass text-${hourglassVariant}`, style: {
|
|
1537
1759
|
fontSize: `${sizeRem * 0.6}rem`,
|
|
@@ -1544,27 +1766,27 @@ const PopPendingMark = (props) => {
|
|
|
1544
1766
|
*/
|
|
1545
1767
|
/* ------------- Actions ------------ */
|
|
1546
1768
|
// Types of actions
|
|
1547
|
-
var ActionType$
|
|
1769
|
+
var ActionType$3;
|
|
1548
1770
|
(function (ActionType) {
|
|
1549
1771
|
// Indicate that the text was recently copied
|
|
1550
1772
|
ActionType["IndicateRecentlyCopied"] = "indicate-recently-copied";
|
|
1551
1773
|
// Clear the status
|
|
1552
1774
|
ActionType["ClearRecentlyCopiedStatus"] = "clear-recently-copied-status";
|
|
1553
|
-
})(ActionType$
|
|
1775
|
+
})(ActionType$3 || (ActionType$3 = {}));
|
|
1554
1776
|
/**
|
|
1555
1777
|
* Reducer that executes actions
|
|
1556
1778
|
* @author Gabe Abrams
|
|
1557
1779
|
* @param state current state
|
|
1558
1780
|
* @param action action to execute
|
|
1559
1781
|
*/
|
|
1560
|
-
const reducer$
|
|
1782
|
+
const reducer$3 = (state, action) => {
|
|
1561
1783
|
switch (action.type) {
|
|
1562
|
-
case ActionType$
|
|
1784
|
+
case ActionType$3.IndicateRecentlyCopied: {
|
|
1563
1785
|
return {
|
|
1564
1786
|
recentlyCopied: true,
|
|
1565
1787
|
};
|
|
1566
1788
|
}
|
|
1567
|
-
case ActionType$
|
|
1789
|
+
case ActionType$3.ClearRecentlyCopiedStatus: {
|
|
1568
1790
|
return {
|
|
1569
1791
|
recentlyCopied: false,
|
|
1570
1792
|
};
|
|
@@ -1590,7 +1812,7 @@ const CopiableBox = (props) => {
|
|
|
1590
1812
|
recentlyCopied: false,
|
|
1591
1813
|
};
|
|
1592
1814
|
// Initialize state
|
|
1593
|
-
const [state, dispatch] = useReducer(reducer$
|
|
1815
|
+
const [state, dispatch] = useReducer(reducer$3, initialState);
|
|
1594
1816
|
// Destructure common state
|
|
1595
1817
|
const { recentlyCopied, } = state;
|
|
1596
1818
|
/*------------------------------------------------------------------------*/
|
|
@@ -1610,13 +1832,13 @@ const CopiableBox = (props) => {
|
|
|
1610
1832
|
}
|
|
1611
1833
|
// Show copied notice
|
|
1612
1834
|
dispatch({
|
|
1613
|
-
type: ActionType$
|
|
1835
|
+
type: ActionType$3.IndicateRecentlyCopied,
|
|
1614
1836
|
});
|
|
1615
1837
|
// Wait a moment
|
|
1616
1838
|
yield waitMs(4000);
|
|
1617
1839
|
// Hide copied notice
|
|
1618
1840
|
dispatch({
|
|
1619
|
-
type: ActionType$
|
|
1841
|
+
type: ActionType$3.ClearRecentlyCopiedStatus,
|
|
1620
1842
|
});
|
|
1621
1843
|
});
|
|
1622
1844
|
/*------------------------------------------------------------------------*/
|
|
@@ -1671,20 +1893,20 @@ const CopiableBox = (props) => {
|
|
|
1671
1893
|
*/
|
|
1672
1894
|
/* ------------- Actions ------------ */
|
|
1673
1895
|
// Types of actions
|
|
1674
|
-
var ActionType;
|
|
1896
|
+
var ActionType$2;
|
|
1675
1897
|
(function (ActionType) {
|
|
1676
1898
|
// Toggle whether the children are being shown
|
|
1677
1899
|
ActionType["ToggleItems"] = "toggle-items";
|
|
1678
|
-
})(ActionType || (ActionType = {}));
|
|
1900
|
+
})(ActionType$2 || (ActionType$2 = {}));
|
|
1679
1901
|
/**
|
|
1680
1902
|
* Reducer that executes actions
|
|
1681
1903
|
* @author Yuen Ler Chow
|
|
1682
1904
|
* @param state current state
|
|
1683
1905
|
* @param action action to execute
|
|
1684
1906
|
*/
|
|
1685
|
-
const reducer = (state, action) => {
|
|
1907
|
+
const reducer$2 = (state, action) => {
|
|
1686
1908
|
switch (action.type) {
|
|
1687
|
-
case ActionType.ToggleItems: {
|
|
1909
|
+
case ActionType$2.ToggleItems: {
|
|
1688
1910
|
return { isShowingItems: !state.isShowingItems };
|
|
1689
1911
|
}
|
|
1690
1912
|
default: {
|
|
@@ -1708,7 +1930,7 @@ const NestableItemList = (props) => {
|
|
|
1708
1930
|
isShowingItems: false,
|
|
1709
1931
|
};
|
|
1710
1932
|
// Initialize state
|
|
1711
|
-
const [state, dispatch] = useReducer(reducer, initialState);
|
|
1933
|
+
const [state, dispatch] = useReducer(reducer$2, initialState);
|
|
1712
1934
|
// Destructure common state
|
|
1713
1935
|
const { isShowingItems, } = state;
|
|
1714
1936
|
/*------------------------------------------------------------------------*/
|
|
@@ -1798,7 +2020,7 @@ const NestableItemList = (props) => {
|
|
|
1798
2020
|
backgroundColor: 'transparent',
|
|
1799
2021
|
}, type: "button", onClick: () => {
|
|
1800
2022
|
dispatch({
|
|
1801
|
-
type: ActionType.ToggleItems,
|
|
2023
|
+
type: ActionType$2.ToggleItems,
|
|
1802
2024
|
});
|
|
1803
2025
|
}, "aria-label": `${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}` },
|
|
1804
2026
|
React.createElement(FontAwesomeIcon, { icon: isShowingItems ? faChevronDown : faChevronRight })))),
|
|
@@ -1843,311 +2065,1827 @@ const ItemPicker = (props) => {
|
|
|
1843
2065
|
};
|
|
1844
2066
|
|
|
1845
2067
|
/**
|
|
1846
|
-
*
|
|
2068
|
+
* Path of the route for storing client-side logs
|
|
1847
2069
|
* @author Gabe Abrams
|
|
1848
2070
|
*/
|
|
1849
|
-
const
|
|
2071
|
+
const LOG_REVIEW_ROUTE_PATH_PREFIX = `/admin${ROUTE_PATH_PREFIX}/logs`;
|
|
1850
2072
|
|
|
1851
2073
|
/**
|
|
1852
|
-
*
|
|
2074
|
+
* Source of a log event
|
|
1853
2075
|
* @author Gabe Abrams
|
|
1854
2076
|
*/
|
|
1855
|
-
|
|
2077
|
+
var LogSource;
|
|
2078
|
+
(function (LogSource) {
|
|
2079
|
+
// Client
|
|
2080
|
+
LogSource["Client"] = "client";
|
|
2081
|
+
// Server
|
|
2082
|
+
LogSource["Server"] = "server";
|
|
2083
|
+
})(LogSource || (LogSource = {}));
|
|
2084
|
+
var LogSource$1 = LogSource;
|
|
1856
2085
|
|
|
1857
2086
|
/**
|
|
1858
|
-
*
|
|
2087
|
+
* Type of a log event
|
|
1859
2088
|
* @author Gabe Abrams
|
|
1860
2089
|
*/
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
* (including ellipses if applicable)
|
|
1870
|
-
*/
|
|
1871
|
-
const abbreviate = (text, maxChars) => {
|
|
1872
|
-
// Check if already short enough
|
|
1873
|
-
if (text.trim().length < maxChars) {
|
|
1874
|
-
return text.trim();
|
|
1875
|
-
}
|
|
1876
|
-
// Abbreviate
|
|
1877
|
-
const shortenedText = (text
|
|
1878
|
-
.trim()
|
|
1879
|
-
.substring(0, maxChars - 3)
|
|
1880
|
-
.trim());
|
|
1881
|
-
return `${shortenedText}...`;
|
|
1882
|
-
};
|
|
1883
|
-
|
|
1884
|
-
/**
|
|
1885
|
-
* Sum the numbers in an array
|
|
1886
|
-
* @author Gabe Abrams
|
|
1887
|
-
* @param nums the numbers to sum
|
|
1888
|
-
* @returns the sum of the numbers
|
|
1889
|
-
*/
|
|
1890
|
-
const sum = (nums) => {
|
|
1891
|
-
return nums.reduce((a, b) => {
|
|
1892
|
-
return (a + b);
|
|
1893
|
-
}, 0);
|
|
1894
|
-
};
|
|
2090
|
+
var LogType;
|
|
2091
|
+
(function (LogType) {
|
|
2092
|
+
// User action
|
|
2093
|
+
LogType["Action"] = "action";
|
|
2094
|
+
// Error
|
|
2095
|
+
LogType["Error"] = "error";
|
|
2096
|
+
})(LogType || (LogType = {}));
|
|
2097
|
+
var LogType$1 = LogType;
|
|
1895
2098
|
|
|
1896
2099
|
/**
|
|
1897
|
-
*
|
|
2100
|
+
* Types of actions
|
|
1898
2101
|
* @author Gabe Abrams
|
|
1899
|
-
* @param nums the numbers to average
|
|
1900
|
-
* @returns average value or 0 if no numbers
|
|
1901
2102
|
*/
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2103
|
+
var LogAction;
|
|
2104
|
+
(function (LogAction) {
|
|
2105
|
+
// Target was opened by the user (it was not on screen, but now it is)
|
|
2106
|
+
LogAction["Open"] = "open";
|
|
2107
|
+
// Target was closed by the user (it was on screen, but now it is not)
|
|
2108
|
+
LogAction["Close"] = "close";
|
|
2109
|
+
// Target was cancelled by the user (it was on closed without saving)
|
|
2110
|
+
LogAction["Cancel"] = "cancel";
|
|
2111
|
+
// Target was expanded by the user (it always remains on screen, but size was changed)
|
|
2112
|
+
LogAction["Expand"] = "expand";
|
|
2113
|
+
// Target was collapsed by the user (it always remains on screen, but size was changed)
|
|
2114
|
+
LogAction["Collapse"] = "collapse";
|
|
2115
|
+
// Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
|
|
2116
|
+
LogAction["View"] = "view";
|
|
2117
|
+
// Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
|
|
2118
|
+
LogAction["Interrupt"] = "interrupt";
|
|
2119
|
+
// Target was created by the user (it did not exist before)
|
|
2120
|
+
LogAction["Create"] = "create";
|
|
2121
|
+
// Target was edited by the user (it existed and was changed)
|
|
2122
|
+
LogAction["Edit"] = "edit";
|
|
2123
|
+
// Target was deleted by the user (it existed and now it doesn't)
|
|
2124
|
+
LogAction["Delete"] = "delete";
|
|
2125
|
+
// Target was added by the user (it already existed and was added to another place)
|
|
2126
|
+
LogAction["Add"] = "add";
|
|
2127
|
+
// Target was removed by the user (it was removed from something but still exists)
|
|
2128
|
+
LogAction["Remove"] = "remove";
|
|
2129
|
+
// Target was activated by the user (click, check, tap, keypress, etc.)
|
|
2130
|
+
LogAction["Activate"] = "activate";
|
|
2131
|
+
// Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
|
|
2132
|
+
LogAction["Deactivate"] = "deactivate";
|
|
2133
|
+
// User showed interest in a target (hover, peek, etc.)
|
|
2134
|
+
LogAction["Peek"] = "peek";
|
|
2135
|
+
// Unknown action
|
|
2136
|
+
LogAction["Unknown"] = "unknown";
|
|
2137
|
+
})(LogAction || (LogAction = {}));
|
|
2138
|
+
var LogAction$1 = LogAction;
|
|
1912
2139
|
|
|
1913
2140
|
/**
|
|
1914
|
-
*
|
|
2141
|
+
* Server-side API param types
|
|
1915
2142
|
* @author Gabe Abrams
|
|
1916
|
-
* @param num the number to round
|
|
1917
|
-
* @param numDecimals the number of decimals to round to
|
|
1918
|
-
* @returns rounded number
|
|
1919
2143
|
*/
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2144
|
+
var ParamType;
|
|
2145
|
+
(function (ParamType) {
|
|
2146
|
+
ParamType["Boolean"] = "boolean";
|
|
2147
|
+
ParamType["BooleanOptional"] = "boolean-optional";
|
|
2148
|
+
ParamType["Float"] = "float";
|
|
2149
|
+
ParamType["FloatOptional"] = "float-optional";
|
|
2150
|
+
ParamType["Int"] = "int";
|
|
2151
|
+
ParamType["IntOptional"] = "int-optional";
|
|
2152
|
+
ParamType["JSON"] = "json";
|
|
2153
|
+
ParamType["JSONOptional"] = "json-optional";
|
|
2154
|
+
ParamType["String"] = "string";
|
|
2155
|
+
ParamType["StringOptional"] = "string-optional";
|
|
2156
|
+
})(ParamType || (ParamType = {}));
|
|
2157
|
+
var ParamType$1 = ParamType;
|
|
1924
2158
|
|
|
1925
2159
|
/**
|
|
1926
|
-
* Round a number
|
|
2160
|
+
* Round a number to a certain number of decimals
|
|
1927
2161
|
* @author Gabe Abrams
|
|
1928
2162
|
* @param num the number to round
|
|
1929
2163
|
* @param numDecimals the number of decimals to round to
|
|
1930
2164
|
* @returns rounded number
|
|
1931
2165
|
*/
|
|
1932
|
-
const
|
|
2166
|
+
const roundToNumDecimals = (num, numDecimals) => {
|
|
1933
2167
|
const rounder = 10 ** numDecimals;
|
|
1934
|
-
return (Math.
|
|
2168
|
+
return (Math.round(num * rounder) / rounder);
|
|
1935
2169
|
};
|
|
1936
2170
|
|
|
1937
2171
|
/**
|
|
1938
|
-
*
|
|
2172
|
+
* Escape a CSV cell if needed
|
|
1939
2173
|
* @author Gabe Abrams
|
|
1940
|
-
* @param
|
|
1941
|
-
* @
|
|
1942
|
-
* @param max the maximum number in the bound
|
|
1943
|
-
* @returns bounded number
|
|
2174
|
+
* @param text the cell contents
|
|
2175
|
+
* @returns escaped cell text
|
|
1944
2176
|
*/
|
|
1945
|
-
const
|
|
1946
|
-
|
|
2177
|
+
const escapeCellText = (text) => {
|
|
2178
|
+
if (!text.includes(',')) {
|
|
2179
|
+
// No need to escape
|
|
2180
|
+
return text;
|
|
2181
|
+
}
|
|
2182
|
+
// Perform escape
|
|
2183
|
+
return `"${text.replace(/"/g, '""')}`;
|
|
1947
2184
|
};
|
|
1948
|
-
|
|
1949
2185
|
/**
|
|
1950
|
-
*
|
|
1951
|
-
* (e.g. 5.2 becomes 5.20 with 2 digit padding)
|
|
2186
|
+
* Generate a CSV file
|
|
1952
2187
|
* @author Gabe Abrams
|
|
1953
|
-
* @param
|
|
1954
|
-
* @param
|
|
1955
|
-
* @returns
|
|
2188
|
+
* @param data list of row data in the form of json objects
|
|
2189
|
+
* @param columns list of columns to include in the csv
|
|
2190
|
+
* @returns multiline csv string
|
|
1956
2191
|
*/
|
|
1957
|
-
const
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2192
|
+
const genCSV = (data, columns) => {
|
|
2193
|
+
let csv = '';
|
|
2194
|
+
// Add header
|
|
2195
|
+
csv += (columns
|
|
2196
|
+
.map((column) => {
|
|
2197
|
+
return escapeCellText(column.title);
|
|
2198
|
+
})
|
|
2199
|
+
.join(','));
|
|
2200
|
+
// Add each row
|
|
2201
|
+
data.forEach((datum) => {
|
|
2202
|
+
csv += (columns
|
|
2203
|
+
.map((column) => {
|
|
2204
|
+
return escapeCellText(datum[column.param]);
|
|
2205
|
+
})
|
|
2206
|
+
.join(','));
|
|
2207
|
+
});
|
|
1972
2208
|
// Return
|
|
1973
|
-
return
|
|
2209
|
+
return csv;
|
|
1974
2210
|
};
|
|
1975
2211
|
|
|
1976
2212
|
/**
|
|
1977
|
-
*
|
|
2213
|
+
* Button for downloading a csv file
|
|
1978
2214
|
* @author Gabe Abrams
|
|
1979
|
-
* @param num the number to pad
|
|
1980
|
-
* @param numDigits the minimum number of digits before the decimal
|
|
1981
|
-
* @returns padded number
|
|
1982
2215
|
*/
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2216
|
+
/*------------------------------------------------------------------------*/
|
|
2217
|
+
/* Component */
|
|
2218
|
+
/*------------------------------------------------------------------------*/
|
|
2219
|
+
const CSVDownloadButton = (props) => {
|
|
2220
|
+
/*------------------------------------------------------------------------*/
|
|
2221
|
+
/* Setup */
|
|
2222
|
+
/*------------------------------------------------------------------------*/
|
|
2223
|
+
/* -------------- Props ------------- */
|
|
2224
|
+
// Destructure all props
|
|
2225
|
+
const { filename, csv, id, className, ariaLabel, style, onClick, children, } = props;
|
|
2226
|
+
/*------------------------------------------------------------------------*/
|
|
2227
|
+
/* Render */
|
|
2228
|
+
/*------------------------------------------------------------------------*/
|
|
2229
|
+
/*----------------------------------------*/
|
|
2230
|
+
/* Main UI */
|
|
2231
|
+
/*----------------------------------------*/
|
|
2232
|
+
// Render the button
|
|
2233
|
+
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
|
|
2234
|
+
? `Click to download ${filename}`
|
|
2235
|
+
: ariaLabel), style: style, onClick: onClick },
|
|
2236
|
+
!children && (React.createElement(React.Fragment, null,
|
|
2237
|
+
React.createElement(FontAwesomeIcon, { icon: faCloudDownloadAlt, className: "mr-2" }),
|
|
2238
|
+
"Download CSV")),
|
|
2239
|
+
children));
|
|
1992
2240
|
};
|
|
1993
2241
|
|
|
1994
2242
|
/**
|
|
1995
|
-
*
|
|
2243
|
+
* Intelligent table
|
|
1996
2244
|
* @author Gabe Abrams
|
|
1997
|
-
* @param num the number to round
|
|
1998
|
-
* @param numDecimals the number of decimals to round to
|
|
1999
|
-
* @returns rounded number
|
|
2000
2245
|
*/
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
//
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
/*
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2246
|
+
// Sort types
|
|
2247
|
+
var SortType;
|
|
2248
|
+
(function (SortType) {
|
|
2249
|
+
// Ascending
|
|
2250
|
+
SortType["Ascending"] = "ascending";
|
|
2251
|
+
// Descending
|
|
2252
|
+
SortType["Descending"] = "descending";
|
|
2253
|
+
})(SortType || (SortType = {}));
|
|
2254
|
+
/* ------------- Actions ------------ */
|
|
2255
|
+
// Types of actions
|
|
2256
|
+
var ActionType$1;
|
|
2257
|
+
(function (ActionType) {
|
|
2258
|
+
// Toggle sort column param
|
|
2259
|
+
ActionType["ToggleSortColumn"] = "toggle-sort-column";
|
|
2260
|
+
// Toggle the visibility of a column
|
|
2261
|
+
ActionType["ToggleColumnVisibility"] = "toggle-column-visibility";
|
|
2262
|
+
// Toggle the column visibility customization modal
|
|
2263
|
+
ActionType["ToggleColVisCusModalVisibility"] = "toggle-col-vis-cus-modal-visibility";
|
|
2264
|
+
})(ActionType$1 || (ActionType$1 = {}));
|
|
2013
2265
|
/**
|
|
2014
|
-
*
|
|
2266
|
+
* Reducer that executes actions
|
|
2015
2267
|
* @author Gabe Abrams
|
|
2016
|
-
* @param
|
|
2017
|
-
* @param
|
|
2018
|
-
* @param opts.path pathname of the request
|
|
2019
|
-
* @param [opts.body] body of the response if successful
|
|
2020
|
-
* @param [opts.errorMessage] error message if not successful
|
|
2021
|
-
* @param [opts.errorCode] error code if not successful
|
|
2268
|
+
* @param state current state
|
|
2269
|
+
* @param action action to execute
|
|
2022
2270
|
*/
|
|
2023
|
-
const
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
errorMessage,
|
|
2037
|
-
errorCode,
|
|
2271
|
+
const reducer$1 = (state, action) => {
|
|
2272
|
+
switch (action.type) {
|
|
2273
|
+
case ActionType$1.ToggleSortColumn: {
|
|
2274
|
+
if (action.param !== state.sortColumnParam) {
|
|
2275
|
+
// Different column param
|
|
2276
|
+
return Object.assign(Object.assign({}, state), { sortColumnParam: action.param, sortType: SortType.Ascending });
|
|
2277
|
+
}
|
|
2278
|
+
if (state.sortType === SortType.Ascending) {
|
|
2279
|
+
// Switch to descending
|
|
2280
|
+
return Object.assign(Object.assign({}, state), { sortType: SortType.Descending });
|
|
2281
|
+
}
|
|
2282
|
+
// Stop sorting by column
|
|
2283
|
+
return Object.assign(Object.assign({}, state), { sortColumnParam: undefined, sortType: SortType.Ascending });
|
|
2038
2284
|
}
|
|
2039
|
-
: {
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2285
|
+
case ActionType$1.ToggleColumnVisibility: {
|
|
2286
|
+
const { columnVisibilityMap } = state;
|
|
2287
|
+
columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
|
|
2288
|
+
return Object.assign(Object.assign({}, state), { columnVisibilityMap });
|
|
2289
|
+
}
|
|
2290
|
+
case ActionType$1.ToggleColVisCusModalVisibility: {
|
|
2291
|
+
return Object.assign(Object.assign({}, state), { columnVisibilityCustomizationModalVisible: !state.columnVisibilityCustomizationModalVisible });
|
|
2292
|
+
}
|
|
2293
|
+
default: {
|
|
2294
|
+
return state;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2043
2297
|
};
|
|
2044
2298
|
/*------------------------------------------------------------------------*/
|
|
2045
|
-
/*
|
|
2299
|
+
/* Component */
|
|
2046
2300
|
/*------------------------------------------------------------------------*/
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
*/
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2301
|
+
const IntelliTable = (props) => {
|
|
2302
|
+
/*------------------------------------------------------------------------*/
|
|
2303
|
+
/* Setup */
|
|
2304
|
+
/*------------------------------------------------------------------------*/
|
|
2305
|
+
var _a;
|
|
2306
|
+
/* -------------- Props ------------- */
|
|
2307
|
+
// Destructure all props
|
|
2308
|
+
const { title, id, data, columns, } = props;
|
|
2309
|
+
/* -------------- State ------------- */
|
|
2310
|
+
// Initial state
|
|
2311
|
+
const initColumnVisibilityMap = {};
|
|
2312
|
+
columns.forEach((column) => {
|
|
2313
|
+
initColumnVisibilityMap[column.param] = !column.startsHidden;
|
|
2314
|
+
});
|
|
2315
|
+
const initialState = {
|
|
2316
|
+
sortColumnParam: undefined,
|
|
2317
|
+
sortType: SortType.Descending,
|
|
2318
|
+
columnVisibilityMap: initColumnVisibilityMap,
|
|
2319
|
+
columnVisibilityCustomizationModalVisible: false,
|
|
2320
|
+
};
|
|
2321
|
+
// Initialize state
|
|
2322
|
+
const [state, dispatch] = useReducer(reducer$1, initialState);
|
|
2323
|
+
// Destructure common state
|
|
2324
|
+
const { sortColumnParam, sortType, columnVisibilityMap, columnVisibilityCustomizationModalVisible, } = state;
|
|
2325
|
+
/* ------- Col Vis Customization Modal ------ */
|
|
2326
|
+
if (columnVisibilityCustomizationModalVisible) {
|
|
2327
|
+
// Create modal
|
|
2328
|
+
(React.createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
|
|
2329
|
+
dispatch({
|
|
2330
|
+
type: ActionType$1.ToggleColVisCusModalVisibility,
|
|
2331
|
+
});
|
|
2332
|
+
} }, columns.map((column) => {
|
|
2333
|
+
return (React.createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, text: column.title, onChanged: () => {
|
|
2334
|
+
dispatch({
|
|
2335
|
+
type: ActionType$1.ToggleColumnVisibility,
|
|
2336
|
+
param: column.param,
|
|
2337
|
+
});
|
|
2338
|
+
}, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column` }));
|
|
2339
|
+
})));
|
|
2340
|
+
}
|
|
2341
|
+
/*----------------------------------------*/
|
|
2342
|
+
/* Main UI */
|
|
2343
|
+
/*----------------------------------------*/
|
|
2344
|
+
// Table header
|
|
2345
|
+
const headerCells = (columns
|
|
2346
|
+
.filter((column) => {
|
|
2347
|
+
return columnVisibilityMap[column.param];
|
|
2348
|
+
})
|
|
2349
|
+
.map((column) => {
|
|
2350
|
+
// Custom info based on current sort type
|
|
2351
|
+
let sortButtonAriaLabel;
|
|
2352
|
+
let sortIcon = faSort;
|
|
2353
|
+
if (!sortColumnParam) {
|
|
2354
|
+
// Not being sorted yet
|
|
2355
|
+
sortButtonAriaLabel = `sort ascending by ${column.title}`;
|
|
2356
|
+
sortIcon = faSort;
|
|
2065
2357
|
}
|
|
2066
|
-
|
|
2067
|
-
//
|
|
2358
|
+
else if (column.param === sortColumnParam) {
|
|
2359
|
+
// Already sorted by this column
|
|
2360
|
+
if (sortType === SortType.Ascending) {
|
|
2361
|
+
// Sorted ascending
|
|
2362
|
+
sortButtonAriaLabel = `sort descending by ${column.title}`;
|
|
2363
|
+
sortIcon = faSortDown;
|
|
2364
|
+
}
|
|
2365
|
+
else {
|
|
2366
|
+
// Sorted descending
|
|
2367
|
+
sortButtonAriaLabel = `stop sorting by ${column.title}`;
|
|
2368
|
+
sortIcon = faSortUp;
|
|
2369
|
+
}
|
|
2068
2370
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2371
|
+
else {
|
|
2372
|
+
// Sorted by a different column
|
|
2373
|
+
sortButtonAriaLabel = `sort ascending by ${column.title}`;
|
|
2374
|
+
sortIcon = faSort;
|
|
2072
2375
|
}
|
|
2073
|
-
//
|
|
2074
|
-
|
|
2376
|
+
// Create the cell UI
|
|
2377
|
+
return (React.createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}` },
|
|
2378
|
+
React.createElement("div", { className: "d-flex align-items-center justify-content-center flex-row h-100" },
|
|
2379
|
+
React.createElement("h4", { className: "m-0" }, column.title),
|
|
2380
|
+
React.createElement("div", null,
|
|
2381
|
+
React.createElement("button", { type: "button", className: "btn btn-light", "aria-label": sortButtonAriaLabel, onClick: () => {
|
|
2382
|
+
dispatch({
|
|
2383
|
+
type: ActionType$1.ToggleSortColumn,
|
|
2384
|
+
param: column.param,
|
|
2385
|
+
});
|
|
2386
|
+
} },
|
|
2387
|
+
React.createElement(FontAwesomeIcon, { icon: sortIcon }))))));
|
|
2388
|
+
}));
|
|
2389
|
+
const tableHeader = (React.createElement("thead", null,
|
|
2390
|
+
React.createElement("tr", null, headerCells)));
|
|
2391
|
+
// Sort data
|
|
2392
|
+
let sortedData = [...data];
|
|
2393
|
+
const paramType = (_a = columns.find((column) => {
|
|
2394
|
+
return (column.param === sortColumnParam);
|
|
2395
|
+
})) === null || _a === void 0 ? void 0 : _a.type;
|
|
2396
|
+
const descending = (sortType === SortType.Descending);
|
|
2397
|
+
if (sortColumnParam) {
|
|
2398
|
+
sortedData.sort((a, b) => {
|
|
2399
|
+
const aVal = a[sortColumnParam];
|
|
2400
|
+
const bVal = b[sortColumnParam];
|
|
2401
|
+
// Sort differently based on the data type
|
|
2402
|
+
// > Boolean
|
|
2403
|
+
if (paramType === ParamType$1.Boolean) {
|
|
2404
|
+
if (aVal && !bVal) {
|
|
2405
|
+
return (descending ? -1 : 1);
|
|
2406
|
+
}
|
|
2407
|
+
if (!aVal && bVal) {
|
|
2408
|
+
return (descending ? 1 : -1);
|
|
2409
|
+
}
|
|
2410
|
+
return 0;
|
|
2411
|
+
}
|
|
2412
|
+
// > Number
|
|
2413
|
+
if (paramType === ParamType$1.Int
|
|
2414
|
+
|| paramType === ParamType$1.Float) {
|
|
2415
|
+
return (descending
|
|
2416
|
+
? (bVal - aVal)
|
|
2417
|
+
: (aVal - bVal));
|
|
2418
|
+
}
|
|
2419
|
+
// > String
|
|
2420
|
+
if (paramType === ParamType$1.String) {
|
|
2421
|
+
if (aVal < bVal) {
|
|
2422
|
+
return (descending ? -1 : 1);
|
|
2423
|
+
}
|
|
2424
|
+
if (aVal > bVal) {
|
|
2425
|
+
return (descending ? 1 : -1);
|
|
2426
|
+
}
|
|
2427
|
+
return 0;
|
|
2428
|
+
}
|
|
2429
|
+
// > JSON
|
|
2430
|
+
if (paramType === ParamType$1.JSON) {
|
|
2431
|
+
const aSize = (Array.isArray(aVal)
|
|
2432
|
+
? aVal.length
|
|
2433
|
+
: Object.keys(aVal).length);
|
|
2434
|
+
const bSize = (Array.isArray(bVal)
|
|
2435
|
+
? bVal.length
|
|
2436
|
+
: Object.keys(bVal).length);
|
|
2437
|
+
return (descending
|
|
2438
|
+
? (bSize - aSize)
|
|
2439
|
+
: (aSize - bSize));
|
|
2440
|
+
}
|
|
2441
|
+
// No sort
|
|
2442
|
+
return 0;
|
|
2443
|
+
});
|
|
2075
2444
|
}
|
|
2076
|
-
//
|
|
2077
|
-
const
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2445
|
+
// Table body
|
|
2446
|
+
const rows = sortedData.map((datum) => {
|
|
2447
|
+
// Build cells
|
|
2448
|
+
const cells = (columns
|
|
2449
|
+
.filter((column) => {
|
|
2450
|
+
return columnVisibilityMap[column.param];
|
|
2451
|
+
})
|
|
2452
|
+
.map((column) => {
|
|
2453
|
+
// Get value
|
|
2454
|
+
let value = datum;
|
|
2455
|
+
const paramParts = column.param.split('.');
|
|
2456
|
+
paramParts.forEach((paramPart) => {
|
|
2457
|
+
value = (value !== null && value !== void 0 ? value : {})[paramPart];
|
|
2458
|
+
});
|
|
2459
|
+
let fullValue;
|
|
2460
|
+
let visibleValue;
|
|
2461
|
+
let title = '';
|
|
2462
|
+
if (column.type === ParamType$1.Boolean) {
|
|
2463
|
+
fullValue = !!(value);
|
|
2464
|
+
const noValue = (value === undefined
|
|
2465
|
+
|| value === null);
|
|
2466
|
+
visibleValue = (noValue
|
|
2467
|
+
? (React.createElement(FontAwesomeIcon, { icon: faCheckCircle }))
|
|
2468
|
+
: (React.createElement(FontAwesomeIcon, { icon: faXmarkCircle })));
|
|
2469
|
+
title = (fullValue ? 'True' : 'False');
|
|
2470
|
+
}
|
|
2471
|
+
else if (column.type === ParamType$1.Int) {
|
|
2472
|
+
fullValue = Number.parseInt(value, 10);
|
|
2473
|
+
const noValue = Number.isNaN(fullValue);
|
|
2474
|
+
visibleValue = (noValue
|
|
2475
|
+
? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
|
|
2476
|
+
: fullValue);
|
|
2477
|
+
title = String(fullValue);
|
|
2478
|
+
}
|
|
2479
|
+
else if (column.type === ParamType$1.Float) {
|
|
2480
|
+
fullValue = Number.parseFloat(value);
|
|
2481
|
+
const noValue = Number.isNaN(fullValue);
|
|
2482
|
+
visibleValue = (noValue
|
|
2483
|
+
? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
|
|
2484
|
+
: roundToNumDecimals(fullValue, 2));
|
|
2485
|
+
title = String(fullValue);
|
|
2486
|
+
}
|
|
2487
|
+
else if (column.type === ParamType$1.String) {
|
|
2488
|
+
fullValue = String(value).trim();
|
|
2489
|
+
const noValue = (value.trim().length) === 0;
|
|
2490
|
+
visibleValue = (noValue
|
|
2491
|
+
? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
|
|
2492
|
+
: fullValue);
|
|
2493
|
+
title = `"${value}"`;
|
|
2494
|
+
}
|
|
2495
|
+
else if (column.type === ParamType$1.JSON) {
|
|
2496
|
+
fullValue = JSON.stringify(value);
|
|
2497
|
+
const noValue = (Array.isArray(value)
|
|
2498
|
+
? (!value || value.length === 0)
|
|
2499
|
+
: Object.keys(value !== null && value !== void 0 ? value : {}).length === 0);
|
|
2500
|
+
visibleValue = (noValue
|
|
2501
|
+
? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
|
|
2502
|
+
: fullValue);
|
|
2503
|
+
}
|
|
2504
|
+
// Create UI
|
|
2505
|
+
return (React.createElement("td", { key: `${datum.id}-${column.param}`, title: title }, visibleValue));
|
|
2506
|
+
}));
|
|
2507
|
+
// Add cells to a row
|
|
2508
|
+
return (React.createElement("tr", { key: datum.id }, cells));
|
|
2081
2509
|
});
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2510
|
+
const tableBody = (React.createElement("tbody", null, rows));
|
|
2511
|
+
// Build table
|
|
2512
|
+
const table = (React.createElement("table", { className: "table table-dark table-striped" },
|
|
2513
|
+
tableHeader,
|
|
2514
|
+
tableBody));
|
|
2515
|
+
// Count the number of hidden columns
|
|
2516
|
+
const numHiddenCols = (Object.values(columnVisibilityMap)
|
|
2517
|
+
.filter((isVisible) => {
|
|
2518
|
+
return !isVisible;
|
|
2519
|
+
})
|
|
2520
|
+
.length);
|
|
2521
|
+
// Build CSV
|
|
2522
|
+
const csv = genCSV(data, columns);
|
|
2523
|
+
// Build main UI
|
|
2524
|
+
return (React.createElement("div", { className: `IntelliTable-container-${id}` },
|
|
2525
|
+
React.createElement("div", { className: "d-flex align-items-center justify-content-center" },
|
|
2526
|
+
React.createElement("h3", { className: "m-0" }, title),
|
|
2527
|
+
React.createElement("div", { className: "flex-grow-1 text-end" },
|
|
2528
|
+
React.createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: `${title}.csv`, csv: csv }),
|
|
2529
|
+
React.createElement("button", { type: "button", className: "btn btn-secondary", "aria-label": `show panel for customizing which columns show in table ${title}`, id: `IntelliTable-${id}-show-column-customization-modal`, onClick: () => {
|
|
2530
|
+
dispatch({
|
|
2531
|
+
type: ActionType$1.ToggleColVisCusModalVisibility,
|
|
2532
|
+
});
|
|
2533
|
+
} },
|
|
2534
|
+
"Show/Hide Cols",
|
|
2535
|
+
numHiddenCols > 0 && (React.createElement(React.Fragment, null,
|
|
2536
|
+
' ',
|
|
2537
|
+
"(",
|
|
2538
|
+
numHiddenCols,
|
|
2539
|
+
' ',
|
|
2540
|
+
"hidden)"))))),
|
|
2541
|
+
React.createElement("div", { className: `IntelliTable-table-${id}`, style: {
|
|
2542
|
+
overflowX: 'auto',
|
|
2543
|
+
} }, table)));
|
|
2544
|
+
};
|
|
2545
|
+
|
|
2546
|
+
/**
|
|
2547
|
+
* Log reviewer panel that allows users (must be approved admins) to
|
|
2548
|
+
* review logs written by dce-reactkit
|
|
2549
|
+
* @author Gabe Abrams
|
|
2550
|
+
*/
|
|
2551
|
+
// Types of filter drawers
|
|
2552
|
+
var FilterDrawer;
|
|
2553
|
+
(function (FilterDrawer) {
|
|
2554
|
+
FilterDrawer["Date"] = "date";
|
|
2555
|
+
FilterDrawer["Context"] = "context";
|
|
2556
|
+
FilterDrawer["Tag"] = "tag";
|
|
2557
|
+
FilterDrawer["Action"] = "action";
|
|
2558
|
+
FilterDrawer["Advanced"] = "advanced";
|
|
2559
|
+
})(FilterDrawer || (FilterDrawer = {}));
|
|
2560
|
+
/*------------------------------------------------------------------------*/
|
|
2561
|
+
/* Style */
|
|
2562
|
+
/*------------------------------------------------------------------------*/
|
|
2563
|
+
const style = `
|
|
2564
|
+
.LogReviewer-outer-container {
|
|
2565
|
+
/* Full Screen */
|
|
2566
|
+
display: inline-block;
|
|
2567
|
+
left: 0;
|
|
2568
|
+
top: 0;
|
|
2569
|
+
width: 100vw;
|
|
2570
|
+
height: 100vh;
|
|
2571
|
+
|
|
2572
|
+
/* On Top and Fixed */
|
|
2573
|
+
position: fixed;
|
|
2574
|
+
z-index: 90000;
|
|
2575
|
+
|
|
2576
|
+
/* Space around contents */
|
|
2577
|
+
padding: 0.5rem;
|
|
2578
|
+
|
|
2579
|
+
/* Dark Background */
|
|
2580
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
2581
|
+
|
|
2582
|
+
/* No Clickthrough */
|
|
2583
|
+
pointer-events: none;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
.LogReviewer-inner-container {
|
|
2587
|
+
/* Full screen, rounded modal-like look */
|
|
2588
|
+
display: flex;
|
|
2589
|
+
height: 100%;
|
|
2590
|
+
border: 0.05rem solid black;
|
|
2591
|
+
border-radius: 0.5rem;
|
|
2592
|
+
overflow: hidden;
|
|
2593
|
+
|
|
2594
|
+
/* Place contents in flex column */
|
|
2595
|
+
flex-direction: column;
|
|
2596
|
+
|
|
2597
|
+
/* Re-allow interaction */
|
|
2598
|
+
pointer-events: all;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
.LogReviewer-header {
|
|
2602
|
+
/* Elements in flex row */
|
|
2603
|
+
display: flex;
|
|
2604
|
+
flex-direction: row;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
.LogReviewer-header-title {
|
|
2608
|
+
/* Take up remaining width */
|
|
2609
|
+
flex-grow: 1;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
.LogReviewer-contents {
|
|
2613
|
+
/* Take up remaining height */
|
|
2614
|
+
flex-grow: 1;
|
|
2615
|
+
|
|
2616
|
+
/* Vertical scroll */
|
|
2617
|
+
overflow-y: auto;
|
|
2618
|
+
}
|
|
2619
|
+
`;
|
|
2620
|
+
/*------------------------------------------------------------------------*/
|
|
2621
|
+
/* Static Functions */
|
|
2622
|
+
/*------------------------------------------------------------------------*/
|
|
2623
|
+
/**
|
|
2624
|
+
* Turn a machine-readable name into a human-readable name
|
|
2625
|
+
* @author Gabe Abrams
|
|
2626
|
+
* @param name machine-readable name
|
|
2627
|
+
* @returns human-readable name
|
|
2628
|
+
*/
|
|
2629
|
+
const genHumanReadableName = (machineReadableName) => {
|
|
2630
|
+
let humanReadableName = '';
|
|
2631
|
+
// Add chars and spaces
|
|
2632
|
+
const chars = machineReadableName.split('');
|
|
2633
|
+
chars.forEach((char) => {
|
|
2634
|
+
if (/[A-Z]/.test(char)) {
|
|
2635
|
+
// Uppercase! Add a space before
|
|
2636
|
+
humanReadableName += ' ';
|
|
2637
|
+
}
|
|
2638
|
+
humanReadableName += chars;
|
|
2639
|
+
});
|
|
2640
|
+
// Trim and return
|
|
2641
|
+
return humanReadableName.trim();
|
|
2642
|
+
};
|
|
2643
|
+
/* ------------- Actions ------------ */
|
|
2644
|
+
// Types of actions
|
|
2645
|
+
var ActionType;
|
|
2646
|
+
(function (ActionType) {
|
|
2647
|
+
// Show the loading bar
|
|
2648
|
+
ActionType["StartLoading"] = "start-loading";
|
|
2649
|
+
// Finish loading one or more months of logs
|
|
2650
|
+
ActionType["FinishLoading"] = "finish-loading";
|
|
2651
|
+
// Reset filters to initial values
|
|
2652
|
+
ActionType["ResetFilters"] = "reset-filters";
|
|
2653
|
+
// Choose a filter drawer to toggle
|
|
2654
|
+
ActionType["ToggleFilterDrawer"] = "toggle-filter-drawer";
|
|
2655
|
+
// Hide filter drawer
|
|
2656
|
+
ActionType["HideFilterDrawer"] = "hide-filter-drawer";
|
|
2657
|
+
// Handle the date filter state
|
|
2658
|
+
ActionType["UpdateDateFilterState"] = "update-date-filter-state";
|
|
2659
|
+
// Update the context filter state
|
|
2660
|
+
ActionType["UpdateContextFilterState"] = "update-context-filter-state";
|
|
2661
|
+
// Update the tag filter state
|
|
2662
|
+
ActionType["UpdateTagFilterState"] = "update-tag-filter-state";
|
|
2663
|
+
// Update the action and error filter state
|
|
2664
|
+
ActionType["UpdateActionErrorFilterState"] = "update-action-error-filter-state";
|
|
2665
|
+
// Update the advanced filter state
|
|
2666
|
+
ActionType["UpdateAdvancedFilterState"] = "update-advanced-filter-state";
|
|
2667
|
+
})(ActionType || (ActionType = {}));
|
|
2668
|
+
/**
|
|
2669
|
+
* Reducer that executes actions
|
|
2670
|
+
* @author Gabe Abrams
|
|
2671
|
+
* @param state current state
|
|
2672
|
+
* @param action action to execute
|
|
2673
|
+
*/
|
|
2674
|
+
const reducer = (state, action) => {
|
|
2675
|
+
switch (action.type) {
|
|
2676
|
+
case ActionType.StartLoading: {
|
|
2677
|
+
return Object.assign(Object.assign({}, state), { loading: true });
|
|
2678
|
+
}
|
|
2679
|
+
case ActionType.FinishLoading: {
|
|
2680
|
+
return Object.assign(Object.assign({}, state), { loading: false, logMap: action.logMap });
|
|
2681
|
+
}
|
|
2682
|
+
case ActionType.ToggleFilterDrawer: {
|
|
2683
|
+
return Object.assign(Object.assign({}, state), { expandedFilterDrawer: (state.expandedFilterDrawer === action.filterDrawer
|
|
2684
|
+
? undefined // hide
|
|
2685
|
+
: action.filterDrawer) });
|
|
2686
|
+
}
|
|
2687
|
+
case ActionType.HideFilterDrawer: {
|
|
2688
|
+
return Object.assign(Object.assign({}, state), { expandedFilterDrawer: undefined });
|
|
2689
|
+
}
|
|
2690
|
+
case ActionType.ResetFilters: {
|
|
2691
|
+
return Object.assign(Object.assign({}, state), { dateFilterState: action.initDateFilterState, contextFilterState: action.initContextFilterState, tagFilterState: action.initTagFilterState, actionErrorFilterState: action.initActionErrorFilterState, advancedFilterState: action.initAdvancedFilterState });
|
|
2692
|
+
}
|
|
2693
|
+
case ActionType.UpdateDateFilterState: {
|
|
2694
|
+
return Object.assign(Object.assign({}, state), { dateFilterState: action.dateFilterState });
|
|
2695
|
+
}
|
|
2696
|
+
case ActionType.UpdateContextFilterState: {
|
|
2697
|
+
return Object.assign(Object.assign({}, state), { contextFilterState: action.contextFilterState });
|
|
2698
|
+
}
|
|
2699
|
+
case ActionType.UpdateTagFilterState: {
|
|
2700
|
+
return Object.assign(Object.assign({}, state), { tagFilterState: action.tagFilterState });
|
|
2701
|
+
}
|
|
2702
|
+
case ActionType.UpdateActionErrorFilterState: {
|
|
2703
|
+
return Object.assign(Object.assign({}, state), { actionErrorFilterState: action.actionErrorFilterState });
|
|
2704
|
+
}
|
|
2705
|
+
case ActionType.UpdateAdvancedFilterState: {
|
|
2706
|
+
return Object.assign(Object.assign({}, state), { advancedFilterState: action.advancedFilterState });
|
|
2707
|
+
}
|
|
2708
|
+
default: {
|
|
2709
|
+
return state;
|
|
2710
|
+
}
|
|
2085
2711
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2712
|
+
};
|
|
2713
|
+
/*------------------------------------------------------------------------*/
|
|
2714
|
+
/* Component */
|
|
2715
|
+
/*------------------------------------------------------------------------*/
|
|
2716
|
+
const LogReviewer = (props) => {
|
|
2717
|
+
/*------------------------------------------------------------------------*/
|
|
2718
|
+
/* Setup */
|
|
2719
|
+
/*------------------------------------------------------------------------*/
|
|
2720
|
+
var _a, _b, _c, _d, _e;
|
|
2721
|
+
/* -------------- Props ------------- */
|
|
2722
|
+
// Destructure props
|
|
2723
|
+
const { LogMetadata, onClose, } = props;
|
|
2724
|
+
/* -------------- State ------------- */
|
|
2725
|
+
// Create initial date filter state
|
|
2726
|
+
const today = getTimeInfoInET();
|
|
2727
|
+
const initStartDate = {
|
|
2728
|
+
year: today.year,
|
|
2729
|
+
month: today.month,
|
|
2730
|
+
day: 1,
|
|
2731
|
+
};
|
|
2732
|
+
const initEndDate = {
|
|
2733
|
+
year: today.year,
|
|
2734
|
+
month: today.month,
|
|
2735
|
+
day: today.day,
|
|
2736
|
+
};
|
|
2737
|
+
const initDateFilterState = {
|
|
2738
|
+
startDate: initStartDate,
|
|
2739
|
+
endDate: initEndDate,
|
|
2740
|
+
};
|
|
2741
|
+
// Create initial context filter state
|
|
2742
|
+
const initContextFilterState = {};
|
|
2743
|
+
Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
|
|
2744
|
+
var _a, _b;
|
|
2745
|
+
const contextValue = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
|
|
2746
|
+
if (typeof contextValue === 'string') {
|
|
2747
|
+
// Case: no subcontexts
|
|
2748
|
+
initContextFilterState[contextValue] = true;
|
|
2749
|
+
}
|
|
2750
|
+
else {
|
|
2751
|
+
// Case: subcontexts exist
|
|
2752
|
+
initContextFilterState[contextValue._] = {};
|
|
2753
|
+
Object.values(((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {})[context]).forEach((subcontext) => {
|
|
2754
|
+
const subcontextValue = contextValue[subcontext];
|
|
2755
|
+
initContextFilterState[contextValue._][subcontextValue] = true;
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
// Create initial tag filter state
|
|
2760
|
+
const initTagFilterState = {};
|
|
2761
|
+
Object.values((_b = LogMetadata.Tag) !== null && _b !== void 0 ? _b : {}).forEach((tagValue) => {
|
|
2762
|
+
initTagFilterState[tagValue] = true;
|
|
2763
|
+
});
|
|
2764
|
+
// Create advanced filter state
|
|
2765
|
+
const initAdvancedFilterState = {
|
|
2766
|
+
userFirstName: '',
|
|
2767
|
+
userLastName: '',
|
|
2768
|
+
userEmail: '',
|
|
2769
|
+
userId: '',
|
|
2770
|
+
includeLearners: true,
|
|
2771
|
+
includeTTMs: true,
|
|
2772
|
+
includeAdmins: true,
|
|
2773
|
+
courseId: '',
|
|
2774
|
+
courseName: '',
|
|
2775
|
+
isMobile: undefined,
|
|
2776
|
+
source: undefined,
|
|
2777
|
+
routePath: '',
|
|
2778
|
+
routeTemplate: '',
|
|
2779
|
+
};
|
|
2780
|
+
// Create action and error filter state
|
|
2781
|
+
const initActionErrorFilterState = {
|
|
2782
|
+
type: undefined,
|
|
2783
|
+
errorMessage: '',
|
|
2784
|
+
errorCode: '',
|
|
2785
|
+
target: {},
|
|
2786
|
+
action: {},
|
|
2787
|
+
};
|
|
2788
|
+
Object.values((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {}).forEach((target) => {
|
|
2789
|
+
initActionErrorFilterState.target[target] = true;
|
|
2790
|
+
});
|
|
2791
|
+
Object.values(LogAction$1).forEach((action) => {
|
|
2792
|
+
initActionErrorFilterState.action[action] = true;
|
|
2793
|
+
});
|
|
2794
|
+
// Initial state
|
|
2795
|
+
const initialState = {
|
|
2796
|
+
loading: true,
|
|
2797
|
+
logMap: {},
|
|
2798
|
+
expandedFilterDrawer: undefined,
|
|
2799
|
+
dateFilterState: initDateFilterState,
|
|
2800
|
+
contextFilterState: initContextFilterState,
|
|
2801
|
+
tagFilterState: initTagFilterState,
|
|
2802
|
+
actionErrorFilterState: initActionErrorFilterState,
|
|
2803
|
+
advancedFilterState: initAdvancedFilterState,
|
|
2804
|
+
};
|
|
2805
|
+
// Initialize state
|
|
2806
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
2807
|
+
// Destructure common state
|
|
2808
|
+
const { loading, logMap, expandedFilterDrawer, dateFilterState, contextFilterState, tagFilterState, actionErrorFilterState, advancedFilterState, } = state;
|
|
2809
|
+
/*------------------------------------------------------------------------*/
|
|
2810
|
+
/* Component Functions */
|
|
2811
|
+
/*------------------------------------------------------------------------*/
|
|
2812
|
+
/**
|
|
2813
|
+
* Get the list of year/month combos that need to be loaded given a new
|
|
2814
|
+
* start or end date and the existing logMap
|
|
2815
|
+
* @author Gabe Abrams
|
|
2816
|
+
* @param newDateFilterState the new date filter state
|
|
2817
|
+
* @returns list of year/month combos that need to be loaded
|
|
2818
|
+
*/
|
|
2819
|
+
const listMonthsToLoad = (newDateFilterState) => {
|
|
2820
|
+
// List of year/month combos that need to be loaded
|
|
2821
|
+
const toLoad = [];
|
|
2822
|
+
// Loop through dates
|
|
2823
|
+
let year = newDateFilterState.startDate.year;
|
|
2824
|
+
let month = newDateFilterState.startDate.month;
|
|
2825
|
+
while (
|
|
2826
|
+
// Earlier year
|
|
2827
|
+
(year <= newDateFilterState.endDate.year)
|
|
2828
|
+
// Current year but included month
|
|
2829
|
+
|| (year === newDateFilterState.endDate.year
|
|
2830
|
+
&& month <= newDateFilterState.endDate.month)) {
|
|
2831
|
+
// Add to list
|
|
2832
|
+
toLoad.push({
|
|
2833
|
+
year,
|
|
2834
|
+
month,
|
|
2835
|
+
});
|
|
2836
|
+
// Increment
|
|
2837
|
+
month += 1;
|
|
2838
|
+
if (month > 12) {
|
|
2839
|
+
month -= 12;
|
|
2840
|
+
year += 1;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
// Return
|
|
2844
|
+
return toLoad;
|
|
2845
|
+
};
|
|
2846
|
+
/**
|
|
2847
|
+
* Handle updated start/end dates (updates state, loads if necessary)
|
|
2848
|
+
* @author Gabe Abrams
|
|
2849
|
+
* @param newDateFilterState the new date filter state
|
|
2850
|
+
*/
|
|
2851
|
+
const handleDateRangeUpdated = (newDateFilterState) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2852
|
+
// Update state
|
|
2853
|
+
dispatch({
|
|
2854
|
+
type: ActionType.UpdateDateFilterState,
|
|
2855
|
+
dateFilterState: newDateFilterState,
|
|
2856
|
+
});
|
|
2857
|
+
// Check which year/month combos we need to load
|
|
2858
|
+
const toLoad = listMonthsToLoad(newDateFilterState);
|
|
2859
|
+
// If nothing to load, finished
|
|
2860
|
+
if (toLoad.length === 0) {
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
// Start loading
|
|
2864
|
+
dispatch({
|
|
2865
|
+
type: ActionType.StartLoading,
|
|
2866
|
+
});
|
|
2867
|
+
// Load required months
|
|
2868
|
+
try {
|
|
2869
|
+
for (let i = 0; i < toLoad.length; i++) {
|
|
2870
|
+
// Destructure
|
|
2871
|
+
const { year, month } = toLoad[i];
|
|
2872
|
+
// Load
|
|
2873
|
+
const logs = yield visitServerEndpoint({
|
|
2874
|
+
path: `${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/${year}/months/${month}`,
|
|
2875
|
+
method: 'GET',
|
|
2094
2876
|
});
|
|
2877
|
+
// Add to map
|
|
2878
|
+
if (!logMap[year]) {
|
|
2879
|
+
logMap[year] = {};
|
|
2880
|
+
}
|
|
2881
|
+
logMap[year][month] = logs;
|
|
2095
2882
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2883
|
+
}
|
|
2884
|
+
catch (err) {
|
|
2885
|
+
return showFatalError(err);
|
|
2886
|
+
}
|
|
2887
|
+
// Finish loading
|
|
2888
|
+
dispatch({
|
|
2889
|
+
type: ActionType.FinishLoading,
|
|
2890
|
+
logMap,
|
|
2891
|
+
});
|
|
2892
|
+
});
|
|
2893
|
+
/*------------------------------------------------------------------------*/
|
|
2894
|
+
/* Lifecycle Functions */
|
|
2895
|
+
/*------------------------------------------------------------------------*/
|
|
2896
|
+
/**
|
|
2897
|
+
* Mount
|
|
2898
|
+
* @author Gabe Abrams
|
|
2899
|
+
*/
|
|
2900
|
+
useEffect(() => {
|
|
2901
|
+
// Perform initial load
|
|
2902
|
+
handleDateRangeUpdated(dateFilterState);
|
|
2903
|
+
}, []);
|
|
2904
|
+
/*------------------------------------------------------------------------*/
|
|
2905
|
+
/* Render */
|
|
2906
|
+
/*------------------------------------------------------------------------*/
|
|
2907
|
+
/*----------------------------------------*/
|
|
2908
|
+
/* Main UI */
|
|
2909
|
+
/*----------------------------------------*/
|
|
2910
|
+
// Body that will be filled with the contents of the panel
|
|
2911
|
+
let body;
|
|
2912
|
+
/* ------------- Loading ------------ */
|
|
2913
|
+
if (loading) {
|
|
2914
|
+
body = (React.createElement("div", { className: "text-center p-5" },
|
|
2915
|
+
React.createElement(LoadingSpinner, null)));
|
|
2916
|
+
}
|
|
2917
|
+
/* ------------ Review UI ----------- */
|
|
2918
|
+
if (!loading) {
|
|
2919
|
+
/*----------------------------------------*/
|
|
2920
|
+
/* Filters */
|
|
2921
|
+
/*----------------------------------------*/
|
|
2922
|
+
// Filter toggle
|
|
2923
|
+
const filterToggles = (React.createElement("div", { className: "LogReviewer-filter-toggles d-flex align-items-center justify-content-center" },
|
|
2924
|
+
React.createElement("h3", { className: "m-0" }, "Filters:"),
|
|
2925
|
+
React.createElement("div", { className: "LogReviewer-filter-toggle-buttons" },
|
|
2926
|
+
React.createElement("button", { type: "button", id: "LogReviewer-toggle-date-filter-drawer", className: `btn btn-${FilterDrawer.Date === expandedFilterDrawer} me-2`, "aria-label": "toggle date filter drawer", onClick: () => {
|
|
2927
|
+
dispatch({
|
|
2928
|
+
type: ActionType.ToggleFilterDrawer,
|
|
2929
|
+
filterDrawer: FilterDrawer.Date,
|
|
2930
|
+
});
|
|
2931
|
+
} },
|
|
2932
|
+
React.createElement(FontAwesomeIcon, { icon: faCalendar, className: "me-2" }),
|
|
2933
|
+
"Date"),
|
|
2934
|
+
React.createElement("button", { type: "button", id: "LogReviewer-toggle-context-filter-drawer", className: `btn btn-${FilterDrawer.Context === expandedFilterDrawer} me-2`, "aria-label": "toggle context filter drawer", onClick: () => {
|
|
2935
|
+
dispatch({
|
|
2936
|
+
type: ActionType.ToggleFilterDrawer,
|
|
2937
|
+
filterDrawer: FilterDrawer.Context,
|
|
2938
|
+
});
|
|
2939
|
+
} },
|
|
2940
|
+
React.createElement(FontAwesomeIcon, { icon: faCircle, className: "me-2" }),
|
|
2941
|
+
"Context"),
|
|
2942
|
+
React.createElement("button", { type: "button", id: "LogReviewer-toggle-tag-filter-drawer", className: `btn btn-${FilterDrawer.Tag === expandedFilterDrawer} me-2`, "aria-label": "toggle tag filter drawer", onClick: () => {
|
|
2943
|
+
dispatch({
|
|
2944
|
+
type: ActionType.ToggleFilterDrawer,
|
|
2945
|
+
filterDrawer: FilterDrawer.Tag,
|
|
2946
|
+
});
|
|
2947
|
+
} },
|
|
2948
|
+
React.createElement(FontAwesomeIcon, { icon: faTag, className: "me-2" }),
|
|
2949
|
+
"Tag"),
|
|
2950
|
+
React.createElement("button", { type: "button", id: "LogReviewer-toggle-action-filter-drawer", className: `btn btn-${FilterDrawer.Action === expandedFilterDrawer} me-2`, "aria-label": "toggle action and error filter drawer", onClick: () => {
|
|
2951
|
+
dispatch({
|
|
2952
|
+
type: ActionType.ToggleFilterDrawer,
|
|
2953
|
+
filterDrawer: FilterDrawer.Action,
|
|
2954
|
+
});
|
|
2955
|
+
} },
|
|
2956
|
+
React.createElement(FontAwesomeIcon, { icon: faHammer, className: "me-2" }),
|
|
2957
|
+
"Action"),
|
|
2958
|
+
React.createElement("button", { type: "button", id: "LogReviewer-toggle-advanced-filter-drawer", className: `btn btn-${FilterDrawer.Advanced === expandedFilterDrawer}`, "aria-label": "toggle advanced filter drawer", onClick: () => {
|
|
2959
|
+
dispatch({
|
|
2960
|
+
type: ActionType.ToggleFilterDrawer,
|
|
2961
|
+
filterDrawer: FilterDrawer.Advanced,
|
|
2962
|
+
});
|
|
2963
|
+
} },
|
|
2964
|
+
React.createElement(FontAwesomeIcon, { icon: faList, className: "me-2" }),
|
|
2965
|
+
"Advanced"))));
|
|
2966
|
+
// Filter drawer
|
|
2967
|
+
let filterDrawer;
|
|
2968
|
+
if (expandedFilterDrawer) {
|
|
2969
|
+
if (expandedFilterDrawer === FilterDrawer.Date) {
|
|
2970
|
+
filterDrawer = (React.createElement(TabBox, { title: "Date" },
|
|
2971
|
+
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) => {
|
|
2972
|
+
dispatch({
|
|
2973
|
+
type: ActionType.UpdateDateFilterState,
|
|
2974
|
+
dateFilterState: Object.assign(Object.assign({}, dateFilterState), { startDate: { month, day, year } }),
|
|
2975
|
+
});
|
|
2976
|
+
} }),
|
|
2977
|
+
' ',
|
|
2978
|
+
"to",
|
|
2979
|
+
' ',
|
|
2980
|
+
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) => {
|
|
2981
|
+
dispatch({
|
|
2982
|
+
type: ActionType.UpdateDateFilterState,
|
|
2983
|
+
dateFilterState: Object.assign(Object.assign({}, dateFilterState), { endDate: { month, day, year } }),
|
|
2984
|
+
});
|
|
2985
|
+
} })));
|
|
2986
|
+
}
|
|
2987
|
+
else if (expandedFilterDrawer === FilterDrawer.Context) {
|
|
2988
|
+
// Create item picker items
|
|
2989
|
+
const pickableItems = (Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {})
|
|
2990
|
+
.map((context) => {
|
|
2991
|
+
var _a;
|
|
2992
|
+
const value = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
|
|
2993
|
+
if (typeof value === 'string') {
|
|
2994
|
+
// No subcategories
|
|
2995
|
+
const item = {
|
|
2996
|
+
id: context,
|
|
2997
|
+
name: genHumanReadableName(context),
|
|
2998
|
+
isGroup: false,
|
|
2999
|
+
checked: !!contextFilterState[context],
|
|
3000
|
+
};
|
|
3001
|
+
return item;
|
|
3002
|
+
}
|
|
3003
|
+
// Has subcategories
|
|
3004
|
+
const children = (Object.keys(value)
|
|
3005
|
+
.filter((subcontext) => {
|
|
3006
|
+
return subcontext !== '_';
|
|
3007
|
+
})
|
|
3008
|
+
.map((subcontext) => {
|
|
3009
|
+
return {
|
|
3010
|
+
id: `${context}-${subcontext}`,
|
|
3011
|
+
name: genHumanReadableName(subcontext),
|
|
3012
|
+
isGroup: false,
|
|
3013
|
+
checked: !!value[subcontext],
|
|
3014
|
+
};
|
|
3015
|
+
}));
|
|
3016
|
+
const item = {
|
|
3017
|
+
id: context,
|
|
3018
|
+
name: context,
|
|
3019
|
+
isGroup: true,
|
|
3020
|
+
children,
|
|
3021
|
+
};
|
|
3022
|
+
return item;
|
|
3023
|
+
}));
|
|
3024
|
+
// Create filter UI
|
|
3025
|
+
filterDrawer = (React.createElement(ItemPicker, { title: "Context", items: pickableItems, onChanged: (updatedItems) => {
|
|
3026
|
+
// Update our state
|
|
3027
|
+
updatedItems.forEach((pickableItem) => {
|
|
3028
|
+
if (pickableItem.isGroup) {
|
|
3029
|
+
// Has subcontexts
|
|
3030
|
+
pickableItem.children.forEach((subcontextItem) => {
|
|
3031
|
+
contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
else {
|
|
3035
|
+
// No subcontexts
|
|
3036
|
+
contextFilterState[pickableItem.id] = (pickableItem.checked);
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
3039
|
+
dispatch({
|
|
3040
|
+
type: ActionType.UpdateContextFilterState,
|
|
3041
|
+
contextFilterState,
|
|
3042
|
+
});
|
|
3043
|
+
} }));
|
|
3044
|
+
}
|
|
3045
|
+
else if (expandedFilterDrawer === FilterDrawer.Tag) {
|
|
3046
|
+
// Create filter UI
|
|
3047
|
+
filterDrawer = (React.createElement(TabBox, { title: "Tags" }, Object.keys(tagFilterState)
|
|
3048
|
+
.map((tag, i) => {
|
|
3049
|
+
const description = genHumanReadableName(tag);
|
|
3050
|
+
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) => {
|
|
3051
|
+
tagFilterState[tag] = checked;
|
|
3052
|
+
} }));
|
|
3053
|
+
})));
|
|
3054
|
+
}
|
|
3055
|
+
else if (expandedFilterDrawer === FilterDrawer.Action) {
|
|
3056
|
+
// Create filter UI
|
|
3057
|
+
filterDrawer = (React.createElement(React.Fragment, null,
|
|
3058
|
+
React.createElement(TabBox, { title: "Log Type" },
|
|
3059
|
+
React.createElement(RadioButton, { id: "LogReviewer-type-all", text: "All Logs", onSelected: () => {
|
|
3060
|
+
actionErrorFilterState.type = undefined;
|
|
3061
|
+
dispatch({
|
|
3062
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
3063
|
+
actionErrorFilterState,
|
|
3064
|
+
});
|
|
3065
|
+
}, ariaLabel: "show logs of all types", selected: actionErrorFilterState.type === undefined }),
|
|
3066
|
+
React.createElement(RadioButton, { id: "LogReviewer-type-action-only", text: "Action Logs Only", onSelected: () => {
|
|
3067
|
+
actionErrorFilterState.type = LogType$1.Action;
|
|
3068
|
+
dispatch({
|
|
3069
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
3070
|
+
actionErrorFilterState,
|
|
3071
|
+
});
|
|
3072
|
+
}, ariaLabel: "only show action logs", selected: actionErrorFilterState.type === LogType$1.Action }),
|
|
3073
|
+
React.createElement(RadioButton, { id: "LogReviewer-type-error-only", text: "Action Error Only", onSelected: () => {
|
|
3074
|
+
actionErrorFilterState.type = LogType$1.Error;
|
|
3075
|
+
dispatch({
|
|
3076
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
3077
|
+
actionErrorFilterState,
|
|
3078
|
+
});
|
|
3079
|
+
}, ariaLabel: "only show error logs", selected: actionErrorFilterState.type === LogType$1.Error, noMarginOnRight: true })),
|
|
3080
|
+
(actionErrorFilterState.type === undefined
|
|
3081
|
+
|| actionErrorFilterState.type === LogType$1.Action) && (React.createElement(TabBox, { title: "Action Log Details" },
|
|
3082
|
+
React.createElement(ButtonInputGroup, { label: "Action" }, Object.keys(LogAction$1)
|
|
3083
|
+
.map((action, i) => {
|
|
3084
|
+
const description = genHumanReadableName(action);
|
|
3085
|
+
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) => {
|
|
3086
|
+
actionErrorFilterState.action[action] = checked;
|
|
3087
|
+
dispatch({
|
|
3088
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
3089
|
+
actionErrorFilterState,
|
|
3090
|
+
});
|
|
3091
|
+
} }));
|
|
3092
|
+
})),
|
|
3093
|
+
React.createElement(ButtonInputGroup, { label: "Target" }, Object.keys((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {})
|
|
3094
|
+
.map((target, i) => {
|
|
3095
|
+
var _a;
|
|
3096
|
+
const description = genHumanReadableName(target);
|
|
3097
|
+
return (React.createElement(CheckboxButton, { id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, onChanged: (checked) => {
|
|
3098
|
+
actionErrorFilterState.target[target] = checked;
|
|
3099
|
+
dispatch({
|
|
3100
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
3101
|
+
actionErrorFilterState,
|
|
3102
|
+
});
|
|
3103
|
+
}, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
|
|
3104
|
+
})))),
|
|
3105
|
+
(actionErrorFilterState.type === undefined
|
|
3106
|
+
|| actionErrorFilterState.type === LogType$1.Error) && (React.createElement(TabBox, { title: "Error Log Details" },
|
|
3107
|
+
React.createElement("div", { className: "input-group mb-2" },
|
|
3108
|
+
React.createElement("span", { className: "input-group-text" }, "Error Message"),
|
|
3109
|
+
React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for error message", value: actionErrorFilterState.errorMessage, onChange: (e) => {
|
|
3110
|
+
actionErrorFilterState.errorMessage = e.target.value;
|
|
3111
|
+
dispatch({
|
|
3112
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
3113
|
+
actionErrorFilterState,
|
|
3114
|
+
});
|
|
3115
|
+
} })),
|
|
3116
|
+
React.createElement("div", { className: "input-group mb-2" },
|
|
3117
|
+
React.createElement("span", { className: "input-group-text" }, "Error Code"),
|
|
3118
|
+
React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorMessage, onChange: (e) => {
|
|
3119
|
+
actionErrorFilterState.errorCode = ((e.target.value)
|
|
3120
|
+
.trim()
|
|
3121
|
+
.toUpperCase());
|
|
3122
|
+
dispatch({
|
|
3123
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
3124
|
+
actionErrorFilterState,
|
|
3125
|
+
});
|
|
3126
|
+
} }))))));
|
|
3127
|
+
}
|
|
3128
|
+
else if (expandedFilterDrawer === FilterDrawer.Advanced) {
|
|
3129
|
+
// Create advanced filter ui
|
|
3130
|
+
filterDrawer = (React.createElement(React.Fragment, null,
|
|
3131
|
+
React.createElement(TabBox, { title: "User Info" },
|
|
3132
|
+
React.createElement("div", { className: "input-group mb-2" },
|
|
3133
|
+
React.createElement("span", { className: "input-group-text" }, "User First Name"),
|
|
3134
|
+
React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, onChange: (e) => {
|
|
3135
|
+
advancedFilterState.userFirstName = e.target.value;
|
|
3136
|
+
dispatch({
|
|
3137
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3138
|
+
advancedFilterState,
|
|
3139
|
+
});
|
|
3140
|
+
} })),
|
|
3141
|
+
React.createElement("div", { className: "input-group mb-2" },
|
|
3142
|
+
React.createElement("span", { className: "input-group-text" }, "User Last Name"),
|
|
3143
|
+
React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, onChange: (e) => {
|
|
3144
|
+
advancedFilterState.userLastName = e.target.value;
|
|
3145
|
+
dispatch({
|
|
3146
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3147
|
+
advancedFilterState,
|
|
3148
|
+
});
|
|
3149
|
+
} })),
|
|
3150
|
+
React.createElement("div", { className: "input-group mb-2" },
|
|
3151
|
+
React.createElement("span", { className: "input-group-text" }, "User Email"),
|
|
3152
|
+
React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user email", value: advancedFilterState.userEmail, onChange: (e) => {
|
|
3153
|
+
advancedFilterState.userEmail = ((e.target.value)
|
|
3154
|
+
.trim());
|
|
3155
|
+
dispatch({
|
|
3156
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3157
|
+
advancedFilterState,
|
|
3158
|
+
});
|
|
3159
|
+
} })),
|
|
3160
|
+
React.createElement("div", { className: "input-group mb-2" },
|
|
3161
|
+
React.createElement("span", { className: "input-group-text" }, "User Canvas Id"),
|
|
3162
|
+
React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, onChange: (e) => {
|
|
3163
|
+
const { value } = e.target;
|
|
3164
|
+
// Only update if value contains only numbers
|
|
3165
|
+
if (/^\d+$/.test(value)) {
|
|
3166
|
+
advancedFilterState.userId = ((e.target.value)
|
|
3167
|
+
.trim());
|
|
3168
|
+
}
|
|
3169
|
+
dispatch({
|
|
3170
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3171
|
+
advancedFilterState,
|
|
3172
|
+
});
|
|
3173
|
+
} })),
|
|
3174
|
+
React.createElement(ButtonInputGroup, { label: "Role" },
|
|
3175
|
+
React.createElement(CheckboxButton, { text: "Students", onChanged: (checked) => {
|
|
3176
|
+
advancedFilterState.includeLearners = checked;
|
|
3177
|
+
dispatch({
|
|
3178
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3179
|
+
advancedFilterState,
|
|
3180
|
+
});
|
|
3181
|
+
}, checked: advancedFilterState.includeLearners, ariaLabel: "show logs from students" }),
|
|
3182
|
+
React.createElement(CheckboxButton, { text: "Teaching Team Members", onChanged: (checked) => {
|
|
3183
|
+
advancedFilterState.includeTTMs = checked;
|
|
3184
|
+
dispatch({
|
|
3185
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3186
|
+
advancedFilterState,
|
|
3187
|
+
});
|
|
3188
|
+
}, checked: advancedFilterState.includeTTMs, ariaLabel: "show logs from teaching team members" }),
|
|
3189
|
+
React.createElement(CheckboxButton, { text: "Admins", onChanged: (checked) => {
|
|
3190
|
+
advancedFilterState.includeAdmins = checked;
|
|
3191
|
+
dispatch({
|
|
3192
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3193
|
+
advancedFilterState,
|
|
3194
|
+
});
|
|
3195
|
+
}, checked: advancedFilterState.includeAdmins, ariaLabel: "show logs from admins" }))),
|
|
3196
|
+
React.createElement(TabBox, { title: "Course Info" },
|
|
3197
|
+
React.createElement("div", { className: "input-group mb-2" },
|
|
3198
|
+
React.createElement("span", { className: "input-group-text" }, "Course Name"),
|
|
3199
|
+
React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, onChange: (e) => {
|
|
3200
|
+
advancedFilterState.courseName = e.target.value;
|
|
3201
|
+
dispatch({
|
|
3202
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3203
|
+
advancedFilterState,
|
|
3204
|
+
});
|
|
3205
|
+
} })),
|
|
3206
|
+
React.createElement("div", { className: "input-group mb-2" },
|
|
3207
|
+
React.createElement("span", { className: "input-group-text" }, "Course Canvas Id"),
|
|
3208
|
+
React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, onChange: (e) => {
|
|
3209
|
+
const { value } = e.target;
|
|
3210
|
+
// Only update if value contains only numbers
|
|
3211
|
+
if (/^\d+$/.test(value)) {
|
|
3212
|
+
advancedFilterState.courseId = ((e.target.value)
|
|
3213
|
+
.trim());
|
|
3214
|
+
}
|
|
3215
|
+
dispatch({
|
|
3216
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3217
|
+
advancedFilterState,
|
|
3218
|
+
});
|
|
3219
|
+
} }))),
|
|
3220
|
+
React.createElement(TabBox, { title: "Device Info" },
|
|
3221
|
+
React.createElement(ButtonInputGroup, { label: "Device Type" },
|
|
3222
|
+
React.createElement(RadioButton, { text: "All Devices", ariaLabel: "show logs from all devices", selected: advancedFilterState.isMobile === undefined, onSelected: () => {
|
|
3223
|
+
advancedFilterState.isMobile = undefined;
|
|
3224
|
+
dispatch({
|
|
3225
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3226
|
+
advancedFilterState,
|
|
3227
|
+
});
|
|
3228
|
+
} }),
|
|
3229
|
+
React.createElement(RadioButton, { text: "Mobile Only", ariaLabel: "show logs from mobile devices", selected: advancedFilterState.isMobile === true, onSelected: () => {
|
|
3230
|
+
advancedFilterState.isMobile = true;
|
|
3231
|
+
dispatch({
|
|
3232
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3233
|
+
advancedFilterState,
|
|
3234
|
+
});
|
|
3235
|
+
} }),
|
|
3236
|
+
React.createElement(RadioButton, { text: "Desktop Only", ariaLabel: "show logs from desktop devices", selected: advancedFilterState.isMobile === false, onSelected: () => {
|
|
3237
|
+
advancedFilterState.isMobile = false;
|
|
3238
|
+
dispatch({
|
|
3239
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3240
|
+
advancedFilterState,
|
|
3241
|
+
});
|
|
3242
|
+
}, noMarginOnRight: true }))),
|
|
3243
|
+
React.createElement(TabBox, { title: "Source" },
|
|
3244
|
+
React.createElement(ButtonInputGroup, { label: "Source Type" },
|
|
3245
|
+
React.createElement(RadioButton, { text: "Both", ariaLabel: "show logs from all sources", selected: advancedFilterState.source === undefined, onSelected: () => {
|
|
3246
|
+
advancedFilterState.source = undefined;
|
|
3247
|
+
dispatch({
|
|
3248
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3249
|
+
advancedFilterState,
|
|
3250
|
+
});
|
|
3251
|
+
} }),
|
|
3252
|
+
React.createElement(RadioButton, { text: "Client Only", ariaLabel: "show logs from client source", selected: advancedFilterState.source === LogSource$1.Client, onSelected: () => {
|
|
3253
|
+
advancedFilterState.source = LogSource$1.Client;
|
|
3254
|
+
dispatch({
|
|
3255
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3256
|
+
advancedFilterState,
|
|
3257
|
+
});
|
|
3258
|
+
} }),
|
|
3259
|
+
React.createElement(RadioButton, { text: "Server Only", ariaLabel: "show logs from server source", selected: advancedFilterState.source === LogSource$1.Server, onSelected: () => {
|
|
3260
|
+
advancedFilterState.source = LogSource$1.Server;
|
|
3261
|
+
dispatch({
|
|
3262
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3263
|
+
advancedFilterState,
|
|
3264
|
+
});
|
|
3265
|
+
}, noMarginOnRight: true })),
|
|
3266
|
+
React.createElement("div", { className: "input-group mb-2" },
|
|
3267
|
+
React.createElement("span", { className: "input-group-text" }, "Server Route Path"),
|
|
3268
|
+
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) => {
|
|
3269
|
+
advancedFilterState.courseName = ((e.target.value)
|
|
3270
|
+
.trim());
|
|
3271
|
+
dispatch({
|
|
3272
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3273
|
+
advancedFilterState,
|
|
3274
|
+
});
|
|
3275
|
+
} })),
|
|
3276
|
+
React.createElement("div", { className: "input-group mb-2" },
|
|
3277
|
+
React.createElement("span", { className: "input-group-text" }, "Server Route Template"),
|
|
3278
|
+
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) => {
|
|
3279
|
+
advancedFilterState.courseName = ((e.target.value)
|
|
3280
|
+
.trim());
|
|
3281
|
+
dispatch({
|
|
3282
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
3283
|
+
advancedFilterState,
|
|
3284
|
+
});
|
|
3285
|
+
} })))));
|
|
2102
3286
|
}
|
|
2103
|
-
// Never return (don't continue execution)
|
|
2104
|
-
yield new Promise(() => {
|
|
2105
|
-
// Promise that never returns
|
|
2106
|
-
});
|
|
2107
3287
|
}
|
|
2108
|
-
//
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
3288
|
+
// Filters UI
|
|
3289
|
+
const filters = (React.createElement(React.Fragment, null,
|
|
3290
|
+
filterToggles,
|
|
3291
|
+
filterDrawer && (React.createElement(Drawer, null, filterDrawer))));
|
|
3292
|
+
// Actually filter the logs
|
|
3293
|
+
// > Perform filters
|
|
3294
|
+
const logs = [];
|
|
3295
|
+
Object.keys(logMap).forEach((year) => {
|
|
3296
|
+
Object.keys(logMap).forEach((month) => {
|
|
3297
|
+
logMap[year][month].forEach((log) => {
|
|
3298
|
+
/* ----------- Date Filter ---------- */
|
|
3299
|
+
var _a;
|
|
3300
|
+
// Before start date
|
|
3301
|
+
if (
|
|
3302
|
+
// Previous year
|
|
3303
|
+
log.year < dateFilterState.startDate.year
|
|
3304
|
+
// Same year, earlier month
|
|
3305
|
+
|| ((log.year === dateFilterState.startDate.year)
|
|
3306
|
+
&& (log.month < dateFilterState.startDate.month))
|
|
3307
|
+
// Same year, same month, earlier day
|
|
3308
|
+
|| ((log.year === dateFilterState.startDate.year)
|
|
3309
|
+
&& (log.month === dateFilterState.startDate.month)
|
|
3310
|
+
&& (log.day < dateFilterState.startDate.day))) {
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
// After end date
|
|
3314
|
+
if (
|
|
3315
|
+
// Later year
|
|
3316
|
+
log.year > dateFilterState.endDate.year
|
|
3317
|
+
// Same year, later month
|
|
3318
|
+
|| ((log.year === dateFilterState.endDate.year)
|
|
3319
|
+
&& (log.month > dateFilterState.endDate.month))
|
|
3320
|
+
// Same year, same month, later day
|
|
3321
|
+
|| ((log.year === dateFilterState.endDate.year)
|
|
3322
|
+
&& (log.month === dateFilterState.endDate.month)
|
|
3323
|
+
&& (log.day > dateFilterState.endDate.day))) {
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
/* --------- Context Filter --------- */
|
|
3327
|
+
// Context doesn't match
|
|
3328
|
+
if (
|
|
3329
|
+
// Whole context is deselected
|
|
3330
|
+
contextFilterState[log.context] === false
|
|
3331
|
+
// None of the subcontexts are selected
|
|
3332
|
+
|| (Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
|
|
3333
|
+
.every((isSelected) => {
|
|
3334
|
+
return !isSelected;
|
|
3335
|
+
}))) {
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
// Subcontext doesn't match
|
|
3339
|
+
if (
|
|
3340
|
+
// Log has a subcontext
|
|
3341
|
+
log.subcontext
|
|
3342
|
+
// Context has subcontexts
|
|
3343
|
+
&& (contextFilterState[log.context]
|
|
3344
|
+
&& contextFilterState[log.context] !== false
|
|
3345
|
+
&& contextFilterState[log.context] !== true)
|
|
3346
|
+
// Subcontext is not selected
|
|
3347
|
+
&& !contextFilterState[log.context][log.subcontext]) {
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
/* -------------- Tags -------------- */
|
|
3351
|
+
// No tags match
|
|
3352
|
+
if (
|
|
3353
|
+
// Log has at least one tag
|
|
3354
|
+
log.tags.length > 0
|
|
3355
|
+
// No tags match
|
|
3356
|
+
&& (log.tags.every((tag) => {
|
|
3357
|
+
return !tagFilterState[tag];
|
|
3358
|
+
}))) {
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
/* ------- Actions and Errors ------- */
|
|
3362
|
+
// Log type doesn't match
|
|
3363
|
+
if (
|
|
3364
|
+
// Filter won't allow all types
|
|
3365
|
+
actionErrorFilterState.type !== undefined
|
|
3366
|
+
// Log type doesn't match
|
|
3367
|
+
&& actionErrorFilterState.type !== log.type) {
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
// Filter errors
|
|
3371
|
+
if (log.type === LogType$1.Error) {
|
|
3372
|
+
// Message doesn't match
|
|
3373
|
+
if (
|
|
3374
|
+
// Message exists
|
|
3375
|
+
log.errorMessage
|
|
3376
|
+
// Message filter exists
|
|
3377
|
+
&& actionErrorFilterState.errorMessage.trim().length > 0
|
|
3378
|
+
// Message doesn't match
|
|
3379
|
+
&& log.errorMessage.toLowerCase().includes(actionErrorFilterState.errorMessage.trim().toLowerCase())) {
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
// Code doesn't match
|
|
3383
|
+
if (
|
|
3384
|
+
// Code exists
|
|
3385
|
+
log.errorCode
|
|
3386
|
+
// Code filter exists
|
|
3387
|
+
&& actionErrorFilterState.errorCode.trim().length > 0
|
|
3388
|
+
// Code doesn't match
|
|
3389
|
+
&& log.errorCode.toUpperCase().includes(actionErrorFilterState.errorCode.trim().toUpperCase())) {
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
// Filter actions
|
|
3394
|
+
if (log.type === LogType$1.Action) {
|
|
3395
|
+
// Target isn't selected
|
|
3396
|
+
if (
|
|
3397
|
+
// Target exists
|
|
3398
|
+
log.target
|
|
3399
|
+
// Target isn't selected
|
|
3400
|
+
&& !actionErrorFilterState.target[log.target]) {
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
// Action
|
|
3404
|
+
if (
|
|
3405
|
+
// Action exists
|
|
3406
|
+
log.action
|
|
3407
|
+
// Action isn't selected
|
|
3408
|
+
&& !actionErrorFilterState.action[log.action]) {
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
/* --------- Advanced Filter -------- */
|
|
3413
|
+
// First name doesn't match
|
|
3414
|
+
if (
|
|
3415
|
+
// First name exists
|
|
3416
|
+
log.userFirstName
|
|
3417
|
+
// First name query doesn't match
|
|
3418
|
+
&& !log.userFirstName.toLowerCase().includes(advancedFilterState.userFirstName.toLowerCase().trim())) {
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
// Last name doesn't match
|
|
3422
|
+
if (
|
|
3423
|
+
// Last name exists
|
|
3424
|
+
log.userLastName
|
|
3425
|
+
// Last name query doesn't match
|
|
3426
|
+
&& !log.userLastName.toLowerCase().includes(advancedFilterState.userLastName.toLowerCase().trim())) {
|
|
3427
|
+
return;
|
|
3428
|
+
}
|
|
3429
|
+
// Email doesn't match
|
|
3430
|
+
if (
|
|
3431
|
+
// Email exists
|
|
3432
|
+
log.userEmail
|
|
3433
|
+
// Email query doesn't match
|
|
3434
|
+
&& !log.userEmail.toLowerCase().includes(advancedFilterState.userEmail.toLowerCase().trim())) {
|
|
3435
|
+
return;
|
|
3436
|
+
}
|
|
3437
|
+
// User id doesn't match
|
|
3438
|
+
if (
|
|
3439
|
+
// User id exists
|
|
3440
|
+
log.userId
|
|
3441
|
+
// User id doesn't match
|
|
3442
|
+
&& !String(log.userId).includes(advancedFilterState.userId.trim())) {
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
// Learner not allowed
|
|
3446
|
+
if (
|
|
3447
|
+
// User is a learner
|
|
3448
|
+
log.isLearner
|
|
3449
|
+
// Learners aren't included
|
|
3450
|
+
&& !advancedFilterState.includeLearners) {
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
// TTM not allowed
|
|
3454
|
+
if (
|
|
3455
|
+
// User is a ttm
|
|
3456
|
+
log.isTTM
|
|
3457
|
+
// TTMs aren't included
|
|
3458
|
+
&& !advancedFilterState.includeTTMs) {
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
// Admin not allowed
|
|
3462
|
+
if (
|
|
3463
|
+
// User is an admin
|
|
3464
|
+
log.isAdmin
|
|
3465
|
+
// Admins aren't included
|
|
3466
|
+
&& !advancedFilterState.includeAdmins) {
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
// Course Id doesn't match
|
|
3470
|
+
if (
|
|
3471
|
+
// Course Id exists
|
|
3472
|
+
log.courseId
|
|
3473
|
+
// Course Id doesn't match
|
|
3474
|
+
&& !String(log.courseId).includes(advancedFilterState.courseId.trim())) {
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
// Course name doesn't match
|
|
3478
|
+
if (
|
|
3479
|
+
// Course name exists
|
|
3480
|
+
log.courseName
|
|
3481
|
+
// Course name doesn't match
|
|
3482
|
+
&& !String(log.courseName).includes(advancedFilterState.courseName.trim())) {
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
// Mobile filter doesn't match
|
|
3486
|
+
if (
|
|
3487
|
+
// Mobile filter exists
|
|
3488
|
+
advancedFilterState.isMobile !== undefined
|
|
3489
|
+
// Device info exists
|
|
3490
|
+
&& log.device
|
|
3491
|
+
// Mobile filter doesn't match
|
|
3492
|
+
&& (advancedFilterState.isMobile === log.device.isMobile)) {
|
|
3493
|
+
return;
|
|
3494
|
+
}
|
|
3495
|
+
// Log source doesn't match
|
|
3496
|
+
if (
|
|
3497
|
+
// Source filter exists
|
|
3498
|
+
advancedFilterState.source !== undefined
|
|
3499
|
+
// Source info exists
|
|
3500
|
+
&& log.source
|
|
3501
|
+
// Source filter doesn't match
|
|
3502
|
+
&& (advancedFilterState.source !== log.source)) {
|
|
3503
|
+
return;
|
|
3504
|
+
}
|
|
3505
|
+
// Route path doesn't match (Only for server source)
|
|
3506
|
+
if (
|
|
3507
|
+
// Source is server
|
|
3508
|
+
(log.source === LogSource$1.Server)
|
|
3509
|
+
// Route path is being filtered
|
|
3510
|
+
&& (advancedFilterState.routePath.trim().length)
|
|
3511
|
+
// Route path doesn't match
|
|
3512
|
+
&& !(log.routePath.includes(advancedFilterState.routePath.trim()))) {
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
// Route template doesn't match (Only for server source)
|
|
3516
|
+
if (
|
|
3517
|
+
// Source is server
|
|
3518
|
+
(log.source === LogSource$1.Server)
|
|
3519
|
+
// Route template is being filtered
|
|
3520
|
+
&& (advancedFilterState.routeTemplate.trim().length)
|
|
3521
|
+
// Route template doesn't match
|
|
3522
|
+
&& !(log.routeTemplate.includes(advancedFilterState.routeTemplate.trim()))) {
|
|
3523
|
+
return;
|
|
3524
|
+
}
|
|
3525
|
+
/* -------------- Done -------------- */
|
|
3526
|
+
// Made it past all filters. Add to the list
|
|
3527
|
+
logs.push(log);
|
|
3528
|
+
});
|
|
3529
|
+
});
|
|
3530
|
+
});
|
|
3531
|
+
/*----------------------------------------*/
|
|
3532
|
+
/* Data */
|
|
3533
|
+
/*----------------------------------------*/
|
|
3534
|
+
// Create data table
|
|
3535
|
+
const columns = [
|
|
3536
|
+
{
|
|
3537
|
+
title: 'First Name',
|
|
3538
|
+
param: 'userFirstName',
|
|
3539
|
+
type: ParamType$1.String,
|
|
3540
|
+
},
|
|
3541
|
+
{
|
|
3542
|
+
title: 'Last Name',
|
|
3543
|
+
param: 'userLastName',
|
|
3544
|
+
type: ParamType$1.String,
|
|
3545
|
+
},
|
|
3546
|
+
{
|
|
3547
|
+
title: 'Email',
|
|
3548
|
+
param: 'userEmail',
|
|
3549
|
+
type: ParamType$1.String,
|
|
3550
|
+
},
|
|
3551
|
+
{
|
|
3552
|
+
title: 'Canvas Id',
|
|
3553
|
+
param: 'userId',
|
|
3554
|
+
type: ParamType$1.Int,
|
|
3555
|
+
},
|
|
3556
|
+
{
|
|
3557
|
+
title: 'Student',
|
|
3558
|
+
param: 'isLearner',
|
|
3559
|
+
type: ParamType$1.Boolean,
|
|
3560
|
+
},
|
|
3561
|
+
{
|
|
3562
|
+
title: 'Teaching Staff',
|
|
3563
|
+
param: 'isTTM',
|
|
3564
|
+
type: ParamType$1.Boolean,
|
|
3565
|
+
startsHidden: true,
|
|
3566
|
+
},
|
|
3567
|
+
{
|
|
3568
|
+
title: 'Admin',
|
|
3569
|
+
param: 'isAdmin',
|
|
3570
|
+
type: ParamType$1.Boolean,
|
|
3571
|
+
startsHidden: true,
|
|
3572
|
+
},
|
|
3573
|
+
{
|
|
3574
|
+
title: 'Course Canvas Id',
|
|
3575
|
+
param: 'courseId',
|
|
3576
|
+
type: ParamType$1.Int,
|
|
3577
|
+
startsHidden: true,
|
|
3578
|
+
},
|
|
3579
|
+
{
|
|
3580
|
+
title: 'Course Name',
|
|
3581
|
+
param: 'courseName',
|
|
3582
|
+
type: ParamType$1.String,
|
|
3583
|
+
},
|
|
3584
|
+
{
|
|
3585
|
+
title: 'Browser Name',
|
|
3586
|
+
param: 'browser.name',
|
|
3587
|
+
type: ParamType$1.String,
|
|
3588
|
+
startsHidden: true,
|
|
3589
|
+
},
|
|
3590
|
+
{
|
|
3591
|
+
title: 'Browser Version',
|
|
3592
|
+
param: 'browser.version',
|
|
3593
|
+
type: ParamType$1.String,
|
|
3594
|
+
startsHidden: true,
|
|
3595
|
+
},
|
|
3596
|
+
{
|
|
3597
|
+
title: 'OS',
|
|
3598
|
+
param: 'device.os',
|
|
3599
|
+
type: ParamType$1.String,
|
|
3600
|
+
startsHidden: true,
|
|
3601
|
+
},
|
|
3602
|
+
{
|
|
3603
|
+
title: 'Mobile',
|
|
3604
|
+
param: 'device.isMobile',
|
|
3605
|
+
type: ParamType$1.Boolean,
|
|
3606
|
+
startsHidden: true,
|
|
3607
|
+
},
|
|
3608
|
+
{
|
|
3609
|
+
title: 'Year',
|
|
3610
|
+
param: 'year',
|
|
3611
|
+
type: ParamType$1.Int,
|
|
3612
|
+
},
|
|
3613
|
+
{
|
|
3614
|
+
title: 'Month',
|
|
3615
|
+
param: 'month',
|
|
3616
|
+
type: ParamType$1.Int,
|
|
3617
|
+
},
|
|
3618
|
+
{
|
|
3619
|
+
title: 'Day',
|
|
3620
|
+
param: 'day',
|
|
3621
|
+
type: ParamType$1.Int,
|
|
3622
|
+
},
|
|
3623
|
+
{
|
|
3624
|
+
title: 'Hour',
|
|
3625
|
+
param: 'hour',
|
|
3626
|
+
type: ParamType$1.Int,
|
|
3627
|
+
},
|
|
3628
|
+
{
|
|
3629
|
+
title: 'Minute',
|
|
3630
|
+
param: 'minute',
|
|
3631
|
+
type: ParamType$1.Int,
|
|
3632
|
+
startsHidden: true,
|
|
3633
|
+
},
|
|
3634
|
+
{
|
|
3635
|
+
title: 'Timestamp',
|
|
3636
|
+
param: 'timestamp',
|
|
3637
|
+
type: ParamType$1.Int,
|
|
3638
|
+
startsHidden: true,
|
|
3639
|
+
},
|
|
3640
|
+
{
|
|
3641
|
+
title: 'Context',
|
|
3642
|
+
param: 'context',
|
|
3643
|
+
type: ParamType$1.String,
|
|
3644
|
+
},
|
|
3645
|
+
{
|
|
3646
|
+
title: 'Subcontext',
|
|
3647
|
+
param: 'subcontext',
|
|
3648
|
+
type: ParamType$1.String,
|
|
3649
|
+
},
|
|
3650
|
+
{
|
|
3651
|
+
title: 'Tags',
|
|
3652
|
+
param: 'tags',
|
|
3653
|
+
type: ParamType$1.JSON,
|
|
3654
|
+
startsHidden: true,
|
|
3655
|
+
},
|
|
3656
|
+
{
|
|
3657
|
+
title: 'Log Level',
|
|
3658
|
+
param: 'level',
|
|
3659
|
+
type: ParamType$1.String,
|
|
3660
|
+
startsHidden: true,
|
|
3661
|
+
},
|
|
3662
|
+
{
|
|
3663
|
+
title: 'Metadata',
|
|
3664
|
+
param: 'metadata',
|
|
3665
|
+
type: ParamType$1.JSON,
|
|
3666
|
+
startsHidden: true,
|
|
3667
|
+
},
|
|
3668
|
+
{
|
|
3669
|
+
title: 'Source',
|
|
3670
|
+
param: 'source',
|
|
3671
|
+
type: ParamType$1.String,
|
|
3672
|
+
},
|
|
3673
|
+
{
|
|
3674
|
+
title: 'Server Route Path',
|
|
3675
|
+
param: 'routePath',
|
|
3676
|
+
type: ParamType$1.String,
|
|
3677
|
+
startsHidden: true,
|
|
3678
|
+
},
|
|
3679
|
+
{
|
|
3680
|
+
title: 'Server Route Template',
|
|
3681
|
+
param: 'routeTemplate',
|
|
3682
|
+
type: ParamType$1.String,
|
|
3683
|
+
startsHidden: true,
|
|
3684
|
+
},
|
|
3685
|
+
{
|
|
3686
|
+
title: 'Type',
|
|
3687
|
+
param: 'type',
|
|
3688
|
+
type: ParamType$1.String,
|
|
3689
|
+
},
|
|
3690
|
+
{
|
|
3691
|
+
title: 'Error Message',
|
|
3692
|
+
param: 'errorMessage',
|
|
3693
|
+
type: ParamType$1.String,
|
|
3694
|
+
startsHidden: true,
|
|
3695
|
+
},
|
|
3696
|
+
{
|
|
3697
|
+
title: 'Error Code',
|
|
3698
|
+
param: 'errorCode',
|
|
3699
|
+
type: ParamType$1.String,
|
|
3700
|
+
startsHidden: true,
|
|
3701
|
+
},
|
|
3702
|
+
{
|
|
3703
|
+
title: 'Error Stack',
|
|
3704
|
+
param: 'errorStack',
|
|
3705
|
+
type: ParamType$1.String,
|
|
3706
|
+
startsHidden: true,
|
|
3707
|
+
},
|
|
3708
|
+
{
|
|
3709
|
+
title: 'Action Target',
|
|
3710
|
+
param: 'target',
|
|
3711
|
+
type: ParamType$1.String,
|
|
3712
|
+
startsHidden: true,
|
|
3713
|
+
},
|
|
3714
|
+
{
|
|
3715
|
+
title: 'Action Type',
|
|
3716
|
+
param: 'action',
|
|
3717
|
+
type: ParamType$1.String,
|
|
3718
|
+
startsHidden: true,
|
|
3719
|
+
},
|
|
3720
|
+
];
|
|
3721
|
+
// Create intelliTable
|
|
3722
|
+
const dataTable = (React.createElement(IntelliTable, { title: "Matching Logs", id: "logs", data: logs, columns: columns }));
|
|
3723
|
+
// Main body
|
|
3724
|
+
body = (React.createElement(React.Fragment, null,
|
|
3725
|
+
filters,
|
|
3726
|
+
dataTable));
|
|
3727
|
+
}
|
|
3728
|
+
/* ---------- Wrap in Modal --------- */
|
|
3729
|
+
return (React.createElement("div", { className: "LogReviewer-outer-container" },
|
|
3730
|
+
React.createElement("style", null, style),
|
|
3731
|
+
React.createElement("div", { className: "LogReviewer-inner-container" },
|
|
3732
|
+
React.createElement("div", { className: "LogReviewer-header" },
|
|
3733
|
+
React.createElement("div", { className: "LogReviewer-header-title" },
|
|
3734
|
+
React.createElement("h1", { className: "m-0" }, "Log Review Dashboard")),
|
|
3735
|
+
React.createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-lg", "aria-label": "close log reviewer panel", onClick: onClose, style: {
|
|
3736
|
+
border: 0,
|
|
3737
|
+
backgroundColor: 'transparent',
|
|
3738
|
+
padding: 0,
|
|
3739
|
+
margin: 0,
|
|
3740
|
+
} },
|
|
3741
|
+
React.createElement(FontAwesomeIcon, { icon: faTimes }))),
|
|
3742
|
+
React.createElement("div", { className: "LogReviewer-contents" }, body))));
|
|
3743
|
+
};
|
|
3744
|
+
|
|
3745
|
+
/**
|
|
3746
|
+
* One minute in ms
|
|
3747
|
+
* @author Gabe Abrams
|
|
3748
|
+
*/
|
|
3749
|
+
const MINUTE_IN_MS = 60000;
|
|
3750
|
+
|
|
3751
|
+
/**
|
|
3752
|
+
* One hour in ms
|
|
3753
|
+
* @author Gabe Abrams
|
|
3754
|
+
*/
|
|
3755
|
+
const HOUR_IN_MS = 3600000;
|
|
3756
|
+
|
|
3757
|
+
/**
|
|
3758
|
+
* One day in ms
|
|
3759
|
+
* @author Gabe Abrams
|
|
3760
|
+
*/
|
|
3761
|
+
const DAY_IN_MS = 86400000;
|
|
3762
|
+
|
|
3763
|
+
/**
|
|
3764
|
+
* Shorten text so it fits into a certain number of chars
|
|
3765
|
+
* @author Gabe Abrams
|
|
3766
|
+
* @param text the text to abbreviate
|
|
3767
|
+
* @param maxChars the maximum number of chars to include
|
|
3768
|
+
* @returns abbreviated text with length no greater than maxChars
|
|
3769
|
+
* (including ellipses if applicable)
|
|
3770
|
+
*/
|
|
3771
|
+
const abbreviate = (text, maxChars) => {
|
|
3772
|
+
// Check if already short enough
|
|
3773
|
+
if (text.trim().length < maxChars) {
|
|
3774
|
+
return text.trim();
|
|
3775
|
+
}
|
|
3776
|
+
// Abbreviate
|
|
3777
|
+
const shortenedText = (text
|
|
3778
|
+
.trim()
|
|
3779
|
+
.substring(0, maxChars - 3)
|
|
3780
|
+
.trim());
|
|
3781
|
+
return `${shortenedText}...`;
|
|
3782
|
+
};
|
|
3783
|
+
|
|
3784
|
+
/**
|
|
3785
|
+
* Sum the numbers in an array
|
|
3786
|
+
* @author Gabe Abrams
|
|
3787
|
+
* @param nums the numbers to sum
|
|
3788
|
+
* @returns the sum of the numbers
|
|
3789
|
+
*/
|
|
3790
|
+
const sum = (nums) => {
|
|
3791
|
+
return nums.reduce((a, b) => {
|
|
3792
|
+
return (a + b);
|
|
3793
|
+
}, 0);
|
|
3794
|
+
};
|
|
3795
|
+
|
|
3796
|
+
/**
|
|
3797
|
+
* Get the average of a set of numbers
|
|
3798
|
+
* @author Gabe Abrams
|
|
3799
|
+
* @param nums the numbers to average
|
|
3800
|
+
* @returns average value or 0 if no numbers
|
|
3801
|
+
*/
|
|
3802
|
+
const avg = (nums) => {
|
|
3803
|
+
// Handle empty array case
|
|
3804
|
+
if (nums.length === 0) {
|
|
3805
|
+
return 0;
|
|
2112
3806
|
}
|
|
2113
|
-
//
|
|
2114
|
-
const
|
|
2115
|
-
//
|
|
2116
|
-
return
|
|
2117
|
-
}
|
|
3807
|
+
// Get the total value
|
|
3808
|
+
const total = sum(nums);
|
|
3809
|
+
// Get average
|
|
3810
|
+
return (total / nums.length);
|
|
3811
|
+
};
|
|
2118
3812
|
|
|
2119
3813
|
/**
|
|
2120
|
-
*
|
|
3814
|
+
* Round a number (ceiling) to a certain number of decimals
|
|
2121
3815
|
* @author Gabe Abrams
|
|
3816
|
+
* @param num the number to round
|
|
3817
|
+
* @param numDecimals the number of decimals to round to
|
|
3818
|
+
* @returns rounded number
|
|
2122
3819
|
*/
|
|
2123
|
-
const
|
|
3820
|
+
const ceilToNumDecimals = (num, numDecimals) => {
|
|
3821
|
+
const rounder = 10 ** numDecimals;
|
|
3822
|
+
return (Math.ceil(num * rounder) / rounder);
|
|
3823
|
+
};
|
|
2124
3824
|
|
|
2125
3825
|
/**
|
|
2126
|
-
*
|
|
3826
|
+
* Round a number (floor) to a certain number of decimals
|
|
2127
3827
|
* @author Gabe Abrams
|
|
3828
|
+
* @param num the number to round
|
|
3829
|
+
* @param numDecimals the number of decimals to round to
|
|
3830
|
+
* @returns rounded number
|
|
2128
3831
|
*/
|
|
2129
|
-
const
|
|
3832
|
+
const floorToNumDecimals = (num, numDecimals) => {
|
|
3833
|
+
const rounder = 10 ** numDecimals;
|
|
3834
|
+
return (Math.floor(num * rounder) / rounder);
|
|
3835
|
+
};
|
|
2130
3836
|
|
|
2131
3837
|
/**
|
|
2132
|
-
*
|
|
3838
|
+
* Pad a number's decimal with zeros on the right
|
|
3839
|
+
* (e.g. 5.2 becomes 5.20 with 2 digit padding)
|
|
2133
3840
|
* @author Gabe Abrams
|
|
3841
|
+
* @param num the number to pad
|
|
3842
|
+
* @param numDigits the minimum number of digits after the decimal
|
|
3843
|
+
* @returns padded number
|
|
2134
3844
|
*/
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
3845
|
+
const padDecimalZeros = (num, numDigits) => {
|
|
3846
|
+
// Skip if nothing to do
|
|
3847
|
+
if (numDigits < 1) {
|
|
3848
|
+
return String(num);
|
|
3849
|
+
}
|
|
3850
|
+
// Convert to string
|
|
3851
|
+
let out = String(num);
|
|
3852
|
+
// Add a decimal point if there isn't one
|
|
3853
|
+
if (!out.includes('.')) {
|
|
3854
|
+
out += '.';
|
|
3855
|
+
}
|
|
3856
|
+
// Add zeros
|
|
3857
|
+
while (out.split('.')[1].length < numDigits) {
|
|
3858
|
+
out = `${out}0`;
|
|
3859
|
+
}
|
|
3860
|
+
// Return
|
|
3861
|
+
return out;
|
|
3862
|
+
};
|
|
3863
|
+
|
|
3864
|
+
/**
|
|
3865
|
+
* Pad a number with zeros on the left (e.g. 5 becomes 05 with 2 digit padding)
|
|
3866
|
+
* @author Gabe Abrams
|
|
3867
|
+
* @param num the number to pad
|
|
3868
|
+
* @param numDigits the minimum number of digits before the decimal
|
|
3869
|
+
* @returns padded number
|
|
3870
|
+
*/
|
|
3871
|
+
const padZerosLeft = (num, numDigits) => {
|
|
3872
|
+
// Convert to string
|
|
3873
|
+
let out = String(num);
|
|
3874
|
+
// Add zeros
|
|
3875
|
+
while (out.split('.')[0].length < numDigits) {
|
|
3876
|
+
out = `0${out}`;
|
|
3877
|
+
}
|
|
3878
|
+
// Return
|
|
3879
|
+
return out;
|
|
3880
|
+
};
|
|
3881
|
+
|
|
3882
|
+
/**
|
|
3883
|
+
* Route for checking the status of the current user's
|
|
3884
|
+
* access to log review
|
|
3885
|
+
* @author Gabe Abrams
|
|
3886
|
+
*/
|
|
3887
|
+
const LOG_REVIEW_STATUS_ROUTE = `/admin${ROUTE_PATH_PREFIX}/logs/access`;
|
|
2149
3888
|
|
|
2150
|
-
// Import custom error
|
|
2151
3889
|
// Stored copy of caccl functions
|
|
2152
3890
|
let _cacclGetLaunchInfo;
|
|
2153
3891
|
// Stored copy of dce-mango log collection
|
|
@@ -2188,10 +3926,20 @@ const internalGetLogCollection = () => {
|
|
|
2188
3926
|
* @param opts.getLaunchInfo CACCL LTI's get launch info function
|
|
2189
3927
|
* @param [opts.logCollection] mongo collection from dce-mango to use for
|
|
2190
3928
|
* storing logs. If none is included, logs are written to the console
|
|
3929
|
+
* @param [opts.logReviewAdmins=all admins] info on which admins can review
|
|
3930
|
+
* logs from the client. If not included, all Canvas admins are allowed to
|
|
3931
|
+
* review logs. If null, no Canvas admins are allowed to review logs.
|
|
3932
|
+
* If an array of Canvas userIds (numbers), only Canvas admins with those
|
|
3933
|
+
* userIds are allowed to review logs. If a dce-mango collection, only
|
|
3934
|
+
* Canvas admins with entries in that collection ({ userId, ...}) are allowed
|
|
3935
|
+
* to review logs
|
|
2191
3936
|
*/
|
|
2192
3937
|
const initServer = (opts) => {
|
|
2193
3938
|
_cacclGetLaunchInfo = opts.getLaunchInfo;
|
|
2194
3939
|
_logCollection = opts.logCollection;
|
|
3940
|
+
/*----------------------------------------*/
|
|
3941
|
+
/* Logging */
|
|
3942
|
+
/*----------------------------------------*/
|
|
2195
3943
|
/**
|
|
2196
3944
|
* Log an event
|
|
2197
3945
|
* @author Gabe Abrams
|
|
@@ -2258,6 +4006,74 @@ const initServer = (opts) => {
|
|
|
2258
4006
|
return log;
|
|
2259
4007
|
},
|
|
2260
4008
|
}));
|
|
4009
|
+
/*----------------------------------------*/
|
|
4010
|
+
/* Log Reviewer */
|
|
4011
|
+
/*----------------------------------------*/
|
|
4012
|
+
if (opts.logReviewAdmins !== null) {
|
|
4013
|
+
/**
|
|
4014
|
+
* Check if a given user is allowed to review logs
|
|
4015
|
+
* @author Gabe Abrams
|
|
4016
|
+
* @param userId the id of the user
|
|
4017
|
+
* @returns true if the user can review logs
|
|
4018
|
+
*/
|
|
4019
|
+
const canReviewLogs = (userId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
4020
|
+
try {
|
|
4021
|
+
// Array of userIds
|
|
4022
|
+
if (Array.isArray(opts.logReviewAdmins)) {
|
|
4023
|
+
return opts.logReviewAdmins.some((allowedId) => {
|
|
4024
|
+
return (userId === allowedId);
|
|
4025
|
+
});
|
|
4026
|
+
}
|
|
4027
|
+
// Must be a collection
|
|
4028
|
+
const matches = yield opts.logReviewAdmins.find({ userId });
|
|
4029
|
+
// Make sure at least one entry matches
|
|
4030
|
+
return matches.length > 0;
|
|
4031
|
+
}
|
|
4032
|
+
catch (err) {
|
|
4033
|
+
// If an error occurred, simply return false
|
|
4034
|
+
return false;
|
|
4035
|
+
}
|
|
4036
|
+
});
|
|
4037
|
+
/**
|
|
4038
|
+
* Check if the current user has access to logs
|
|
4039
|
+
* @author Gabe Abrams
|
|
4040
|
+
* @returns {boolean} true if user has access
|
|
4041
|
+
*/
|
|
4042
|
+
opts.app.get(LOG_REVIEW_STATUS_ROUTE, genRouteHandler({
|
|
4043
|
+
handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
4044
|
+
const { userId } = params;
|
|
4045
|
+
const canReview = yield canReviewLogs(userId);
|
|
4046
|
+
return canReview;
|
|
4047
|
+
}),
|
|
4048
|
+
}));
|
|
4049
|
+
/**
|
|
4050
|
+
* Get all logs for a certain month
|
|
4051
|
+
* @author Gabe Abrams
|
|
4052
|
+
* @param {number} year the year to query (e.g. 2022)
|
|
4053
|
+
* @param {number} month the month to query (e.g. 1 = January)
|
|
4054
|
+
* @returns {Log[]} list of logs from the given month
|
|
4055
|
+
*/
|
|
4056
|
+
opts.app.post(`${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/:year/months/:month`, genRouteHandler({
|
|
4057
|
+
paramTypes: {
|
|
4058
|
+
year: ParamType$1.Int,
|
|
4059
|
+
month: ParamType$1.Int,
|
|
4060
|
+
},
|
|
4061
|
+
handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
4062
|
+
// Get user info
|
|
4063
|
+
const { userId } = params;
|
|
4064
|
+
// Validate user
|
|
4065
|
+
// isAdmin is already checked because path starts with '/admin'
|
|
4066
|
+
const canReview = yield canReviewLogs(userId);
|
|
4067
|
+
if (!canReview) {
|
|
4068
|
+
throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
|
|
4069
|
+
}
|
|
4070
|
+
// Query for logs
|
|
4071
|
+
const logs = yield _logCollection.find({ userId });
|
|
4072
|
+
// Return logs
|
|
4073
|
+
return logs;
|
|
4074
|
+
}),
|
|
4075
|
+
}));
|
|
4076
|
+
}
|
|
2261
4077
|
};
|
|
2262
4078
|
|
|
2263
4079
|
// Import shared types
|
|
@@ -2557,90 +4373,6 @@ const parseUserAgent = (userAgent) => {
|
|
|
2557
4373
|
};
|
|
2558
4374
|
};
|
|
2559
4375
|
|
|
2560
|
-
/**
|
|
2561
|
-
* Type of a log event
|
|
2562
|
-
* @author Gabe Abrams
|
|
2563
|
-
*/
|
|
2564
|
-
var LogType;
|
|
2565
|
-
(function (LogType) {
|
|
2566
|
-
// User action
|
|
2567
|
-
LogType["Action"] = "action";
|
|
2568
|
-
// Error
|
|
2569
|
-
LogType["Error"] = "error";
|
|
2570
|
-
})(LogType || (LogType = {}));
|
|
2571
|
-
var LogType$1 = LogType;
|
|
2572
|
-
|
|
2573
|
-
/**
|
|
2574
|
-
* Source of a log event
|
|
2575
|
-
* @author Gabe Abrams
|
|
2576
|
-
*/
|
|
2577
|
-
var LogSource;
|
|
2578
|
-
(function (LogSource) {
|
|
2579
|
-
// Client
|
|
2580
|
-
LogSource["Client"] = "client";
|
|
2581
|
-
// Server
|
|
2582
|
-
LogSource["Server"] = "server";
|
|
2583
|
-
})(LogSource || (LogSource = {}));
|
|
2584
|
-
var LogSource$1 = LogSource;
|
|
2585
|
-
|
|
2586
|
-
/**
|
|
2587
|
-
* Built-in metadata for logs
|
|
2588
|
-
* @author Gabe Abrams
|
|
2589
|
-
*/
|
|
2590
|
-
const LogBuiltInMetadata = {
|
|
2591
|
-
// Contexts
|
|
2592
|
-
Context: {
|
|
2593
|
-
Uncategorized: 'n/a',
|
|
2594
|
-
ServerRenderedErrorPage: '_server-rendered-error-page',
|
|
2595
|
-
ServerEndpointError: '_server-endpoint-error',
|
|
2596
|
-
},
|
|
2597
|
-
// Targets
|
|
2598
|
-
Target: {
|
|
2599
|
-
NoSpecificTarget: 'n/a',
|
|
2600
|
-
},
|
|
2601
|
-
};
|
|
2602
|
-
|
|
2603
|
-
/**
|
|
2604
|
-
* Types of actions
|
|
2605
|
-
* @author Gabe Abrams
|
|
2606
|
-
*/
|
|
2607
|
-
var LogAction;
|
|
2608
|
-
(function (LogAction) {
|
|
2609
|
-
// Target was opened by the user (it was not on screen, but now it is)
|
|
2610
|
-
LogAction["Open"] = "open";
|
|
2611
|
-
// Target was closed by the user (it was on screen, but now it is not)
|
|
2612
|
-
LogAction["Close"] = "close";
|
|
2613
|
-
// Target was cancelled by the user (it was on closed without saving)
|
|
2614
|
-
LogAction["Cancel"] = "cancel";
|
|
2615
|
-
// Target was expanded by the user (it always remains on screen, but size was changed)
|
|
2616
|
-
LogAction["Expand"] = "expand";
|
|
2617
|
-
// Target was collapsed by the user (it always remains on screen, but size was changed)
|
|
2618
|
-
LogAction["Collapse"] = "collapse";
|
|
2619
|
-
// Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
|
|
2620
|
-
LogAction["View"] = "view";
|
|
2621
|
-
// Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
|
|
2622
|
-
LogAction["Interrupt"] = "interrupt";
|
|
2623
|
-
// Target was created by the user (it did not exist before)
|
|
2624
|
-
LogAction["Create"] = "create";
|
|
2625
|
-
// Target was edited by the user (it existed and was changed)
|
|
2626
|
-
LogAction["Edit"] = "edit";
|
|
2627
|
-
// Target was deleted by the user (it existed and now it doesn't)
|
|
2628
|
-
LogAction["Delete"] = "delete";
|
|
2629
|
-
// Target was added by the user (it already existed and was added to another place)
|
|
2630
|
-
LogAction["Add"] = "add";
|
|
2631
|
-
// Target was removed by the user (it was removed from something but still exists)
|
|
2632
|
-
LogAction["Remove"] = "remove";
|
|
2633
|
-
// Target was activated by the user (click, check, tap, keypress, etc.)
|
|
2634
|
-
LogAction["Activate"] = "activate";
|
|
2635
|
-
// Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
|
|
2636
|
-
LogAction["Deactivate"] = "deactivate";
|
|
2637
|
-
// User showed interest in a target (hover, peek, etc.)
|
|
2638
|
-
LogAction["Peek"] = "peek";
|
|
2639
|
-
// Unknown action
|
|
2640
|
-
LogAction["Unknown"] = "unknown";
|
|
2641
|
-
})(LogAction || (LogAction = {}));
|
|
2642
|
-
var LogAction$1 = LogAction;
|
|
2643
|
-
|
|
2644
4376
|
/**
|
|
2645
4377
|
* Allowed log levels
|
|
2646
4378
|
* @author Gabe Abrams
|
|
@@ -3238,21 +4970,7 @@ const startMinWait = (minWaitMs) => {
|
|
|
3238
4970
|
});
|
|
3239
4971
|
};
|
|
3240
4972
|
|
|
3241
|
-
//
|
|
3242
|
-
const monthMap = {
|
|
3243
|
-
1: 'Jan',
|
|
3244
|
-
2: 'Feb',
|
|
3245
|
-
3: 'Mar',
|
|
3246
|
-
4: 'Apr',
|
|
3247
|
-
5: 'May',
|
|
3248
|
-
6: 'Jun',
|
|
3249
|
-
7: 'Jul',
|
|
3250
|
-
8: 'Aug',
|
|
3251
|
-
9: 'Sep',
|
|
3252
|
-
10: 'Oct',
|
|
3253
|
-
11: 'Nov',
|
|
3254
|
-
12: 'Dec',
|
|
3255
|
-
};
|
|
4973
|
+
// Import shared helpers
|
|
3256
4974
|
/**
|
|
3257
4975
|
* Get a human-readable description of a date (all in ET)
|
|
3258
4976
|
* @author Gabe Abrams
|
|
@@ -3263,8 +4981,10 @@ const getHumanReadableDate = (dateOrTimestamp) => {
|
|
|
3263
4981
|
// Get the date info
|
|
3264
4982
|
const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
|
|
3265
4983
|
const currYear = getTimeInfoInET().year;
|
|
4984
|
+
// Get the short month description
|
|
4985
|
+
const monthName = getMonthName(month).short;
|
|
3266
4986
|
// Create start of description
|
|
3267
|
-
let description = `${
|
|
4987
|
+
let description = `${monthName} ${day}${getOrdinal(day)}`;
|
|
3268
4988
|
// Add on year if it's different
|
|
3269
4989
|
if (year !== currYear) {
|
|
3270
4990
|
description += ` ${year}`;
|
|
@@ -3386,41 +5106,6 @@ const parallelLimit = (taskFunctions, limit) => __awaiter(void 0, void 0, void 0
|
|
|
3386
5106
|
return results;
|
|
3387
5107
|
});
|
|
3388
5108
|
|
|
3389
|
-
/**
|
|
3390
|
-
* Log a user action on the client (cannot be used on the server)
|
|
3391
|
-
* @author Gabe Abrams
|
|
3392
|
-
*/
|
|
3393
|
-
const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3394
|
-
var _a, _b, _c, _d, _e, _f;
|
|
3395
|
-
return visitServerEndpoint({
|
|
3396
|
-
path: LOG_ROUTE_PATH,
|
|
3397
|
-
method: 'POST',
|
|
3398
|
-
params: {
|
|
3399
|
-
context: (typeof opts.context === 'string'
|
|
3400
|
-
? opts.context
|
|
3401
|
-
: ((_b = ((_a = opts.context) !== null && _a !== void 0 ? _a : {})._) !== null && _b !== void 0 ? _b : LogBuiltInMetadata.Context.Uncategorized)),
|
|
3402
|
-
subcontext: ((_c = opts.subcontext) !== null && _c !== void 0 ? _c : LogBuiltInMetadata.Context.Uncategorized),
|
|
3403
|
-
tags: JSON.stringify((_d = opts.tags) !== null && _d !== void 0 ? _d : []),
|
|
3404
|
-
metadata: JSON.stringify((_e = opts.metadata) !== null && _e !== void 0 ? _e : {}),
|
|
3405
|
-
errorMessage: (opts.error
|
|
3406
|
-
? opts.error.message
|
|
3407
|
-
: undefined),
|
|
3408
|
-
errorCode: (opts.error
|
|
3409
|
-
? opts.error.code
|
|
3410
|
-
: undefined),
|
|
3411
|
-
errorStack: (opts.error
|
|
3412
|
-
? opts.error.stack
|
|
3413
|
-
: undefined),
|
|
3414
|
-
target: (opts.action
|
|
3415
|
-
? ((_f = opts.target) !== null && _f !== void 0 ? _f : LogBuiltInMetadata.Target.NoSpecificTarget)
|
|
3416
|
-
: undefined),
|
|
3417
|
-
action: (opts.action
|
|
3418
|
-
? opts.action
|
|
3419
|
-
: undefined),
|
|
3420
|
-
},
|
|
3421
|
-
});
|
|
3422
|
-
});
|
|
3423
|
-
|
|
3424
5109
|
/**
|
|
3425
5110
|
* Initialize a log collection given the dce-mango Collection class
|
|
3426
5111
|
* @author Gabe Abrams
|
|
@@ -3455,5 +5140,5 @@ var DayOfWeek;
|
|
|
3455
5140
|
})(DayOfWeek || (DayOfWeek = {}));
|
|
3456
5141
|
var DayOfWeek$1 = DayOfWeek;
|
|
3457
5142
|
|
|
3458
|
-
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 };
|
|
5143
|
+
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, 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 };
|
|
3459
5144
|
//# sourceMappingURL=index.js.map
|