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