dce-reactkit 3.2.2-beta.1 → 3.2.2-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +3 -0
- package/dist/cjs/index.js +2018 -274
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/components/ButtonInputGroup.d.ts +1 -0
- package/dist/cjs/types/components/CSVDownloadButton.d.ts +19 -0
- package/dist/cjs/types/components/IntelliTable.d.ts +17 -0
- package/dist/cjs/types/components/ItemPicker/index.d.ts +1 -0
- package/dist/cjs/types/components/LogReviewer.d.ts +13 -0
- package/dist/cjs/types/components/SimpleDateChooser.d.ts +1 -0
- package/dist/cjs/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
- package/dist/cjs/types/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
- package/dist/cjs/types/helpers/canReviewLogs.d.ts +7 -0
- package/dist/cjs/types/helpers/genCSV.d.ts +14 -0
- package/dist/cjs/types/helpers/getMonthName.d.ts +14 -0
- package/dist/cjs/types/index.d.ts +9 -1
- package/dist/cjs/types/server/initServer.d.ts +8 -0
- package/dist/cjs/types/types/IntelliTableColumn.d.ts +12 -0
- package/dist/cjs/types/types/LogMetadataType.d.ts +19 -0
- package/dist/cjs/types/types/ReactKitErrorCode.d.ts +2 -1
- package/dist/esm/index.js +2014 -276
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/components/ButtonInputGroup.d.ts +1 -0
- package/dist/esm/types/components/CSVDownloadButton.d.ts +19 -0
- package/dist/esm/types/components/IntelliTable.d.ts +17 -0
- package/dist/esm/types/components/ItemPicker/index.d.ts +1 -0
- package/dist/esm/types/components/LogReviewer.d.ts +13 -0
- package/dist/esm/types/components/SimpleDateChooser.d.ts +1 -0
- package/dist/esm/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
- package/dist/esm/types/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
- package/dist/esm/types/helpers/canReviewLogs.d.ts +7 -0
- package/dist/esm/types/helpers/genCSV.d.ts +14 -0
- package/dist/esm/types/helpers/getMonthName.d.ts +14 -0
- package/dist/esm/types/index.d.ts +9 -1
- package/dist/esm/types/server/initServer.d.ts +8 -0
- package/dist/esm/types/types/IntelliTableColumn.d.ts +12 -0
- package/dist/esm/types/types/LogMetadataType.d.ts +19 -0
- package/dist/esm/types/types/ReactKitErrorCode.d.ts +2 -1
- package/dist/index.d.ts +171 -47
- package/package.json +1 -1
- package/sandbox.js +78 -17
- package/src/components/AppWrapper.tsx +5 -1
- package/src/components/ButtonInputGroup.tsx +4 -1
- package/src/components/CSVDownloadButton.tsx +102 -0
- package/src/components/IntelliTable.tsx +610 -0
- package/src/components/ItemPicker/index.tsx +4 -0
- package/src/components/LogReviewer.tsx +2091 -0
- package/src/components/SimpleDateChooser.tsx +26 -27
- package/src/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.ts +9 -0
- package/src/constants/LOG_REVIEW_STATUS_ROUTE.ts +10 -0
- package/src/helpers/canReviewLogs.ts +33 -0
- package/src/helpers/genCSV.ts +59 -0
- package/src/helpers/getHumanReadableDate.ts +6 -17
- package/src/helpers/getMonthName.ts +29 -0
- package/src/helpers/logClientEvent.tsx +5 -0
- package/src/index.ts +16 -0
- package/src/server/initServer.ts +125 -3
- package/src/types/IntelliTableColumn.ts +24 -0
- package/src/types/LogMetadataType.ts +23 -0
- package/src/types/ReactKitErrorCode.tsx +2 -1
|
@@ -0,0 +1,2091 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log reviewer panel that allows users (must be approved admins) to
|
|
3
|
+
* review logs written by dce-reactkit
|
|
4
|
+
* @author Gabe Abrams
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Import React
|
|
8
|
+
import React, { useReducer, useEffect } from 'react';
|
|
9
|
+
|
|
10
|
+
// Import FontAwesome
|
|
11
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
12
|
+
import {
|
|
13
|
+
faCalendar,
|
|
14
|
+
faCircle,
|
|
15
|
+
faHammer,
|
|
16
|
+
faList,
|
|
17
|
+
faTag,
|
|
18
|
+
faTimes,
|
|
19
|
+
} from '@fortawesome/free-solid-svg-icons';
|
|
20
|
+
|
|
21
|
+
// Import shared helpers
|
|
22
|
+
import visitServerEndpoint from '../helpers/visitServerEndpoint';
|
|
23
|
+
import getTimeInfoInET from '../helpers/getTimeInfoInET';
|
|
24
|
+
import { showFatalError } from './AppWrapper';
|
|
25
|
+
|
|
26
|
+
// Import shared constants
|
|
27
|
+
import LOG_REVIEW_ROUTE_PATH_PREFIX from '../constants/LOG_REVIEW_ROUTE_PATH_PREFIX';
|
|
28
|
+
|
|
29
|
+
// Import shared types
|
|
30
|
+
import Log from '../types/Log';
|
|
31
|
+
import LogMetadataType from '../types/LogMetadataType';
|
|
32
|
+
import LogSource from '../types/LogSource';
|
|
33
|
+
import LogType from '../types/LogType';
|
|
34
|
+
import LogAction from '../types/LogAction';
|
|
35
|
+
import ParamType from '../types/ParamType';
|
|
36
|
+
import IntelliTableColumn from '../types/IntelliTableColumn';
|
|
37
|
+
|
|
38
|
+
// Import shared components
|
|
39
|
+
import SimpleDateChooser from './SimpleDateChooser';
|
|
40
|
+
import LoadingSpinner from './LoadingSpinner';
|
|
41
|
+
import Drawer from './Drawer';
|
|
42
|
+
import ItemPicker from './ItemPicker';
|
|
43
|
+
import PickableItem from './ItemPicker/types/PickableItem';
|
|
44
|
+
import CheckboxButton from './CheckboxButton';
|
|
45
|
+
import TabBox from './TabBox';
|
|
46
|
+
import RadioButton from './RadioButton';
|
|
47
|
+
import ButtonInputGroup from './ButtonInputGroup';
|
|
48
|
+
import IntelliTable from './IntelliTable';
|
|
49
|
+
|
|
50
|
+
/*------------------------------------------------------------------------*/
|
|
51
|
+
/* Types */
|
|
52
|
+
/*------------------------------------------------------------------------*/
|
|
53
|
+
|
|
54
|
+
// Props
|
|
55
|
+
type Props = {
|
|
56
|
+
// LogMetadata file for the app
|
|
57
|
+
LogMetadata: LogMetadataType,
|
|
58
|
+
// Function to call when the user wants to close the log reviewer
|
|
59
|
+
onClose: () => void,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Map of loaded logs (year => month => Log[])
|
|
63
|
+
type LogMap = {
|
|
64
|
+
[k: string]: {
|
|
65
|
+
[k: string]: Log[]
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Triple of year/month/day
|
|
70
|
+
type DateTriple = {
|
|
71
|
+
// Full year
|
|
72
|
+
year: number,
|
|
73
|
+
// 1-indexed month
|
|
74
|
+
month: number,
|
|
75
|
+
// 1-indexed day
|
|
76
|
+
day: number,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Types of filter drawers
|
|
80
|
+
enum FilterDrawer {
|
|
81
|
+
Date = 'date',
|
|
82
|
+
Context = 'context',
|
|
83
|
+
Tag = 'tag',
|
|
84
|
+
Action = 'action',
|
|
85
|
+
Advanced = 'advanced',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Date filter state
|
|
89
|
+
type DateFilterState = {
|
|
90
|
+
// Current start date
|
|
91
|
+
startDate: {
|
|
92
|
+
// Full year
|
|
93
|
+
year: number,
|
|
94
|
+
// 1-indexed month
|
|
95
|
+
month: number,
|
|
96
|
+
// 1-indexed day
|
|
97
|
+
day: number,
|
|
98
|
+
},
|
|
99
|
+
// Current end date
|
|
100
|
+
endDate: {
|
|
101
|
+
// Full year
|
|
102
|
+
year: number,
|
|
103
|
+
// 1-indexed month
|
|
104
|
+
month: number,
|
|
105
|
+
// 1-indexed day
|
|
106
|
+
day: number,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Context filter state
|
|
111
|
+
type ContextFilterState = {
|
|
112
|
+
[k: string]: (
|
|
113
|
+
// No subcontexts
|
|
114
|
+
| boolean // True if selected
|
|
115
|
+
// Includes subcontexts
|
|
116
|
+
| {
|
|
117
|
+
[k: string]: boolean // True if selected
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Tag filter state
|
|
123
|
+
type TagFilterState = {
|
|
124
|
+
[k: string]: boolean
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Action filter state (only relevant for action logs)
|
|
128
|
+
type ActionErrorFilterState = {
|
|
129
|
+
// Required type of log
|
|
130
|
+
type: LogType | undefined, // If undefined, no filter applied
|
|
131
|
+
// Query for error message (only relevant if type is error)
|
|
132
|
+
errorMessage: string, // If empty, no filter applied
|
|
133
|
+
// Query for error code (only relevant if type is error)
|
|
134
|
+
errorCode: string, // If empty, no filter applied
|
|
135
|
+
// Action targets to include (only relevant if type is action)
|
|
136
|
+
target: {
|
|
137
|
+
[k: string]: boolean
|
|
138
|
+
},
|
|
139
|
+
// Action types to include (only relevant if type is action)
|
|
140
|
+
action: {
|
|
141
|
+
[k: string]: boolean
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Advanced filter state
|
|
146
|
+
type AdvancedFilterState = {
|
|
147
|
+
// Query for user first name (case insensitive)
|
|
148
|
+
userFirstName: string, // If empty, no filter applied
|
|
149
|
+
// Query for user last name (case insensitive)
|
|
150
|
+
userLastName: string, // If empty, no filter applied
|
|
151
|
+
// Query for user email (case insensitive)
|
|
152
|
+
userEmail: string, // If empty, no filter applied
|
|
153
|
+
// Match for userId (numerical)
|
|
154
|
+
userId: string, // If empty, no filter applied
|
|
155
|
+
// If true, include students
|
|
156
|
+
includeLearners: boolean,
|
|
157
|
+
// If true, include ttms
|
|
158
|
+
includeTTMs: boolean,
|
|
159
|
+
// If true, include admins
|
|
160
|
+
includeAdmins: boolean,
|
|
161
|
+
// Match for courseId (numerical)
|
|
162
|
+
courseId: string, // If empty, no filter applied
|
|
163
|
+
// Query for course name (case insensitive)
|
|
164
|
+
courseName: string, // If empty, no filter applied
|
|
165
|
+
// Required isMobile value
|
|
166
|
+
isMobile: (true | false | undefined), // If undefined, no filter applied
|
|
167
|
+
// Required log source value
|
|
168
|
+
source: LogSource | undefined, // If undefined, no filter applied
|
|
169
|
+
// Query for route path (only relevant if source is server)
|
|
170
|
+
routePath: string, // If empty, no filter applied
|
|
171
|
+
// Query for route template (only relevant if source is server)
|
|
172
|
+
routeTemplate: string, // If empty, no filter applied
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/*------------------------------------------------------------------------*/
|
|
176
|
+
/* Style */
|
|
177
|
+
/*------------------------------------------------------------------------*/
|
|
178
|
+
|
|
179
|
+
const style = `
|
|
180
|
+
.LogReviewer-outer-container {
|
|
181
|
+
/* Full Screen */
|
|
182
|
+
display: inline-block;
|
|
183
|
+
left: 0;
|
|
184
|
+
top: 0;
|
|
185
|
+
width: 100vw;
|
|
186
|
+
height: 100vh;
|
|
187
|
+
|
|
188
|
+
/* On Top and Fixed */
|
|
189
|
+
position: fixed;
|
|
190
|
+
z-index: 90000;
|
|
191
|
+
|
|
192
|
+
/* Space around contents */
|
|
193
|
+
padding: 0.5rem;
|
|
194
|
+
|
|
195
|
+
/* Dark Background */
|
|
196
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
197
|
+
|
|
198
|
+
/* No Clickthrough */
|
|
199
|
+
pointer-events: none;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.LogReviewer-inner-container {
|
|
203
|
+
/* Full screen, rounded modal-like look */
|
|
204
|
+
display: flex;
|
|
205
|
+
height: 100%;
|
|
206
|
+
border: 0.05rem solid black;
|
|
207
|
+
border-radius: 0.5rem;
|
|
208
|
+
overflow: hidden;
|
|
209
|
+
padding: 0.7rem;
|
|
210
|
+
|
|
211
|
+
/* Solid background */
|
|
212
|
+
background-color: white;
|
|
213
|
+
color: black;
|
|
214
|
+
|
|
215
|
+
/* Place contents in flex column */
|
|
216
|
+
flex-direction: column;
|
|
217
|
+
|
|
218
|
+
/* Re-allow interaction */
|
|
219
|
+
pointer-events: all;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.LogReviewer-header {
|
|
223
|
+
/* Elements in flex row */
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-direction: row;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.LogReviewer-header-title {
|
|
229
|
+
/* Take up remaining width */
|
|
230
|
+
flex-grow: 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.LogReviewer-contents {
|
|
234
|
+
/* Take up remaining height */
|
|
235
|
+
flex-grow: 1;
|
|
236
|
+
|
|
237
|
+
/* Vertical scroll */
|
|
238
|
+
overflow-y: auto;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.LogReviewer-header-close-button {
|
|
242
|
+
border: 0 !important;
|
|
243
|
+
background-color: transparent !important;
|
|
244
|
+
padding-top: 0 !important;
|
|
245
|
+
padding-bottom: 0 !important;
|
|
246
|
+
margin: 0 !important;
|
|
247
|
+
color: #333 !important;
|
|
248
|
+
}
|
|
249
|
+
.LogReviewer-header-close-button:hover {
|
|
250
|
+
border: 0 !important;
|
|
251
|
+
background-color: transparent !important;
|
|
252
|
+
padding-top: 0 !important;
|
|
253
|
+
padding-bottom: 0 !important;
|
|
254
|
+
margin: 0 !important;
|
|
255
|
+
color: #000 !important;
|
|
256
|
+
}
|
|
257
|
+
`;
|
|
258
|
+
|
|
259
|
+
/*------------------------------------------------------------------------*/
|
|
260
|
+
/* Static Functions */
|
|
261
|
+
/*------------------------------------------------------------------------*/
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Turn a machine-readable name into a human-readable name
|
|
265
|
+
* @author Gabe Abrams
|
|
266
|
+
* @param name machine-readable name
|
|
267
|
+
* @returns human-readable name
|
|
268
|
+
*/
|
|
269
|
+
const genHumanReadableName = (machineReadableName: string) => {
|
|
270
|
+
let humanReadableName = '';
|
|
271
|
+
|
|
272
|
+
// Add chars and spaces
|
|
273
|
+
const chars = machineReadableName.split('');
|
|
274
|
+
chars.forEach((char) => {
|
|
275
|
+
if (/[A-Z]/.test(char)) {
|
|
276
|
+
// Uppercase! Add a space before
|
|
277
|
+
humanReadableName += ' ';
|
|
278
|
+
}
|
|
279
|
+
humanReadableName += char;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Trim and return
|
|
283
|
+
return humanReadableName.trim();
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/*------------------------------------------------------------------------*/
|
|
287
|
+
/* State */
|
|
288
|
+
/*------------------------------------------------------------------------*/
|
|
289
|
+
|
|
290
|
+
/* -------- State Definition -------- */
|
|
291
|
+
|
|
292
|
+
type State = {
|
|
293
|
+
/* -------------- Logs -------------- */
|
|
294
|
+
// True if currently loading
|
|
295
|
+
loading: boolean,
|
|
296
|
+
// Loaded logs (year => month => Log[])
|
|
297
|
+
logMap: LogMap,
|
|
298
|
+
/* ------------- Filters ------------ */
|
|
299
|
+
// Current expanded filter drawer
|
|
300
|
+
expandedFilterDrawer: FilterDrawer | undefined,
|
|
301
|
+
// State of date filters
|
|
302
|
+
dateFilterState: DateFilterState,
|
|
303
|
+
// State of context filters
|
|
304
|
+
contextFilterState: ContextFilterState,
|
|
305
|
+
// State of tag filters
|
|
306
|
+
tagFilterState: TagFilterState,
|
|
307
|
+
// State of the action and error filter
|
|
308
|
+
actionErrorFilterState: ActionErrorFilterState,
|
|
309
|
+
// State of the advanced filter
|
|
310
|
+
advancedFilterState: AdvancedFilterState,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/* ------------- Actions ------------ */
|
|
314
|
+
|
|
315
|
+
// Types of actions
|
|
316
|
+
enum ActionType {
|
|
317
|
+
// Show the loading bar
|
|
318
|
+
StartLoading = 'start-loading',
|
|
319
|
+
// Finish loading one or more months of logs
|
|
320
|
+
FinishLoading = 'finish-loading',
|
|
321
|
+
// Reset filters to initial values
|
|
322
|
+
ResetFilters = 'reset-filters',
|
|
323
|
+
// Choose a filter drawer to toggle
|
|
324
|
+
ToggleFilterDrawer = 'toggle-filter-drawer',
|
|
325
|
+
// Hide filter drawer
|
|
326
|
+
HideFilterDrawer = 'hide-filter-drawer',
|
|
327
|
+
// Handle the date filter state
|
|
328
|
+
UpdateDateFilterState = 'update-date-filter-state',
|
|
329
|
+
// Update the context filter state
|
|
330
|
+
UpdateContextFilterState = 'update-context-filter-state',
|
|
331
|
+
// Update the tag filter state
|
|
332
|
+
UpdateTagFilterState = 'update-tag-filter-state',
|
|
333
|
+
// Update the action and error filter state
|
|
334
|
+
UpdateActionErrorFilterState = 'update-action-error-filter-state',
|
|
335
|
+
// Update the advanced filter state
|
|
336
|
+
UpdateAdvancedFilterState = 'update-advanced-filter-state',
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Action definitions
|
|
340
|
+
type Action = (
|
|
341
|
+
| {
|
|
342
|
+
// Action type
|
|
343
|
+
type: ActionType.FinishLoading,
|
|
344
|
+
// Updated logMap
|
|
345
|
+
logMap: LogMap,
|
|
346
|
+
}
|
|
347
|
+
| {
|
|
348
|
+
// Action type
|
|
349
|
+
type: ActionType.ToggleFilterDrawer,
|
|
350
|
+
// The drawer to show
|
|
351
|
+
filterDrawer: FilterDrawer,
|
|
352
|
+
}
|
|
353
|
+
| {
|
|
354
|
+
// Action type
|
|
355
|
+
type: ActionType.ResetFilters,
|
|
356
|
+
// Initial filter states
|
|
357
|
+
initDateFilterState: DateFilterState,
|
|
358
|
+
initContextFilterState: ContextFilterState,
|
|
359
|
+
initTagFilterState: TagFilterState,
|
|
360
|
+
initActionErrorFilterState: ActionErrorFilterState,
|
|
361
|
+
initAdvancedFilterState: AdvancedFilterState,
|
|
362
|
+
}
|
|
363
|
+
| {
|
|
364
|
+
// Action type
|
|
365
|
+
type: ActionType.UpdateDateFilterState,
|
|
366
|
+
// New date filter state
|
|
367
|
+
dateFilterState: DateFilterState,
|
|
368
|
+
}
|
|
369
|
+
| {
|
|
370
|
+
// Action type
|
|
371
|
+
type: ActionType.UpdateContextFilterState,
|
|
372
|
+
// New context filter state
|
|
373
|
+
contextFilterState: ContextFilterState,
|
|
374
|
+
}
|
|
375
|
+
| {
|
|
376
|
+
// Action type
|
|
377
|
+
type: ActionType.UpdateTagFilterState,
|
|
378
|
+
// New tag filter state
|
|
379
|
+
tagFilterState: TagFilterState,
|
|
380
|
+
}
|
|
381
|
+
| {
|
|
382
|
+
// Action type
|
|
383
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
384
|
+
// New action and error filter state
|
|
385
|
+
actionErrorFilterState: ActionErrorFilterState,
|
|
386
|
+
}
|
|
387
|
+
| {
|
|
388
|
+
// Action type
|
|
389
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
390
|
+
// New advanced filter state
|
|
391
|
+
advancedFilterState: AdvancedFilterState,
|
|
392
|
+
}
|
|
393
|
+
| {
|
|
394
|
+
// Action type
|
|
395
|
+
type: (
|
|
396
|
+
| ActionType.StartLoading
|
|
397
|
+
| ActionType.HideFilterDrawer
|
|
398
|
+
),
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Reducer that executes actions
|
|
404
|
+
* @author Gabe Abrams
|
|
405
|
+
* @param state current state
|
|
406
|
+
* @param action action to execute
|
|
407
|
+
*/
|
|
408
|
+
const reducer = (state: State, action: Action): State => {
|
|
409
|
+
switch (action.type) {
|
|
410
|
+
case ActionType.StartLoading: {
|
|
411
|
+
return {
|
|
412
|
+
...state,
|
|
413
|
+
loading: true,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
case ActionType.FinishLoading: {
|
|
417
|
+
return {
|
|
418
|
+
...state,
|
|
419
|
+
loading: false,
|
|
420
|
+
logMap: action.logMap,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
case ActionType.ToggleFilterDrawer: {
|
|
424
|
+
return {
|
|
425
|
+
...state,
|
|
426
|
+
expandedFilterDrawer: (
|
|
427
|
+
state.expandedFilterDrawer === action.filterDrawer
|
|
428
|
+
? undefined // hide
|
|
429
|
+
: action.filterDrawer
|
|
430
|
+
),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
case ActionType.HideFilterDrawer: {
|
|
434
|
+
return {
|
|
435
|
+
...state,
|
|
436
|
+
expandedFilterDrawer: undefined,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
case ActionType.ResetFilters: {
|
|
440
|
+
return {
|
|
441
|
+
...state,
|
|
442
|
+
dateFilterState: action.initDateFilterState,
|
|
443
|
+
contextFilterState: action.initContextFilterState,
|
|
444
|
+
tagFilterState: action.initTagFilterState,
|
|
445
|
+
actionErrorFilterState: action.initActionErrorFilterState,
|
|
446
|
+
advancedFilterState: action.initAdvancedFilterState,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
case ActionType.UpdateDateFilterState: {
|
|
450
|
+
return {
|
|
451
|
+
...state,
|
|
452
|
+
dateFilterState: action.dateFilterState,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
case ActionType.UpdateContextFilterState: {
|
|
456
|
+
return {
|
|
457
|
+
...state,
|
|
458
|
+
contextFilterState: action.contextFilterState,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
case ActionType.UpdateTagFilterState: {
|
|
462
|
+
return {
|
|
463
|
+
...state,
|
|
464
|
+
tagFilterState: action.tagFilterState,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
case ActionType.UpdateActionErrorFilterState: {
|
|
468
|
+
return {
|
|
469
|
+
...state,
|
|
470
|
+
actionErrorFilterState: action.actionErrorFilterState,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
case ActionType.UpdateAdvancedFilterState: {
|
|
474
|
+
return {
|
|
475
|
+
...state,
|
|
476
|
+
advancedFilterState: action.advancedFilterState,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
default: {
|
|
480
|
+
return state;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
/*------------------------------------------------------------------------*/
|
|
486
|
+
/* Component */
|
|
487
|
+
/*------------------------------------------------------------------------*/
|
|
488
|
+
|
|
489
|
+
const LogReviewer: React.FC<Props> = (props) => {
|
|
490
|
+
/*------------------------------------------------------------------------*/
|
|
491
|
+
/* Setup */
|
|
492
|
+
/*------------------------------------------------------------------------*/
|
|
493
|
+
|
|
494
|
+
/* -------------- Props ------------- */
|
|
495
|
+
|
|
496
|
+
// Destructure props
|
|
497
|
+
const {
|
|
498
|
+
LogMetadata,
|
|
499
|
+
onClose,
|
|
500
|
+
} = props;
|
|
501
|
+
|
|
502
|
+
/* -------------- State ------------- */
|
|
503
|
+
|
|
504
|
+
// Create initial date filter state
|
|
505
|
+
const today = getTimeInfoInET();
|
|
506
|
+
const initStartDate: DateTriple = {
|
|
507
|
+
year: today.year,
|
|
508
|
+
month: today.month,
|
|
509
|
+
day: 1,
|
|
510
|
+
};
|
|
511
|
+
const initEndDate: DateTriple = {
|
|
512
|
+
year: today.year,
|
|
513
|
+
month: today.month,
|
|
514
|
+
day: today.day,
|
|
515
|
+
};
|
|
516
|
+
const initDateFilterState: DateFilterState = {
|
|
517
|
+
startDate: initStartDate,
|
|
518
|
+
endDate: initEndDate,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// Create initial context filter state
|
|
522
|
+
const initContextFilterState: ContextFilterState = {};
|
|
523
|
+
Object.keys(LogMetadata.Context ?? {}).forEach((context) => {
|
|
524
|
+
const contextValue = (LogMetadata.Context ?? {})[context];
|
|
525
|
+
if (typeof contextValue === 'string') {
|
|
526
|
+
// Case: no subcontexts
|
|
527
|
+
initContextFilterState[contextValue] = true;
|
|
528
|
+
} else {
|
|
529
|
+
// Case: subcontexts exist
|
|
530
|
+
initContextFilterState[contextValue._] = {};
|
|
531
|
+
Object.values((LogMetadata.Context ?? {})[context]).forEach((subcontext) => {
|
|
532
|
+
const subcontextValue = contextValue[subcontext];
|
|
533
|
+
(initContextFilterState[contextValue._] as { [k: string]: boolean })[subcontextValue] = true;
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Create initial tag filter state
|
|
539
|
+
const initTagFilterState: TagFilterState = {};
|
|
540
|
+
Object.values(LogMetadata.Tag ?? {}).forEach((tagValue) => {
|
|
541
|
+
initTagFilterState[tagValue] = true;
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Create advanced filter state
|
|
545
|
+
const initAdvancedFilterState: AdvancedFilterState = {
|
|
546
|
+
userFirstName: '',
|
|
547
|
+
userLastName: '',
|
|
548
|
+
userEmail: '',
|
|
549
|
+
userId: '',
|
|
550
|
+
includeLearners: true,
|
|
551
|
+
includeTTMs: true,
|
|
552
|
+
includeAdmins: true,
|
|
553
|
+
courseId: '',
|
|
554
|
+
courseName: '',
|
|
555
|
+
isMobile: undefined,
|
|
556
|
+
source: undefined,
|
|
557
|
+
routePath: '',
|
|
558
|
+
routeTemplate: '',
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// Create action and error filter state
|
|
562
|
+
const initActionErrorFilterState: ActionErrorFilterState = {
|
|
563
|
+
type: undefined,
|
|
564
|
+
errorMessage: '',
|
|
565
|
+
errorCode: '',
|
|
566
|
+
target: {},
|
|
567
|
+
action: {},
|
|
568
|
+
};
|
|
569
|
+
Object.values(LogMetadata.Target ?? {}).forEach((target) => {
|
|
570
|
+
initActionErrorFilterState.target[target] = true;
|
|
571
|
+
});
|
|
572
|
+
Object.values(LogAction).forEach((action) => {
|
|
573
|
+
initActionErrorFilterState.action[action] = true;
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Initial state
|
|
577
|
+
const initialState: State = {
|
|
578
|
+
loading: true,
|
|
579
|
+
logMap: {},
|
|
580
|
+
expandedFilterDrawer: undefined,
|
|
581
|
+
dateFilterState: initDateFilterState,
|
|
582
|
+
contextFilterState: initContextFilterState,
|
|
583
|
+
tagFilterState: initTagFilterState,
|
|
584
|
+
actionErrorFilterState: initActionErrorFilterState,
|
|
585
|
+
advancedFilterState: initAdvancedFilterState,
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// Initialize state
|
|
589
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
590
|
+
|
|
591
|
+
// Destructure common state
|
|
592
|
+
const {
|
|
593
|
+
loading,
|
|
594
|
+
logMap,
|
|
595
|
+
expandedFilterDrawer,
|
|
596
|
+
dateFilterState,
|
|
597
|
+
contextFilterState,
|
|
598
|
+
tagFilterState,
|
|
599
|
+
actionErrorFilterState,
|
|
600
|
+
advancedFilterState,
|
|
601
|
+
} = state;
|
|
602
|
+
|
|
603
|
+
/*------------------------------------------------------------------------*/
|
|
604
|
+
/* Component Functions */
|
|
605
|
+
/*------------------------------------------------------------------------*/
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Get the list of year/month combos that need to be loaded given a new
|
|
609
|
+
* start or end date and the existing logMap
|
|
610
|
+
* @author Gabe Abrams
|
|
611
|
+
* @param newDateFilterState the new date filter state
|
|
612
|
+
* @returns list of year/month combos that need to be loaded
|
|
613
|
+
*/
|
|
614
|
+
const listMonthsToLoad = (
|
|
615
|
+
newDateFilterState: DateFilterState,
|
|
616
|
+
): { year: number, month: number }[] => {
|
|
617
|
+
// List of year/month combos that need to be loaded
|
|
618
|
+
const toLoad: { year: number, month: number }[] = [];
|
|
619
|
+
|
|
620
|
+
// Loop through dates
|
|
621
|
+
let year = newDateFilterState.startDate.year;
|
|
622
|
+
let month = newDateFilterState.startDate.month;
|
|
623
|
+
while (
|
|
624
|
+
// Earlier year
|
|
625
|
+
(year <= newDateFilterState.endDate.year)
|
|
626
|
+
// Current year but included month
|
|
627
|
+
|| (
|
|
628
|
+
year === newDateFilterState.endDate.year
|
|
629
|
+
&& month <= newDateFilterState.endDate.month
|
|
630
|
+
)
|
|
631
|
+
) {
|
|
632
|
+
// Add to list
|
|
633
|
+
toLoad.push({
|
|
634
|
+
year,
|
|
635
|
+
month,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Increment
|
|
639
|
+
month += 1;
|
|
640
|
+
if (month > 12) {
|
|
641
|
+
month -= 12;
|
|
642
|
+
year += 1;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Return
|
|
647
|
+
return toLoad;
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Handle updated start/end dates (updates state, loads if necessary)
|
|
652
|
+
* @author Gabe Abrams
|
|
653
|
+
* @param newDateFilterState the new date filter state
|
|
654
|
+
*/
|
|
655
|
+
const handleDateRangeUpdated = async (
|
|
656
|
+
newDateFilterState: DateFilterState,
|
|
657
|
+
) => {
|
|
658
|
+
// Update state
|
|
659
|
+
dispatch({
|
|
660
|
+
type: ActionType.UpdateDateFilterState,
|
|
661
|
+
dateFilterState: newDateFilterState,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// Check which year/month combos we need to load
|
|
665
|
+
const toLoad = listMonthsToLoad(newDateFilterState);
|
|
666
|
+
|
|
667
|
+
// If nothing to load, finished
|
|
668
|
+
if (toLoad.length === 0) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Start loading
|
|
673
|
+
dispatch({
|
|
674
|
+
type: ActionType.StartLoading,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// Load required months
|
|
678
|
+
try {
|
|
679
|
+
for (let i = 0; i < toLoad.length; i++) {
|
|
680
|
+
// Destructure
|
|
681
|
+
const { year, month } = toLoad[i];
|
|
682
|
+
|
|
683
|
+
// Load
|
|
684
|
+
const logs = await visitServerEndpoint({
|
|
685
|
+
path: `${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/${year}/months/${month}`,
|
|
686
|
+
method: 'GET',
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Add to map
|
|
690
|
+
if (!logMap[year]) {
|
|
691
|
+
logMap[year] = {};
|
|
692
|
+
}
|
|
693
|
+
logMap[year][month] = logs;
|
|
694
|
+
}
|
|
695
|
+
} catch (err) {
|
|
696
|
+
return showFatalError(err);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Finish loading
|
|
700
|
+
dispatch({
|
|
701
|
+
type: ActionType.FinishLoading,
|
|
702
|
+
logMap,
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
/*------------------------------------------------------------------------*/
|
|
707
|
+
/* Lifecycle Functions */
|
|
708
|
+
/*------------------------------------------------------------------------*/
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Mount
|
|
712
|
+
* @author Gabe Abrams
|
|
713
|
+
*/
|
|
714
|
+
useEffect(
|
|
715
|
+
() => {
|
|
716
|
+
// Perform initial load
|
|
717
|
+
handleDateRangeUpdated(dateFilterState);
|
|
718
|
+
},
|
|
719
|
+
[],
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
/*------------------------------------------------------------------------*/
|
|
723
|
+
/* Render */
|
|
724
|
+
/*------------------------------------------------------------------------*/
|
|
725
|
+
|
|
726
|
+
/*----------------------------------------*/
|
|
727
|
+
/* Main UI */
|
|
728
|
+
/*----------------------------------------*/
|
|
729
|
+
|
|
730
|
+
// Body that will be filled with the contents of the panel
|
|
731
|
+
let body: React.ReactNode;
|
|
732
|
+
|
|
733
|
+
/* ------------- Loading ------------ */
|
|
734
|
+
|
|
735
|
+
if (loading) {
|
|
736
|
+
body = (
|
|
737
|
+
<div className="text-center p-5">
|
|
738
|
+
<LoadingSpinner />
|
|
739
|
+
</div>
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/* ------------ Review UI ----------- */
|
|
744
|
+
|
|
745
|
+
if (!loading) {
|
|
746
|
+
/*----------------------------------------*/
|
|
747
|
+
/* Filters */
|
|
748
|
+
/*----------------------------------------*/
|
|
749
|
+
|
|
750
|
+
// Filter toggle
|
|
751
|
+
const filterToggles = (
|
|
752
|
+
<div className="LogReviewer-filter-toggles">
|
|
753
|
+
<h3 className="m-0">
|
|
754
|
+
Filters:
|
|
755
|
+
</h3>
|
|
756
|
+
<div className="LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0">
|
|
757
|
+
{/* Date */}
|
|
758
|
+
<button
|
|
759
|
+
type="button"
|
|
760
|
+
id="LogReviewer-toggle-date-filter-drawer"
|
|
761
|
+
className={`btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
|
|
762
|
+
aria-label="toggle date filter drawer"
|
|
763
|
+
onClick={() => {
|
|
764
|
+
dispatch({
|
|
765
|
+
type: ActionType.ToggleFilterDrawer,
|
|
766
|
+
filterDrawer: FilterDrawer.Date,
|
|
767
|
+
});
|
|
768
|
+
}}
|
|
769
|
+
>
|
|
770
|
+
<FontAwesomeIcon
|
|
771
|
+
icon={faCalendar}
|
|
772
|
+
className="me-2"
|
|
773
|
+
/>
|
|
774
|
+
Date
|
|
775
|
+
</button>
|
|
776
|
+
{/* Context */}
|
|
777
|
+
<button
|
|
778
|
+
type="button"
|
|
779
|
+
id="LogReviewer-toggle-context-filter-drawer"
|
|
780
|
+
className={`btn btn-${FilterDrawer.Context === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
|
|
781
|
+
aria-label="toggle context filter drawer"
|
|
782
|
+
onClick={() => {
|
|
783
|
+
dispatch({
|
|
784
|
+
type: ActionType.ToggleFilterDrawer,
|
|
785
|
+
filterDrawer: FilterDrawer.Context,
|
|
786
|
+
});
|
|
787
|
+
}}
|
|
788
|
+
>
|
|
789
|
+
<FontAwesomeIcon
|
|
790
|
+
icon={faCircle}
|
|
791
|
+
className="me-2"
|
|
792
|
+
/>
|
|
793
|
+
Context
|
|
794
|
+
</button>
|
|
795
|
+
{/* Tag */}
|
|
796
|
+
{/* Skip if no tags are used */}
|
|
797
|
+
{(LogMetadata.Tag && Object.keys(LogMetadata.Tag).length > 0) && (
|
|
798
|
+
<button
|
|
799
|
+
type="button"
|
|
800
|
+
id="LogReviewer-toggle-tag-filter-drawer"
|
|
801
|
+
className={`btn btn-${FilterDrawer.Tag === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
|
|
802
|
+
aria-label="toggle tag filter drawer"
|
|
803
|
+
onClick={() => {
|
|
804
|
+
dispatch({
|
|
805
|
+
type: ActionType.ToggleFilterDrawer,
|
|
806
|
+
filterDrawer: FilterDrawer.Tag,
|
|
807
|
+
});
|
|
808
|
+
}}
|
|
809
|
+
>
|
|
810
|
+
<FontAwesomeIcon
|
|
811
|
+
icon={faTag}
|
|
812
|
+
className="me-2"
|
|
813
|
+
/>
|
|
814
|
+
Tag
|
|
815
|
+
</button>
|
|
816
|
+
)}
|
|
817
|
+
{/* Action */}
|
|
818
|
+
<button
|
|
819
|
+
type="button"
|
|
820
|
+
id="LogReviewer-toggle-action-filter-drawer"
|
|
821
|
+
className={`btn btn-${FilterDrawer.Action === expandedFilterDrawer ? 'warning' : 'light'} me-2`}
|
|
822
|
+
aria-label="toggle action and error filter drawer"
|
|
823
|
+
onClick={() => {
|
|
824
|
+
dispatch({
|
|
825
|
+
type: ActionType.ToggleFilterDrawer,
|
|
826
|
+
filterDrawer: FilterDrawer.Action,
|
|
827
|
+
});
|
|
828
|
+
}}
|
|
829
|
+
>
|
|
830
|
+
<FontAwesomeIcon
|
|
831
|
+
icon={faHammer}
|
|
832
|
+
className="me-2"
|
|
833
|
+
/>
|
|
834
|
+
Action
|
|
835
|
+
</button>
|
|
836
|
+
{/* Advanced */}
|
|
837
|
+
<button
|
|
838
|
+
type="button"
|
|
839
|
+
id="LogReviewer-toggle-advanced-filter-drawer"
|
|
840
|
+
className={`btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'}`}
|
|
841
|
+
aria-label="toggle advanced filter drawer"
|
|
842
|
+
onClick={() => {
|
|
843
|
+
dispatch({
|
|
844
|
+
type: ActionType.ToggleFilterDrawer,
|
|
845
|
+
filterDrawer: FilterDrawer.Advanced,
|
|
846
|
+
});
|
|
847
|
+
}}
|
|
848
|
+
>
|
|
849
|
+
<FontAwesomeIcon
|
|
850
|
+
icon={faList}
|
|
851
|
+
className="me-2"
|
|
852
|
+
/>
|
|
853
|
+
Advanced
|
|
854
|
+
</button>
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
// Filter drawer
|
|
860
|
+
let filterDrawer: React.ReactNode;
|
|
861
|
+
if (expandedFilterDrawer) {
|
|
862
|
+
if (expandedFilterDrawer === FilterDrawer.Date) {
|
|
863
|
+
filterDrawer = (
|
|
864
|
+
<TabBox title="Date">
|
|
865
|
+
<SimpleDateChooser
|
|
866
|
+
ariaLabel="filter start date"
|
|
867
|
+
name="filter-start-date"
|
|
868
|
+
year={dateFilterState.startDate.year}
|
|
869
|
+
month={dateFilterState.startDate.month}
|
|
870
|
+
day={dateFilterState.startDate.day}
|
|
871
|
+
onChange={(month, day, year) => {
|
|
872
|
+
dispatch({
|
|
873
|
+
type: ActionType.UpdateDateFilterState,
|
|
874
|
+
dateFilterState: {
|
|
875
|
+
...dateFilterState,
|
|
876
|
+
startDate: { month, day, year },
|
|
877
|
+
},
|
|
878
|
+
});
|
|
879
|
+
}}
|
|
880
|
+
/>
|
|
881
|
+
{' '}
|
|
882
|
+
to
|
|
883
|
+
{' '}
|
|
884
|
+
<SimpleDateChooser
|
|
885
|
+
ariaLabel="filter end date"
|
|
886
|
+
name="filter-end-date"
|
|
887
|
+
year={dateFilterState.endDate.year}
|
|
888
|
+
month={dateFilterState.endDate.month}
|
|
889
|
+
day={dateFilterState.endDate.day}
|
|
890
|
+
onChange={(month, day, year) => {
|
|
891
|
+
dispatch({
|
|
892
|
+
type: ActionType.UpdateDateFilterState,
|
|
893
|
+
dateFilterState: {
|
|
894
|
+
...dateFilterState,
|
|
895
|
+
endDate: { month, day, year },
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
}}
|
|
899
|
+
/>
|
|
900
|
+
</TabBox>
|
|
901
|
+
);
|
|
902
|
+
} else if (expandedFilterDrawer === FilterDrawer.Context) {
|
|
903
|
+
// Create item picker items
|
|
904
|
+
const pickableItems: PickableItem[] = (
|
|
905
|
+
Object.keys(LogMetadata.Context ?? {})
|
|
906
|
+
.map((context) => {
|
|
907
|
+
const value = (LogMetadata.Context ?? {})[context];
|
|
908
|
+
if (typeof value === 'string') {
|
|
909
|
+
// No subcategories
|
|
910
|
+
const item: PickableItem = {
|
|
911
|
+
id: context,
|
|
912
|
+
name: genHumanReadableName(context),
|
|
913
|
+
isGroup: false,
|
|
914
|
+
checked: !!contextFilterState[context],
|
|
915
|
+
};
|
|
916
|
+
return item;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Has subcategories
|
|
920
|
+
const children: PickableItem[] = (
|
|
921
|
+
Object.keys(value)
|
|
922
|
+
.filter((subcontext) => {
|
|
923
|
+
return subcontext !== '_'
|
|
924
|
+
})
|
|
925
|
+
.map((subcontext) => {
|
|
926
|
+
return {
|
|
927
|
+
id: `${context}-${subcontext}`,
|
|
928
|
+
name: genHumanReadableName(subcontext),
|
|
929
|
+
isGroup: false,
|
|
930
|
+
checked: !!value[subcontext],
|
|
931
|
+
};
|
|
932
|
+
})
|
|
933
|
+
);
|
|
934
|
+
const item: PickableItem = {
|
|
935
|
+
id: context,
|
|
936
|
+
name: context,
|
|
937
|
+
isGroup: true,
|
|
938
|
+
children,
|
|
939
|
+
};
|
|
940
|
+
return item;
|
|
941
|
+
})
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
// Create filter UI
|
|
945
|
+
filterDrawer = (
|
|
946
|
+
<ItemPicker
|
|
947
|
+
title="Context"
|
|
948
|
+
items={pickableItems}
|
|
949
|
+
onChanged={(updatedItems) => {
|
|
950
|
+
// Update our state
|
|
951
|
+
updatedItems.forEach((pickableItem) => {
|
|
952
|
+
if (pickableItem.isGroup) {
|
|
953
|
+
// Has subcontexts
|
|
954
|
+
pickableItem.children.forEach((subcontextItem) => {
|
|
955
|
+
(contextFilterState as any)[pickableItem.id][subcontextItem.id] = (
|
|
956
|
+
(subcontextItem as any).checked
|
|
957
|
+
);
|
|
958
|
+
});
|
|
959
|
+
} else {
|
|
960
|
+
// No subcontexts
|
|
961
|
+
(contextFilterState as any)[pickableItem.id] = (
|
|
962
|
+
pickableItem.checked
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
dispatch({
|
|
967
|
+
type: ActionType.UpdateContextFilterState,
|
|
968
|
+
contextFilterState,
|
|
969
|
+
});
|
|
970
|
+
}}
|
|
971
|
+
/>
|
|
972
|
+
);
|
|
973
|
+
} else if (expandedFilterDrawer === FilterDrawer.Tag) {
|
|
974
|
+
// Create filter UI
|
|
975
|
+
filterDrawer = (
|
|
976
|
+
<TabBox title="Tags">
|
|
977
|
+
{
|
|
978
|
+
Object.keys(tagFilterState)
|
|
979
|
+
.map((tag, i) => {
|
|
980
|
+
const description = genHumanReadableName(tag);
|
|
981
|
+
return (
|
|
982
|
+
<CheckboxButton
|
|
983
|
+
id={`LogReviewer-tag-${tag}-checkbox`}
|
|
984
|
+
text={description}
|
|
985
|
+
ariaLabel={`include logs tagged with "${description}" in results`}
|
|
986
|
+
noMarginOnRight={i === Object.keys(tagFilterState).length - 1}
|
|
987
|
+
onChanged={(checked) => {
|
|
988
|
+
tagFilterState[tag] = checked;
|
|
989
|
+
}}
|
|
990
|
+
/>
|
|
991
|
+
);
|
|
992
|
+
})
|
|
993
|
+
}
|
|
994
|
+
</TabBox>
|
|
995
|
+
);
|
|
996
|
+
} else if (expandedFilterDrawer === FilterDrawer.Action) {
|
|
997
|
+
// Create filter UI
|
|
998
|
+
filterDrawer = (
|
|
999
|
+
<>
|
|
1000
|
+
{/* Log Type */}
|
|
1001
|
+
<TabBox title="Log Type">
|
|
1002
|
+
<RadioButton
|
|
1003
|
+
id="LogReviewer-type-all"
|
|
1004
|
+
text="All Logs"
|
|
1005
|
+
onSelected={() => {
|
|
1006
|
+
actionErrorFilterState.type = undefined;
|
|
1007
|
+
dispatch({
|
|
1008
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
1009
|
+
actionErrorFilterState,
|
|
1010
|
+
});
|
|
1011
|
+
}}
|
|
1012
|
+
ariaLabel="show logs of all types"
|
|
1013
|
+
selected={actionErrorFilterState.type === undefined}
|
|
1014
|
+
/>
|
|
1015
|
+
<RadioButton
|
|
1016
|
+
id="LogReviewer-type-action-only"
|
|
1017
|
+
text="Action Logs Only"
|
|
1018
|
+
onSelected={() => {
|
|
1019
|
+
actionErrorFilterState.type = LogType.Action;
|
|
1020
|
+
dispatch({
|
|
1021
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
1022
|
+
actionErrorFilterState,
|
|
1023
|
+
});
|
|
1024
|
+
}}
|
|
1025
|
+
ariaLabel="only show action logs"
|
|
1026
|
+
selected={actionErrorFilterState.type === LogType.Action}
|
|
1027
|
+
/>
|
|
1028
|
+
<RadioButton
|
|
1029
|
+
id="LogReviewer-type-error-only"
|
|
1030
|
+
text="Action Error Only"
|
|
1031
|
+
onSelected={() => {
|
|
1032
|
+
actionErrorFilterState.type = LogType.Error;
|
|
1033
|
+
dispatch({
|
|
1034
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
1035
|
+
actionErrorFilterState,
|
|
1036
|
+
});
|
|
1037
|
+
}}
|
|
1038
|
+
ariaLabel="only show error logs"
|
|
1039
|
+
selected={actionErrorFilterState.type === LogType.Error}
|
|
1040
|
+
noMarginOnRight
|
|
1041
|
+
/>
|
|
1042
|
+
</TabBox>
|
|
1043
|
+
{/* Actions */}
|
|
1044
|
+
{
|
|
1045
|
+
(
|
|
1046
|
+
actionErrorFilterState.type === undefined
|
|
1047
|
+
|| actionErrorFilterState.type === LogType.Action
|
|
1048
|
+
) && (
|
|
1049
|
+
<TabBox title="Action Log Details">
|
|
1050
|
+
{/* Action */}
|
|
1051
|
+
<ButtonInputGroup
|
|
1052
|
+
label="Action"
|
|
1053
|
+
className="mb-2"
|
|
1054
|
+
>
|
|
1055
|
+
{
|
|
1056
|
+
Object.keys(LogAction)
|
|
1057
|
+
.map((action, i) => {
|
|
1058
|
+
const description = genHumanReadableName(action);
|
|
1059
|
+
return (
|
|
1060
|
+
<CheckboxButton
|
|
1061
|
+
id={`LogReviewer-action-${action}-checkbox`}
|
|
1062
|
+
text={description}
|
|
1063
|
+
ariaLabel={`include logs with action type "${description}" in results`}
|
|
1064
|
+
noMarginOnRight={i === Object.keys(LogAction).length - 1}
|
|
1065
|
+
onChanged={(checked) => {
|
|
1066
|
+
actionErrorFilterState.action[action] = checked;
|
|
1067
|
+
dispatch({
|
|
1068
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
1069
|
+
actionErrorFilterState,
|
|
1070
|
+
});
|
|
1071
|
+
}}
|
|
1072
|
+
/>
|
|
1073
|
+
);
|
|
1074
|
+
})
|
|
1075
|
+
}
|
|
1076
|
+
</ButtonInputGroup>
|
|
1077
|
+
{/* Target */}
|
|
1078
|
+
<ButtonInputGroup label="Target">
|
|
1079
|
+
{/* Nothing here */}
|
|
1080
|
+
{(Object.keys(LogMetadata.Target ?? {}).length === 0) && (
|
|
1081
|
+
<div>
|
|
1082
|
+
This app does not have any targets yet.
|
|
1083
|
+
</div>
|
|
1084
|
+
)}
|
|
1085
|
+
{/* List of targets */}
|
|
1086
|
+
{
|
|
1087
|
+
Object.keys(LogMetadata.Target ?? {})
|
|
1088
|
+
.map((target, i) => {
|
|
1089
|
+
const description = genHumanReadableName(target);
|
|
1090
|
+
return (
|
|
1091
|
+
<CheckboxButton
|
|
1092
|
+
id={`LogReviewer-target-${target}-checkbox`}
|
|
1093
|
+
text={description}
|
|
1094
|
+
ariaLabel={`include logs with target "${description}" in results`}
|
|
1095
|
+
onChanged={(checked) => {
|
|
1096
|
+
actionErrorFilterState.target[target] = checked;
|
|
1097
|
+
dispatch({
|
|
1098
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
1099
|
+
actionErrorFilterState,
|
|
1100
|
+
});
|
|
1101
|
+
}}
|
|
1102
|
+
noMarginOnRight={i === Object.keys(LogMetadata.Target ?? {}).length - 1}
|
|
1103
|
+
/>
|
|
1104
|
+
);
|
|
1105
|
+
})
|
|
1106
|
+
}
|
|
1107
|
+
</ButtonInputGroup>
|
|
1108
|
+
</TabBox>
|
|
1109
|
+
)
|
|
1110
|
+
}
|
|
1111
|
+
{/* Errors */}
|
|
1112
|
+
{
|
|
1113
|
+
(
|
|
1114
|
+
actionErrorFilterState.type === undefined
|
|
1115
|
+
|| actionErrorFilterState.type === LogType.Error
|
|
1116
|
+
) && (
|
|
1117
|
+
<TabBox title="Error Log Details">
|
|
1118
|
+
{/* Message */}
|
|
1119
|
+
<div className="input-group mb-2">
|
|
1120
|
+
<span className="input-group-text">
|
|
1121
|
+
Error Message
|
|
1122
|
+
</span>
|
|
1123
|
+
<input
|
|
1124
|
+
type="text"
|
|
1125
|
+
className="form-control"
|
|
1126
|
+
aria-label="query for error message"
|
|
1127
|
+
value={actionErrorFilterState.errorMessage}
|
|
1128
|
+
onChange={(e) => {
|
|
1129
|
+
actionErrorFilterState.errorMessage = e.target.value;
|
|
1130
|
+
dispatch({
|
|
1131
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
1132
|
+
actionErrorFilterState,
|
|
1133
|
+
});
|
|
1134
|
+
}}
|
|
1135
|
+
/>
|
|
1136
|
+
</div>
|
|
1137
|
+
{/* Code */}
|
|
1138
|
+
<div className="input-group mb-2">
|
|
1139
|
+
<span className="input-group-text">
|
|
1140
|
+
Error Code
|
|
1141
|
+
</span>
|
|
1142
|
+
<input
|
|
1143
|
+
type="text"
|
|
1144
|
+
className="form-control"
|
|
1145
|
+
aria-label="query for error code"
|
|
1146
|
+
value={actionErrorFilterState.errorMessage}
|
|
1147
|
+
onChange={(e) => {
|
|
1148
|
+
actionErrorFilterState.errorCode = (
|
|
1149
|
+
(e.target.value)
|
|
1150
|
+
.trim()
|
|
1151
|
+
.toUpperCase()
|
|
1152
|
+
);
|
|
1153
|
+
dispatch({
|
|
1154
|
+
type: ActionType.UpdateActionErrorFilterState,
|
|
1155
|
+
actionErrorFilterState,
|
|
1156
|
+
});
|
|
1157
|
+
}}
|
|
1158
|
+
/>
|
|
1159
|
+
</div>
|
|
1160
|
+
</TabBox>
|
|
1161
|
+
)
|
|
1162
|
+
}
|
|
1163
|
+
</>
|
|
1164
|
+
);
|
|
1165
|
+
} else if (expandedFilterDrawer === FilterDrawer.Advanced) {
|
|
1166
|
+
// Create advanced filter ui
|
|
1167
|
+
filterDrawer = (
|
|
1168
|
+
<>
|
|
1169
|
+
{/* User Info */}
|
|
1170
|
+
<TabBox title="User Info">
|
|
1171
|
+
{/* First Name */}
|
|
1172
|
+
<div className="input-group mb-2">
|
|
1173
|
+
<span className="input-group-text">
|
|
1174
|
+
User First Name
|
|
1175
|
+
</span>
|
|
1176
|
+
<input
|
|
1177
|
+
type="text"
|
|
1178
|
+
className="form-control"
|
|
1179
|
+
aria-label="query for user first name"
|
|
1180
|
+
value={advancedFilterState.userFirstName}
|
|
1181
|
+
onChange={(e) => {
|
|
1182
|
+
advancedFilterState.userFirstName = e.target.value;
|
|
1183
|
+
dispatch({
|
|
1184
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1185
|
+
advancedFilterState,
|
|
1186
|
+
});
|
|
1187
|
+
}}
|
|
1188
|
+
/>
|
|
1189
|
+
</div>
|
|
1190
|
+
{/* Last Name */}
|
|
1191
|
+
<div className="input-group mb-2">
|
|
1192
|
+
<span className="input-group-text">
|
|
1193
|
+
User Last Name
|
|
1194
|
+
</span>
|
|
1195
|
+
<input
|
|
1196
|
+
type="text"
|
|
1197
|
+
className="form-control"
|
|
1198
|
+
aria-label="query for user last name"
|
|
1199
|
+
value={advancedFilterState.userLastName}
|
|
1200
|
+
onChange={(e) => {
|
|
1201
|
+
advancedFilterState.userLastName = e.target.value;
|
|
1202
|
+
dispatch({
|
|
1203
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1204
|
+
advancedFilterState,
|
|
1205
|
+
});
|
|
1206
|
+
}}
|
|
1207
|
+
/>
|
|
1208
|
+
</div>
|
|
1209
|
+
{/* Email */}
|
|
1210
|
+
<div className="input-group mb-2">
|
|
1211
|
+
<span className="input-group-text">
|
|
1212
|
+
User Email
|
|
1213
|
+
</span>
|
|
1214
|
+
<input
|
|
1215
|
+
type="text"
|
|
1216
|
+
className="form-control"
|
|
1217
|
+
aria-label="query for user email"
|
|
1218
|
+
value={advancedFilterState.userEmail}
|
|
1219
|
+
onChange={(e) => {
|
|
1220
|
+
advancedFilterState.userEmail = (
|
|
1221
|
+
(e.target.value)
|
|
1222
|
+
.trim()
|
|
1223
|
+
);
|
|
1224
|
+
dispatch({
|
|
1225
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1226
|
+
advancedFilterState,
|
|
1227
|
+
});
|
|
1228
|
+
}}
|
|
1229
|
+
/>
|
|
1230
|
+
</div>
|
|
1231
|
+
{/* Canvas Id */}
|
|
1232
|
+
<div className="input-group mb-2">
|
|
1233
|
+
<span className="input-group-text">
|
|
1234
|
+
User Canvas Id
|
|
1235
|
+
</span>
|
|
1236
|
+
<input
|
|
1237
|
+
type="text"
|
|
1238
|
+
className="form-control"
|
|
1239
|
+
aria-label="query for user canvas id"
|
|
1240
|
+
value={advancedFilterState.userId}
|
|
1241
|
+
onChange={(e) => {
|
|
1242
|
+
const { value } = e.target;
|
|
1243
|
+
// Only update if value contains only numbers
|
|
1244
|
+
if (/^\d+$/.test(value)) {
|
|
1245
|
+
advancedFilterState.userId = (
|
|
1246
|
+
(e.target.value)
|
|
1247
|
+
.trim()
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
dispatch({
|
|
1251
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1252
|
+
advancedFilterState,
|
|
1253
|
+
});
|
|
1254
|
+
}}
|
|
1255
|
+
/>
|
|
1256
|
+
</div>
|
|
1257
|
+
{/* Role */}
|
|
1258
|
+
<ButtonInputGroup label="Role">
|
|
1259
|
+
<CheckboxButton
|
|
1260
|
+
text="Students"
|
|
1261
|
+
onChanged={(checked) => {
|
|
1262
|
+
advancedFilterState.includeLearners = checked;
|
|
1263
|
+
dispatch({
|
|
1264
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1265
|
+
advancedFilterState,
|
|
1266
|
+
});
|
|
1267
|
+
}}
|
|
1268
|
+
checked={advancedFilterState.includeLearners}
|
|
1269
|
+
ariaLabel="show logs from students"
|
|
1270
|
+
/>
|
|
1271
|
+
<CheckboxButton
|
|
1272
|
+
text="Teaching Team Members"
|
|
1273
|
+
onChanged={(checked) => {
|
|
1274
|
+
advancedFilterState.includeTTMs = checked;
|
|
1275
|
+
dispatch({
|
|
1276
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1277
|
+
advancedFilterState,
|
|
1278
|
+
});
|
|
1279
|
+
}}
|
|
1280
|
+
checked={advancedFilterState.includeTTMs}
|
|
1281
|
+
ariaLabel="show logs from teaching team members"
|
|
1282
|
+
/>
|
|
1283
|
+
<CheckboxButton
|
|
1284
|
+
text="Admins"
|
|
1285
|
+
onChanged={(checked) => {
|
|
1286
|
+
advancedFilterState.includeAdmins = checked;
|
|
1287
|
+
dispatch({
|
|
1288
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1289
|
+
advancedFilterState,
|
|
1290
|
+
});
|
|
1291
|
+
}}
|
|
1292
|
+
checked={advancedFilterState.includeAdmins}
|
|
1293
|
+
ariaLabel="show logs from admins"
|
|
1294
|
+
/>
|
|
1295
|
+
</ButtonInputGroup>
|
|
1296
|
+
</TabBox>
|
|
1297
|
+
|
|
1298
|
+
{/* Course Info */}
|
|
1299
|
+
<TabBox title="Course Info">
|
|
1300
|
+
{/* Name */}
|
|
1301
|
+
<div className="input-group mb-2">
|
|
1302
|
+
<span className="input-group-text">
|
|
1303
|
+
Course Name
|
|
1304
|
+
</span>
|
|
1305
|
+
<input
|
|
1306
|
+
type="text"
|
|
1307
|
+
className="form-control"
|
|
1308
|
+
aria-label="query for course name"
|
|
1309
|
+
value={advancedFilterState.courseName}
|
|
1310
|
+
onChange={(e) => {
|
|
1311
|
+
advancedFilterState.courseName = e.target.value;
|
|
1312
|
+
dispatch({
|
|
1313
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1314
|
+
advancedFilterState,
|
|
1315
|
+
});
|
|
1316
|
+
}}
|
|
1317
|
+
/>
|
|
1318
|
+
</div>
|
|
1319
|
+
{/* Canvas Id */}
|
|
1320
|
+
<div className="input-group mb-2">
|
|
1321
|
+
<span className="input-group-text">
|
|
1322
|
+
Course Canvas Id
|
|
1323
|
+
</span>
|
|
1324
|
+
<input
|
|
1325
|
+
type="text"
|
|
1326
|
+
className="form-control"
|
|
1327
|
+
aria-label="query for course canvas id"
|
|
1328
|
+
value={advancedFilterState.courseId}
|
|
1329
|
+
onChange={(e) => {
|
|
1330
|
+
const { value } = e.target;
|
|
1331
|
+
// Only update if value contains only numbers
|
|
1332
|
+
if (/^\d+$/.test(value)) {
|
|
1333
|
+
advancedFilterState.courseId = (
|
|
1334
|
+
(e.target.value)
|
|
1335
|
+
.trim()
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
dispatch({
|
|
1339
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1340
|
+
advancedFilterState,
|
|
1341
|
+
});
|
|
1342
|
+
}}
|
|
1343
|
+
/>
|
|
1344
|
+
</div>
|
|
1345
|
+
</TabBox>
|
|
1346
|
+
|
|
1347
|
+
{/* Device Info */}
|
|
1348
|
+
<TabBox title="Device Info">
|
|
1349
|
+
<ButtonInputGroup label="Device Type">
|
|
1350
|
+
<RadioButton
|
|
1351
|
+
text="All Devices"
|
|
1352
|
+
ariaLabel="show logs from all devices"
|
|
1353
|
+
selected={advancedFilterState.isMobile === undefined}
|
|
1354
|
+
onSelected={() => {
|
|
1355
|
+
advancedFilterState.isMobile = undefined;
|
|
1356
|
+
dispatch({
|
|
1357
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1358
|
+
advancedFilterState,
|
|
1359
|
+
});
|
|
1360
|
+
}}
|
|
1361
|
+
/>
|
|
1362
|
+
<RadioButton
|
|
1363
|
+
text="Mobile Only"
|
|
1364
|
+
ariaLabel="show logs from mobile devices"
|
|
1365
|
+
selected={advancedFilterState.isMobile === true}
|
|
1366
|
+
onSelected={() => {
|
|
1367
|
+
advancedFilterState.isMobile = true;
|
|
1368
|
+
dispatch({
|
|
1369
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1370
|
+
advancedFilterState,
|
|
1371
|
+
});
|
|
1372
|
+
}}
|
|
1373
|
+
/>
|
|
1374
|
+
<RadioButton
|
|
1375
|
+
text="Desktop Only"
|
|
1376
|
+
ariaLabel="show logs from desktop devices"
|
|
1377
|
+
selected={advancedFilterState.isMobile === false}
|
|
1378
|
+
onSelected={() => {
|
|
1379
|
+
advancedFilterState.isMobile = false;
|
|
1380
|
+
dispatch({
|
|
1381
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1382
|
+
advancedFilterState,
|
|
1383
|
+
});
|
|
1384
|
+
}}
|
|
1385
|
+
noMarginOnRight
|
|
1386
|
+
/>
|
|
1387
|
+
</ButtonInputGroup>
|
|
1388
|
+
</TabBox>
|
|
1389
|
+
|
|
1390
|
+
{/* Source */}
|
|
1391
|
+
<TabBox title="Source">
|
|
1392
|
+
<ButtonInputGroup label="Source Type">
|
|
1393
|
+
<RadioButton
|
|
1394
|
+
text="Both"
|
|
1395
|
+
ariaLabel="show logs from all sources"
|
|
1396
|
+
selected={advancedFilterState.source === undefined}
|
|
1397
|
+
onSelected={() => {
|
|
1398
|
+
advancedFilterState.source = undefined;
|
|
1399
|
+
dispatch({
|
|
1400
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1401
|
+
advancedFilterState,
|
|
1402
|
+
});
|
|
1403
|
+
}}
|
|
1404
|
+
/>
|
|
1405
|
+
<RadioButton
|
|
1406
|
+
text="Client Only"
|
|
1407
|
+
ariaLabel="show logs from client source"
|
|
1408
|
+
selected={advancedFilterState.source === LogSource.Client}
|
|
1409
|
+
onSelected={() => {
|
|
1410
|
+
advancedFilterState.source = LogSource.Client;
|
|
1411
|
+
dispatch({
|
|
1412
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1413
|
+
advancedFilterState,
|
|
1414
|
+
});
|
|
1415
|
+
}}
|
|
1416
|
+
/>
|
|
1417
|
+
<RadioButton
|
|
1418
|
+
text="Server Only"
|
|
1419
|
+
ariaLabel="show logs from server source"
|
|
1420
|
+
selected={advancedFilterState.source === LogSource.Server}
|
|
1421
|
+
onSelected={() => {
|
|
1422
|
+
advancedFilterState.source = LogSource.Server;
|
|
1423
|
+
dispatch({
|
|
1424
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1425
|
+
advancedFilterState,
|
|
1426
|
+
});
|
|
1427
|
+
}}
|
|
1428
|
+
noMarginOnRight
|
|
1429
|
+
/>
|
|
1430
|
+
</ButtonInputGroup>
|
|
1431
|
+
|
|
1432
|
+
{/* Server filters */}
|
|
1433
|
+
{advancedFilterState.source !== LogSource.Client && (
|
|
1434
|
+
<div className="mt-2">
|
|
1435
|
+
{/* Route path */}
|
|
1436
|
+
<div className="input-group mb-2">
|
|
1437
|
+
<span className="input-group-text">
|
|
1438
|
+
Server Route Path
|
|
1439
|
+
</span>
|
|
1440
|
+
<input
|
|
1441
|
+
type="text"
|
|
1442
|
+
className="form-control"
|
|
1443
|
+
aria-label="query for server route path"
|
|
1444
|
+
placeholder="e.g. /api/ttm/courses/12345"
|
|
1445
|
+
value={advancedFilterState.routePath}
|
|
1446
|
+
onChange={(e) => {
|
|
1447
|
+
advancedFilterState.courseName = (
|
|
1448
|
+
(e.target.value)
|
|
1449
|
+
.trim()
|
|
1450
|
+
);
|
|
1451
|
+
dispatch({
|
|
1452
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1453
|
+
advancedFilterState,
|
|
1454
|
+
});
|
|
1455
|
+
}}
|
|
1456
|
+
/>
|
|
1457
|
+
</div>
|
|
1458
|
+
|
|
1459
|
+
{/* Route template */}
|
|
1460
|
+
<div className="input-group mb-2">
|
|
1461
|
+
<span className="input-group-text">
|
|
1462
|
+
Server Route Template
|
|
1463
|
+
</span>
|
|
1464
|
+
<input
|
|
1465
|
+
type="text"
|
|
1466
|
+
className="form-control"
|
|
1467
|
+
aria-label="query for server route template"
|
|
1468
|
+
value={advancedFilterState.routeTemplate}
|
|
1469
|
+
placeholder="e.g. /api/ttm/courses/:courseId"
|
|
1470
|
+
onChange={(e) => {
|
|
1471
|
+
advancedFilterState.courseName = (
|
|
1472
|
+
(e.target.value)
|
|
1473
|
+
.trim()
|
|
1474
|
+
);
|
|
1475
|
+
dispatch({
|
|
1476
|
+
type: ActionType.UpdateAdvancedFilterState,
|
|
1477
|
+
advancedFilterState,
|
|
1478
|
+
});
|
|
1479
|
+
}}
|
|
1480
|
+
/>
|
|
1481
|
+
</div>
|
|
1482
|
+
</div>
|
|
1483
|
+
)}
|
|
1484
|
+
</TabBox>
|
|
1485
|
+
</>
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Filters UI
|
|
1491
|
+
const filters = (
|
|
1492
|
+
<>
|
|
1493
|
+
{filterToggles}
|
|
1494
|
+
{filterDrawer && (
|
|
1495
|
+
<Drawer>
|
|
1496
|
+
{filterDrawer}
|
|
1497
|
+
</Drawer>
|
|
1498
|
+
)}
|
|
1499
|
+
</>
|
|
1500
|
+
);
|
|
1501
|
+
|
|
1502
|
+
// Actually filter the logs
|
|
1503
|
+
// > Perform filters
|
|
1504
|
+
const logs: Log[] = [];
|
|
1505
|
+
Object.keys(logMap).forEach((year) => {
|
|
1506
|
+
Object.keys(logMap[year]).forEach((month) => {
|
|
1507
|
+
logMap[year][month].forEach((log) => {
|
|
1508
|
+
/* ----------- Date Filter ---------- */
|
|
1509
|
+
|
|
1510
|
+
// Before start date
|
|
1511
|
+
if (
|
|
1512
|
+
// Previous year
|
|
1513
|
+
log.year < dateFilterState.startDate.year
|
|
1514
|
+
// Same year, earlier month
|
|
1515
|
+
|| (
|
|
1516
|
+
(log.year === dateFilterState.startDate.year)
|
|
1517
|
+
&& (log.month < dateFilterState.startDate.month)
|
|
1518
|
+
)
|
|
1519
|
+
// Same year, same month, earlier day
|
|
1520
|
+
|| (
|
|
1521
|
+
(log.year === dateFilterState.startDate.year)
|
|
1522
|
+
&& (log.month === dateFilterState.startDate.month)
|
|
1523
|
+
&& (log.day < dateFilterState.startDate.day)
|
|
1524
|
+
)
|
|
1525
|
+
) {
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// After end date
|
|
1530
|
+
if (
|
|
1531
|
+
// Later year
|
|
1532
|
+
log.year > dateFilterState.endDate.year
|
|
1533
|
+
// Same year, later month
|
|
1534
|
+
|| (
|
|
1535
|
+
(log.year === dateFilterState.endDate.year)
|
|
1536
|
+
&& (log.month > dateFilterState.endDate.month)
|
|
1537
|
+
)
|
|
1538
|
+
// Same year, same month, later day
|
|
1539
|
+
|| (
|
|
1540
|
+
(log.year === dateFilterState.endDate.year)
|
|
1541
|
+
&& (log.month === dateFilterState.endDate.month)
|
|
1542
|
+
&& (log.day > dateFilterState.endDate.day)
|
|
1543
|
+
)
|
|
1544
|
+
) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/* --------- Context Filter --------- */
|
|
1549
|
+
|
|
1550
|
+
// Context doesn't match
|
|
1551
|
+
if (
|
|
1552
|
+
// Whole context is deselected
|
|
1553
|
+
contextFilterState[log.context] === false
|
|
1554
|
+
// None of the subcontexts are selected
|
|
1555
|
+
|| (
|
|
1556
|
+
Object.values(contextFilterState[log.context] ?? {})
|
|
1557
|
+
.every((isSelected) => {
|
|
1558
|
+
return !isSelected;
|
|
1559
|
+
})
|
|
1560
|
+
)
|
|
1561
|
+
) {
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Subcontext doesn't match
|
|
1566
|
+
if (
|
|
1567
|
+
// Log has a subcontext
|
|
1568
|
+
log.subcontext
|
|
1569
|
+
// Context has subcontexts
|
|
1570
|
+
&& (
|
|
1571
|
+
contextFilterState[log.context]
|
|
1572
|
+
&& contextFilterState[log.context] !== false
|
|
1573
|
+
&& contextFilterState[log.context] !== true
|
|
1574
|
+
)
|
|
1575
|
+
// Subcontext is not selected
|
|
1576
|
+
&& !(contextFilterState as any)[log.context][log.subcontext]
|
|
1577
|
+
) {
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/* -------------- Tags -------------- */
|
|
1582
|
+
|
|
1583
|
+
// No tags match
|
|
1584
|
+
if (
|
|
1585
|
+
// Log has at least one tag
|
|
1586
|
+
log.tags.length > 0
|
|
1587
|
+
// No tags match
|
|
1588
|
+
&& (
|
|
1589
|
+
log.tags.every((tag) => {
|
|
1590
|
+
return !tagFilterState[tag];
|
|
1591
|
+
})
|
|
1592
|
+
)
|
|
1593
|
+
) {
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/* ------- Actions and Errors ------- */
|
|
1598
|
+
|
|
1599
|
+
// Log type doesn't match
|
|
1600
|
+
if (
|
|
1601
|
+
// Filter won't allow all types
|
|
1602
|
+
actionErrorFilterState.type !== undefined
|
|
1603
|
+
// Log type doesn't match
|
|
1604
|
+
&& actionErrorFilterState.type !== log.type
|
|
1605
|
+
) {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Filter errors
|
|
1610
|
+
if (log.type === LogType.Error) {
|
|
1611
|
+
// Message doesn't match
|
|
1612
|
+
if (
|
|
1613
|
+
// Message exists
|
|
1614
|
+
log.errorMessage
|
|
1615
|
+
// Message filter exists
|
|
1616
|
+
&& actionErrorFilterState.errorMessage.trim().length > 0
|
|
1617
|
+
// Message doesn't match
|
|
1618
|
+
&& log.errorMessage.toLowerCase().includes(
|
|
1619
|
+
actionErrorFilterState.errorMessage.trim().toLowerCase(),
|
|
1620
|
+
)
|
|
1621
|
+
) {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// Code doesn't match
|
|
1626
|
+
if (
|
|
1627
|
+
// Code exists
|
|
1628
|
+
log.errorCode
|
|
1629
|
+
// Code filter exists
|
|
1630
|
+
&& actionErrorFilterState.errorCode.trim().length > 0
|
|
1631
|
+
// Code doesn't match
|
|
1632
|
+
&& log.errorCode.toUpperCase().includes(
|
|
1633
|
+
actionErrorFilterState.errorCode.trim().toUpperCase(),
|
|
1634
|
+
)
|
|
1635
|
+
) {
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// Filter actions
|
|
1641
|
+
if (log.type === LogType.Action) {
|
|
1642
|
+
// Target isn't selected
|
|
1643
|
+
if (
|
|
1644
|
+
// Target exists
|
|
1645
|
+
log.target
|
|
1646
|
+
// Target isn't selected
|
|
1647
|
+
&& !actionErrorFilterState.target[log.target]
|
|
1648
|
+
) {
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Action
|
|
1653
|
+
if (
|
|
1654
|
+
// Action exists
|
|
1655
|
+
log.action
|
|
1656
|
+
// Action isn't selected
|
|
1657
|
+
&& !actionErrorFilterState.action[log.action]
|
|
1658
|
+
) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/* --------- Advanced Filter -------- */
|
|
1664
|
+
|
|
1665
|
+
// First name doesn't match
|
|
1666
|
+
if (
|
|
1667
|
+
// First name exists
|
|
1668
|
+
log.userFirstName
|
|
1669
|
+
// First name query doesn't match
|
|
1670
|
+
&& !log.userFirstName.toLowerCase().includes(
|
|
1671
|
+
advancedFilterState.userFirstName.toLowerCase().trim(),
|
|
1672
|
+
)
|
|
1673
|
+
) {
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// Last name doesn't match
|
|
1678
|
+
if (
|
|
1679
|
+
// Last name exists
|
|
1680
|
+
log.userLastName
|
|
1681
|
+
// Last name query doesn't match
|
|
1682
|
+
&& !log.userLastName.toLowerCase().includes(
|
|
1683
|
+
advancedFilterState.userLastName.toLowerCase().trim(),
|
|
1684
|
+
)
|
|
1685
|
+
) {
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Email doesn't match
|
|
1690
|
+
if (
|
|
1691
|
+
// Email exists
|
|
1692
|
+
log.userEmail
|
|
1693
|
+
// Email query doesn't match
|
|
1694
|
+
&& !log.userEmail.toLowerCase().includes(
|
|
1695
|
+
advancedFilterState.userEmail.toLowerCase().trim(),
|
|
1696
|
+
)
|
|
1697
|
+
) {
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// User id doesn't match
|
|
1702
|
+
if (
|
|
1703
|
+
// User id exists
|
|
1704
|
+
log.userId
|
|
1705
|
+
// User id doesn't match
|
|
1706
|
+
&& !String(log.userId).includes(
|
|
1707
|
+
advancedFilterState.userId.trim(),
|
|
1708
|
+
)
|
|
1709
|
+
) {
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// Learner not allowed
|
|
1714
|
+
if (
|
|
1715
|
+
// User is a learner
|
|
1716
|
+
log.isLearner
|
|
1717
|
+
// Learners aren't included
|
|
1718
|
+
&& !advancedFilterState.includeLearners
|
|
1719
|
+
) {
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// TTM not allowed
|
|
1724
|
+
if (
|
|
1725
|
+
// User is a ttm
|
|
1726
|
+
log.isTTM
|
|
1727
|
+
// TTMs aren't included
|
|
1728
|
+
&& !advancedFilterState.includeTTMs
|
|
1729
|
+
) {
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// Admin not allowed
|
|
1734
|
+
if (
|
|
1735
|
+
// User is an admin
|
|
1736
|
+
log.isAdmin
|
|
1737
|
+
// Admins aren't included
|
|
1738
|
+
&& !advancedFilterState.includeAdmins
|
|
1739
|
+
) {
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Course Id doesn't match
|
|
1744
|
+
if (
|
|
1745
|
+
// Course Id exists
|
|
1746
|
+
log.courseId
|
|
1747
|
+
// Course Id doesn't match
|
|
1748
|
+
&& !String(log.courseId).includes(
|
|
1749
|
+
advancedFilterState.courseId.trim(),
|
|
1750
|
+
)
|
|
1751
|
+
) {
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Course name doesn't match
|
|
1756
|
+
if (
|
|
1757
|
+
// Course name exists
|
|
1758
|
+
log.courseName
|
|
1759
|
+
// Course name doesn't match
|
|
1760
|
+
&& !String(log.courseName).includes(
|
|
1761
|
+
advancedFilterState.courseName.trim(),
|
|
1762
|
+
)
|
|
1763
|
+
) {
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// Mobile filter doesn't match
|
|
1768
|
+
if (
|
|
1769
|
+
// Mobile filter exists
|
|
1770
|
+
advancedFilterState.isMobile !== undefined
|
|
1771
|
+
// Device info exists
|
|
1772
|
+
&& log.device
|
|
1773
|
+
// Mobile filter doesn't match
|
|
1774
|
+
&& (advancedFilterState.isMobile === log.device.isMobile)
|
|
1775
|
+
) {
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// Log source doesn't match
|
|
1780
|
+
if (
|
|
1781
|
+
// Source filter exists
|
|
1782
|
+
advancedFilterState.source !== undefined
|
|
1783
|
+
// Source info exists
|
|
1784
|
+
&& log.source
|
|
1785
|
+
// Source filter doesn't match
|
|
1786
|
+
&& (advancedFilterState.source !== log.source)
|
|
1787
|
+
) {
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// Route path doesn't match (Only for server source)
|
|
1792
|
+
if (
|
|
1793
|
+
// Source is server
|
|
1794
|
+
(log.source === LogSource.Server)
|
|
1795
|
+
// Route path is being filtered
|
|
1796
|
+
&& (advancedFilterState.routePath.trim().length)
|
|
1797
|
+
// Route path doesn't match
|
|
1798
|
+
&& !(log.routePath.includes(advancedFilterState.routePath.trim()))
|
|
1799
|
+
) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// Route template doesn't match (Only for server source)
|
|
1804
|
+
if (
|
|
1805
|
+
// Source is server
|
|
1806
|
+
(log.source === LogSource.Server)
|
|
1807
|
+
// Route template is being filtered
|
|
1808
|
+
&& (advancedFilterState.routeTemplate.trim().length)
|
|
1809
|
+
// Route template doesn't match
|
|
1810
|
+
&& !(log.routeTemplate.includes(advancedFilterState.routeTemplate.trim()))
|
|
1811
|
+
) {
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/* -------------- Done -------------- */
|
|
1816
|
+
|
|
1817
|
+
// Made it past all filters. Add to the list
|
|
1818
|
+
logs.push(log);
|
|
1819
|
+
});
|
|
1820
|
+
});
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
/*----------------------------------------*/
|
|
1824
|
+
/* Data */
|
|
1825
|
+
/*----------------------------------------*/
|
|
1826
|
+
|
|
1827
|
+
// Create data table
|
|
1828
|
+
const columns: IntelliTableColumn[] = [
|
|
1829
|
+
{
|
|
1830
|
+
title: 'First Name',
|
|
1831
|
+
param: 'userFirstName',
|
|
1832
|
+
type: ParamType.String,
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
title: 'Last Name',
|
|
1836
|
+
param: 'userLastName',
|
|
1837
|
+
type: ParamType.String,
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
title: 'Email',
|
|
1841
|
+
param: 'userEmail',
|
|
1842
|
+
type: ParamType.String,
|
|
1843
|
+
},
|
|
1844
|
+
{
|
|
1845
|
+
title: 'Canvas Id',
|
|
1846
|
+
param: 'userId',
|
|
1847
|
+
type: ParamType.Int,
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
title: 'Student',
|
|
1851
|
+
param: 'isLearner',
|
|
1852
|
+
type: ParamType.Boolean,
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
title: 'Teaching Staff',
|
|
1856
|
+
param: 'isTTM',
|
|
1857
|
+
type: ParamType.Boolean,
|
|
1858
|
+
startsHidden: true,
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
title: 'Admin',
|
|
1862
|
+
param: 'isAdmin',
|
|
1863
|
+
type: ParamType.Boolean,
|
|
1864
|
+
startsHidden: true,
|
|
1865
|
+
},
|
|
1866
|
+
{
|
|
1867
|
+
title: 'Course Canvas Id',
|
|
1868
|
+
param: 'courseId',
|
|
1869
|
+
type: ParamType.Int,
|
|
1870
|
+
startsHidden: true,
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
title: 'Course Name',
|
|
1874
|
+
param: 'courseName',
|
|
1875
|
+
type: ParamType.String,
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
title: 'Browser Name',
|
|
1879
|
+
param: 'browser.name',
|
|
1880
|
+
type: ParamType.String,
|
|
1881
|
+
startsHidden: true,
|
|
1882
|
+
},
|
|
1883
|
+
{
|
|
1884
|
+
title: 'Browser Version',
|
|
1885
|
+
param: 'browser.version',
|
|
1886
|
+
type: ParamType.String,
|
|
1887
|
+
startsHidden: true,
|
|
1888
|
+
},
|
|
1889
|
+
{
|
|
1890
|
+
title: 'OS',
|
|
1891
|
+
param: 'device.os',
|
|
1892
|
+
type: ParamType.String,
|
|
1893
|
+
startsHidden: true,
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
title: 'Mobile',
|
|
1897
|
+
param: 'device.isMobile',
|
|
1898
|
+
type: ParamType.Boolean,
|
|
1899
|
+
startsHidden: true,
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
title: 'Year',
|
|
1903
|
+
param: 'year',
|
|
1904
|
+
type: ParamType.Int,
|
|
1905
|
+
},
|
|
1906
|
+
{
|
|
1907
|
+
title: 'Month',
|
|
1908
|
+
param: 'month',
|
|
1909
|
+
type: ParamType.Int,
|
|
1910
|
+
},
|
|
1911
|
+
{
|
|
1912
|
+
title: 'Day',
|
|
1913
|
+
param: 'day',
|
|
1914
|
+
type: ParamType.Int,
|
|
1915
|
+
},
|
|
1916
|
+
{
|
|
1917
|
+
title: 'Hour',
|
|
1918
|
+
param: 'hour',
|
|
1919
|
+
type: ParamType.Int,
|
|
1920
|
+
},
|
|
1921
|
+
{
|
|
1922
|
+
title: 'Minute',
|
|
1923
|
+
param: 'minute',
|
|
1924
|
+
type: ParamType.Int,
|
|
1925
|
+
startsHidden: true,
|
|
1926
|
+
},
|
|
1927
|
+
{
|
|
1928
|
+
title: 'Timestamp',
|
|
1929
|
+
param: 'timestamp',
|
|
1930
|
+
type: ParamType.Int,
|
|
1931
|
+
startsHidden: true,
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
title: 'Context',
|
|
1935
|
+
param: 'context',
|
|
1936
|
+
type: ParamType.String,
|
|
1937
|
+
},
|
|
1938
|
+
{
|
|
1939
|
+
title: 'Subcontext',
|
|
1940
|
+
param: 'subcontext',
|
|
1941
|
+
type: ParamType.String,
|
|
1942
|
+
},
|
|
1943
|
+
{
|
|
1944
|
+
title: 'Tags',
|
|
1945
|
+
param: 'tags',
|
|
1946
|
+
type: ParamType.JSON,
|
|
1947
|
+
startsHidden: true,
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
title: 'Log Level',
|
|
1951
|
+
param: 'level',
|
|
1952
|
+
type: ParamType.String,
|
|
1953
|
+
startsHidden: true,
|
|
1954
|
+
},
|
|
1955
|
+
{
|
|
1956
|
+
title: 'Metadata',
|
|
1957
|
+
param: 'metadata',
|
|
1958
|
+
type: ParamType.JSON,
|
|
1959
|
+
startsHidden: true,
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
title: 'Source',
|
|
1963
|
+
param: 'source',
|
|
1964
|
+
type: ParamType.String,
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
title: 'Server Route Path',
|
|
1968
|
+
param: 'routePath',
|
|
1969
|
+
type: ParamType.String,
|
|
1970
|
+
startsHidden: true,
|
|
1971
|
+
},
|
|
1972
|
+
{
|
|
1973
|
+
title: 'Server Route Template',
|
|
1974
|
+
param: 'routeTemplate',
|
|
1975
|
+
type: ParamType.String,
|
|
1976
|
+
startsHidden: true,
|
|
1977
|
+
},
|
|
1978
|
+
{
|
|
1979
|
+
title: 'Type',
|
|
1980
|
+
param: 'type',
|
|
1981
|
+
type: ParamType.String,
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
title: 'Error Message',
|
|
1985
|
+
param: 'errorMessage',
|
|
1986
|
+
type: ParamType.String,
|
|
1987
|
+
startsHidden: true,
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
title: 'Error Code',
|
|
1991
|
+
param: 'errorCode',
|
|
1992
|
+
type: ParamType.String,
|
|
1993
|
+
startsHidden: true,
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
title: 'Error Stack',
|
|
1997
|
+
param: 'errorStack',
|
|
1998
|
+
type: ParamType.String,
|
|
1999
|
+
startsHidden: true,
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
title: 'Action Target',
|
|
2003
|
+
param: 'target',
|
|
2004
|
+
type: ParamType.String,
|
|
2005
|
+
startsHidden: true,
|
|
2006
|
+
},
|
|
2007
|
+
{
|
|
2008
|
+
title: 'Action Type',
|
|
2009
|
+
param: 'action',
|
|
2010
|
+
type: ParamType.String,
|
|
2011
|
+
startsHidden: true,
|
|
2012
|
+
},
|
|
2013
|
+
];
|
|
2014
|
+
|
|
2015
|
+
// Create intelliTable
|
|
2016
|
+
const dataTable = (
|
|
2017
|
+
logs.length === 0
|
|
2018
|
+
? (
|
|
2019
|
+
<>
|
|
2020
|
+
<h3 className="m-0">
|
|
2021
|
+
Matching Logs:
|
|
2022
|
+
</h3>
|
|
2023
|
+
<div className="alert alert-warning text-center">
|
|
2024
|
+
<h4 className="m-1">
|
|
2025
|
+
No Logs to Show
|
|
2026
|
+
</h4>
|
|
2027
|
+
<div>
|
|
2028
|
+
Either your filters are too strict or no matching logs have been
|
|
2029
|
+
created yet.
|
|
2030
|
+
</div>
|
|
2031
|
+
</div>
|
|
2032
|
+
</>
|
|
2033
|
+
)
|
|
2034
|
+
: (
|
|
2035
|
+
<IntelliTable
|
|
2036
|
+
title="Matching Logs:"
|
|
2037
|
+
id="logs"
|
|
2038
|
+
data={logs}
|
|
2039
|
+
columns={columns}
|
|
2040
|
+
/>
|
|
2041
|
+
)
|
|
2042
|
+
);
|
|
2043
|
+
|
|
2044
|
+
// Main body
|
|
2045
|
+
body = (
|
|
2046
|
+
<>
|
|
2047
|
+
{filters}
|
|
2048
|
+
{dataTable}
|
|
2049
|
+
</>
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/* ---------- Wrap in Modal --------- */
|
|
2054
|
+
|
|
2055
|
+
return (
|
|
2056
|
+
<div className="LogReviewer-outer-container">
|
|
2057
|
+
{/* Style */}
|
|
2058
|
+
<style>{style}</style>
|
|
2059
|
+
|
|
2060
|
+
<div className="LogReviewer-inner-container">
|
|
2061
|
+
<div className="LogReviewer-header">
|
|
2062
|
+
<div className="LogReviewer-header-title">
|
|
2063
|
+
<h3 className="text-center m-0">
|
|
2064
|
+
Log Review Dashboard
|
|
2065
|
+
</h3>
|
|
2066
|
+
</div>
|
|
2067
|
+
<button
|
|
2068
|
+
type="button"
|
|
2069
|
+
className="LogReviewer-header-close-button btn btn-dark btn-lg"
|
|
2070
|
+
aria-label="close log reviewer panel"
|
|
2071
|
+
onClick={onClose}
|
|
2072
|
+
>
|
|
2073
|
+
<FontAwesomeIcon
|
|
2074
|
+
icon={faTimes}
|
|
2075
|
+
/>
|
|
2076
|
+
</button>
|
|
2077
|
+
</div>
|
|
2078
|
+
<div className="LogReviewer-contents">
|
|
2079
|
+
{body}
|
|
2080
|
+
</div>
|
|
2081
|
+
</div>
|
|
2082
|
+
</div>
|
|
2083
|
+
);
|
|
2084
|
+
};
|
|
2085
|
+
|
|
2086
|
+
/*------------------------------------------------------------------------*/
|
|
2087
|
+
/* Wrap Up */
|
|
2088
|
+
/*------------------------------------------------------------------------*/
|
|
2089
|
+
|
|
2090
|
+
// Export component
|
|
2091
|
+
export default LogReviewer;
|