dce-reactkit 3.2.2-beta.1 → 3.2.2-beta.3

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.
Files changed (52) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/dist/cjs/index.js +1966 -259
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/types/components/CSVDownloadButton.d.ts +19 -0
  5. package/dist/cjs/types/components/IntelliTable.d.ts +17 -0
  6. package/dist/cjs/types/components/LogReviewer.d.ts +13 -0
  7. package/dist/cjs/types/components/SimpleDateChooser.d.ts +1 -0
  8. package/dist/cjs/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
  9. package/dist/cjs/types/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
  10. package/dist/cjs/types/helpers/canReviewLogs.d.ts +7 -0
  11. package/dist/cjs/types/helpers/genCSV.d.ts +14 -0
  12. package/dist/cjs/types/helpers/getMonthName.d.ts +14 -0
  13. package/dist/cjs/types/index.d.ts +9 -1
  14. package/dist/cjs/types/server/initServer.d.ts +8 -0
  15. package/dist/cjs/types/types/IntelliTableColumn.d.ts +12 -0
  16. package/dist/cjs/types/types/LogMetadataType.d.ts +19 -0
  17. package/dist/cjs/types/types/ReactKitErrorCode.d.ts +2 -1
  18. package/dist/esm/index.js +1962 -261
  19. package/dist/esm/index.js.map +1 -1
  20. package/dist/esm/types/components/CSVDownloadButton.d.ts +19 -0
  21. package/dist/esm/types/components/IntelliTable.d.ts +17 -0
  22. package/dist/esm/types/components/LogReviewer.d.ts +13 -0
  23. package/dist/esm/types/components/SimpleDateChooser.d.ts +1 -0
  24. package/dist/esm/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
  25. package/dist/esm/types/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
  26. package/dist/esm/types/helpers/canReviewLogs.d.ts +7 -0
  27. package/dist/esm/types/helpers/genCSV.d.ts +14 -0
  28. package/dist/esm/types/helpers/getMonthName.d.ts +14 -0
  29. package/dist/esm/types/index.d.ts +9 -1
  30. package/dist/esm/types/server/initServer.d.ts +8 -0
  31. package/dist/esm/types/types/IntelliTableColumn.d.ts +12 -0
  32. package/dist/esm/types/types/LogMetadataType.d.ts +19 -0
  33. package/dist/esm/types/types/ReactKitErrorCode.d.ts +2 -1
  34. package/dist/index.d.ts +169 -47
  35. package/package.json +1 -1
  36. package/sandbox.js +78 -17
  37. package/src/components/AppWrapper.tsx +5 -1
  38. package/src/components/CSVDownloadButton.tsx +102 -0
  39. package/src/components/IntelliTable.tsx +610 -0
  40. package/src/components/LogReviewer.tsx +2038 -0
  41. package/src/components/SimpleDateChooser.tsx +26 -27
  42. package/src/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.ts +9 -0
  43. package/src/constants/LOG_REVIEW_STATUS_ROUTE.ts +10 -0
  44. package/src/helpers/canReviewLogs.ts +33 -0
  45. package/src/helpers/genCSV.ts +59 -0
  46. package/src/helpers/getHumanReadableDate.ts +6 -17
  47. package/src/helpers/getMonthName.ts +29 -0
  48. package/src/index.ts +16 -0
  49. package/src/server/initServer.ts +105 -3
  50. package/src/types/IntelliTableColumn.ts +24 -0
  51. package/src/types/LogMetadataType.ts +23 -0
  52. package/src/types/ReactKitErrorCode.tsx +2 -1
@@ -5,10 +5,12 @@
5
5
 
6
6
  // Import React
7
7
  import React from 'react';
8
+ import getMonthName from '../helpers/getMonthName';
8
9
 
9
10
  // Import helpers
10
11
  import getOrdinal from '../helpers/getOrdinal';
11
12
  import getTimeInfoInET from '../helpers/getTimeInfoInET';
13
+ import forceNumIntoBounds from '../helpers/forceNumIntoBounds';
12
14
 
13
15
  /*------------------------------------------------------------------------*/
14
16
  /* Types */
@@ -32,31 +34,14 @@ type Props = {
32
34
  * @param year new full year number
33
35
  */
34
36
  onChange: (month: number, day: number, year: number) => void,
35
- // Number of months in the future to allow the user to schedule into
36
- // (max is 12)
37
+ // Number of months to allow the user to choose from
38
+ // (max is 12, default is 6)
37
39
  numMonthsToShow?: number,
40
+ // If true, instead of showing numMonthsToShow months into the future,
41
+ // show numMonthsToShow months into the past
42
+ chooseFromPast?: boolean,
38
43
  };
39
44
 
40
- /*------------------------------------------------------------------------*/
41
- /* Constants */
42
- /*------------------------------------------------------------------------*/
43
-
44
- // Constants
45
- const MONTH_NAMES = [
46
- 'January',
47
- 'February',
48
- 'March',
49
- 'April',
50
- 'May',
51
- 'June',
52
- 'July',
53
- 'August',
54
- 'September',
55
- 'October',
56
- 'November',
57
- 'December',
58
- ];
59
-
60
45
  /*------------------------------------------------------------------------*/
61
46
  /* Component */
62
47
  /*------------------------------------------------------------------------*/
@@ -75,8 +60,13 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
75
60
  day,
76
61
  year,
77
62
  onChange,
63
+ chooseFromPast,
78
64
  } = props;
79
- const numMonthsToShow = Math.min(props.numMonthsToShow ?? 6, 12);
65
+ let numMonthsToShow = forceNumIntoBounds(
66
+ (props.numMonthsToShow ?? 6),
67
+ 1,
68
+ 12,
69
+ );
80
70
 
81
71
  /*------------------------------------------------------------------------*/
82
72
  /* Render */
@@ -94,19 +84,28 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
94
84
  days: number[],
95
85
  year: number,
96
86
  }[] = [];
87
+ let startYear = today.year;
88
+ let startMonth = today.month;
89
+ if (chooseFromPast) {
90
+ startMonth -= Math.max(0, numMonthsToShow - 1);
91
+ if (startMonth <= 0) {
92
+ startMonth += 12;
93
+ startYear -= 1;
94
+ }
95
+ }
97
96
  for (let i = 0; i < numMonthsToShow; i++) {
98
97
  // Get month and year info
99
- const unmoddedMonth = (today.month + i);
98
+ const unmoddedMonth = (startMonth + i);
100
99
  const month = (
101
100
  unmoddedMonth > 12
102
101
  ? unmoddedMonth - 12
103
102
  : unmoddedMonth
104
103
  );
105
- const monthName = MONTH_NAMES[month - 1];
104
+ const monthName = getMonthName(month).full;
106
105
  const year = (
107
106
  unmoddedMonth > 12
108
- ? today.year + 1
109
- : today.year
107
+ ? startYear + 1
108
+ : startYear
110
109
  );
111
110
 
112
111
  // Figure out which days are allowed
@@ -0,0 +1,9 @@
1
+ import ROUTE_PATH_PREFIX from './ROUTE_PATH_PREFIX';
2
+
3
+ /**
4
+ * Path of the route for storing client-side logs
5
+ * @author Gabe Abrams
6
+ */
7
+ const LOG_REVIEW_ROUTE_PATH_PREFIX = `/admin${ROUTE_PATH_PREFIX}/logs`;
8
+
9
+ export default LOG_REVIEW_ROUTE_PATH_PREFIX;
@@ -0,0 +1,10 @@
1
+ import ROUTE_PATH_PREFIX from './ROUTE_PATH_PREFIX';
2
+
3
+ /**
4
+ * Route for checking the status of the current user's
5
+ * access to log review
6
+ * @author Gabe Abrams
7
+ */
8
+ const LOG_REVIEW_STATUS_ROUTE = `/admin${ROUTE_PATH_PREFIX}/logs/access`;
9
+
10
+ export default LOG_REVIEW_STATUS_ROUTE;
@@ -0,0 +1,33 @@
1
+ // Import shared helpers
2
+ import visitServerEndpoint from './visitServerEndpoint';
3
+
4
+ // Import shared constants
5
+ import LOG_REVIEW_STATUS_ROUTE from '../constants/LOG_REVIEW_STATUS_ROUTE';
6
+
7
+ // Cache user's ability
8
+ let canReview: boolean | undefined = undefined;
9
+
10
+ /**
11
+ * Check if the current user can review logs
12
+ * @author Gabe Abrams
13
+ * @returns true if current user can review logs
14
+ */
15
+ const canReviewLogs = async (): Promise<boolean> => {
16
+ // If cached, use that value
17
+ if (canReview !== undefined) {
18
+ return canReview;
19
+ }
20
+
21
+ // Ask on server
22
+ try {
23
+ canReview = !!(await visitServerEndpoint({
24
+ path: LOG_REVIEW_STATUS_ROUTE,
25
+ method: 'GET',
26
+ }));
27
+ } catch (err) {
28
+ canReview = false;
29
+ }
30
+ return canReview;
31
+ };
32
+
33
+ export default canReviewLogs;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Escape a CSV cell if needed
3
+ * @author Gabe Abrams
4
+ * @param text the cell contents
5
+ * @returns escaped cell text
6
+ */
7
+ const escapeCellText = (text: string): string => {
8
+ if (!text.includes(',')) {
9
+ // No need to escape
10
+ return text;
11
+ }
12
+
13
+ // Perform escape
14
+ return `"${text.replace(/"/g, '""')}`;
15
+ };
16
+
17
+ /**
18
+ * Generate a CSV file
19
+ * @author Gabe Abrams
20
+ * @param data list of row data in the form of json objects
21
+ * @param columns list of columns to include in the csv
22
+ * @returns multiline csv string
23
+ */
24
+ const genCSV = (
25
+ data: {
26
+ [k: string]: any
27
+ }[],
28
+ columns: {
29
+ title: string,
30
+ param: string,
31
+ }[],
32
+ ): string => {
33
+ let csv = '';
34
+
35
+ // Add header
36
+ csv += (
37
+ columns
38
+ .map((column) => {
39
+ return escapeCellText(column.title);
40
+ })
41
+ .join(',')
42
+ );
43
+
44
+ // Add each row
45
+ data.forEach((datum) => {
46
+ csv += (
47
+ columns
48
+ .map((column) => {
49
+ return escapeCellText(datum[column.param]);
50
+ })
51
+ .join(',')
52
+ );
53
+ });
54
+
55
+ // Return
56
+ return csv;
57
+ };
58
+
59
+ export default genCSV;
@@ -1,21 +1,7 @@
1
+ // Import shared helpers
1
2
  import getTimeInfoInET from './getTimeInfoInET';
2
3
  import getOrdinal from './getOrdinal';
3
-
4
- // Map of month to three letter description
5
- const monthMap = {
6
- 1: 'Jan',
7
- 2: 'Feb',
8
- 3: 'Mar',
9
- 4: 'Apr',
10
- 5: 'May',
11
- 6: 'Jun',
12
- 7: 'Jul',
13
- 8: 'Aug',
14
- 9: 'Sep',
15
- 10: 'Oct',
16
- 11: 'Nov',
17
- 12: 'Dec',
18
- };
4
+ import getMonthName from './getMonthName';
19
5
 
20
6
  /**
21
7
  * Get a human-readable description of a date (all in ET)
@@ -32,8 +18,11 @@ const getHumanReadableDate = (dateOrTimestamp?: Date | number) => {
32
18
  } = getTimeInfoInET(dateOrTimestamp);
33
19
  const currYear = getTimeInfoInET().year;
34
20
 
21
+ // Get the short month description
22
+ const monthName = getMonthName(month).short;
23
+
35
24
  // Create start of description
36
- let description = `${monthMap[month as keyof typeof monthMap]} ${day}${getOrdinal(day)}`;
25
+ let description = `${monthName} ${day}${getOrdinal(day)}`;
37
26
 
38
27
  // Add on year if it's different
39
28
  if (year !== currYear) {
@@ -0,0 +1,29 @@
1
+ const monthNames = [
2
+ { short: 'Jan', full: 'January' },
3
+ { short: 'Feb', full: 'February' },
4
+ { short: 'Mar', full: 'March' },
5
+ { short: 'Apr', full: 'April' },
6
+ { short: 'May', full: 'May' },
7
+ { short: 'Jun', full: 'June' },
8
+ { short: 'Jul', full: 'July' },
9
+ { short: 'Aug', full: 'August' },
10
+ { short: 'Sep', full: 'September' },
11
+ { short: 'Oct', full: 'October' },
12
+ { short: 'Nov', full: 'November' },
13
+ { short: 'Dec', full: 'December' },
14
+ ];
15
+
16
+ /**
17
+ * Get the name of a month given the month number (1 = January, etc.)
18
+ * If an invalid number is provided, we will treat it like January
19
+ * @author Gabe Abrams
20
+ * @param month the number of the month
21
+ * @returns object containing multiple month name formats:
22
+ * { short, full } where short will look like "Jan" and full will look like
23
+ * "January"
24
+ */
25
+ const getMonthName = (month: number) => {
26
+ return (monthNames[month - 1] ?? monthNames[0]);
27
+ };
28
+
29
+ export default getMonthName;
package/src/index.ts CHANGED
@@ -14,6 +14,9 @@ import PopFailureMark from './components/PopFailureMark';
14
14
  import PopPendingMark from './components/PopPendingMark';
15
15
  import CopiableBox from './components/CopiableBox';
16
16
  import ItemPicker from './components/ItemPicker';
17
+ import LogReviewer from './components/LogReviewer';
18
+ import IntelliTable from './components/IntelliTable';
19
+ import CSVDownloadButton from './components/CSVDownloadButton';
17
20
 
18
21
  // Import errors
19
22
  import ErrorWithCode from './errors/ErrorWithCode';
@@ -50,6 +53,9 @@ import onlyKeepLetters from './helpers/onlyKeepLetters';
50
53
  import parallelLimit from './helpers/parallelLimit';
51
54
  import logClientEvent from './helpers/logClientEvent';
52
55
  import initLogCollection from './server/initLogCollection';
56
+ import getMonthName from './helpers/getMonthName';
57
+ import genCSV from './helpers/genCSV';
58
+ import canReviewLogs from './helpers/canReviewLogs';
53
59
 
54
60
  // Import types
55
61
  import ModalButtonType from './types/ModalButtonType';
@@ -64,6 +70,8 @@ import LogType from './types/LogType';
64
70
  import LogSource from './types/LogSource';
65
71
  import LogAction from './types/LogAction';
66
72
  import LogBuiltInMetadata from './types/LogBuiltInMetadata';
73
+ import LogMetadataType from './types/LogMetadataType';
74
+ import IntelliTableColumn from './types/IntelliTableColumn';
67
75
 
68
76
  // Component-specific-types
69
77
  import PickableItem from './components/ItemPicker/types/PickableItem';
@@ -86,6 +94,9 @@ export {
86
94
  PopPendingMark,
87
95
  CopiableBox,
88
96
  ItemPicker,
97
+ LogReviewer,
98
+ IntelliTable,
99
+ CSVDownloadButton,
89
100
  // Global functions
90
101
  alert,
91
102
  confirm,
@@ -116,6 +127,9 @@ export {
116
127
  stringsToHumanReadableList,
117
128
  onlyKeepLetters,
118
129
  parallelLimit,
130
+ getMonthName,
131
+ genCSV,
132
+ canReviewLogs,
119
133
  // Client helpers
120
134
  visitServerEndpoint,
121
135
  logClientEvent,
@@ -137,6 +151,8 @@ export {
137
151
  LogSource,
138
152
  LogAction,
139
153
  LogBuiltInMetadata,
154
+ LogMetadataType,
155
+ IntelliTableColumn,
140
156
  // Component-specific-types
141
157
  PickableItem,
142
158
  // Server types
@@ -1,13 +1,18 @@
1
1
  // Import custom error
2
- import LOG_ROUTE_PATH from '../constants/LOG_ROUTE_PATH';
3
2
  import ErrorWithCode from '../errors/ErrorWithCode';
4
3
 
4
+ // Import shared constants
5
+ import LOG_REVIEW_ROUTE_PATH_PREFIX from '../constants/LOG_REVIEW_ROUTE_PATH_PREFIX';
6
+ import LOG_ROUTE_PATH from '../constants/LOG_ROUTE_PATH';
7
+ import LOG_REVIEW_STATUS_ROUTE from '../constants/LOG_REVIEW_STATUS_ROUTE';
8
+
5
9
  // Import shared helpers
6
10
  import genRouteHandler from '../helpers/genRouteHandler';
7
- import LogFunction from '../types/LogFunction';
8
- import ParamType from '../types/ParamType';
9
11
 
10
12
  // Import shared types
13
+ import Log from '../types/Log';
14
+ import LogFunction from '../types/LogFunction';
15
+ import ParamType from '../types/ParamType';
11
16
  import ReactKitErrorCode from '../types/ReactKitErrorCode';
12
17
 
13
18
  // Types
@@ -66,17 +71,29 @@ export const internalGetLogCollection = () => {
66
71
  * @param opts.getLaunchInfo CACCL LTI's get launch info function
67
72
  * @param [opts.logCollection] mongo collection from dce-mango to use for
68
73
  * storing logs. If none is included, logs are written to the console
74
+ * @param [opts.logReviewAdmins=all admins] info on which admins can review
75
+ * logs from the client. If not included, all Canvas admins are allowed to
76
+ * review logs. If null, no Canvas admins are allowed to review logs.
77
+ * If an array of Canvas userIds (numbers), only Canvas admins with those
78
+ * userIds are allowed to review logs. If a dce-mango collection, only
79
+ * Canvas admins with entries in that collection ({ userId, ...}) are allowed
80
+ * to review logs
69
81
  */
70
82
  const initServer = (
71
83
  opts: {
72
84
  app: any,
73
85
  getLaunchInfo: GetLaunchInfoFunction,
74
86
  logCollection?: any,
87
+ logReviewAdmins?: (number[] | any),
75
88
  },
76
89
  ) => {
77
90
  _cacclGetLaunchInfo = opts.getLaunchInfo;
78
91
  _logCollection = opts.logCollection;
79
92
 
93
+ /*----------------------------------------*/
94
+ /* Logging */
95
+ /*----------------------------------------*/
96
+
80
97
  /**
81
98
  * Log an event
82
99
  * @author Gabe Abrams
@@ -154,6 +171,91 @@ const initServer = (
154
171
  },
155
172
  }),
156
173
  );
174
+
175
+ /*----------------------------------------*/
176
+ /* Log Reviewer */
177
+ /*----------------------------------------*/
178
+
179
+ if (opts.logReviewAdmins !== null) {
180
+ /**
181
+ * Check if a given user is allowed to review logs
182
+ * @author Gabe Abrams
183
+ * @param userId the id of the user
184
+ * @returns true if the user can review logs
185
+ */
186
+ const canReviewLogs = async (userId: number): Promise<boolean> => {
187
+ try {
188
+ // Array of userIds
189
+ if (Array.isArray(opts.logReviewAdmins)) {
190
+ return opts.logReviewAdmins.some((allowedId) => {
191
+ return (userId === allowedId);
192
+ });
193
+ }
194
+
195
+ // Must be a collection
196
+ const matches = await opts.logReviewAdmins.find({ userId });
197
+
198
+ // Make sure at least one entry matches
199
+ return matches.length > 0;
200
+ } catch (err) {
201
+ // If an error occurred, simply return false
202
+ return false;
203
+ }
204
+ };
205
+
206
+ /**
207
+ * Check if the current user has access to logs
208
+ * @author Gabe Abrams
209
+ * @returns {boolean} true if user has access
210
+ */
211
+ opts.app.get(
212
+ LOG_REVIEW_STATUS_ROUTE,
213
+ genRouteHandler({
214
+ handler: async ({ params }) => {
215
+ const { userId } = params;
216
+ const canReview = await canReviewLogs(userId);
217
+ return canReview;
218
+ },
219
+ }),
220
+ );
221
+
222
+ /**
223
+ * Get all logs for a certain month
224
+ * @author Gabe Abrams
225
+ * @param {number} year the year to query (e.g. 2022)
226
+ * @param {number} month the month to query (e.g. 1 = January)
227
+ * @returns {Log[]} list of logs from the given month
228
+ */
229
+ opts.app.post(
230
+ `${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/:year/months/:month`,
231
+ genRouteHandler({
232
+ paramTypes: {
233
+ year: ParamType.Int,
234
+ month: ParamType.Int,
235
+ },
236
+ handler: async ({ params }) => {
237
+ // Get user info
238
+ const { userId } = params;
239
+
240
+ // Validate user
241
+ // isAdmin is already checked because path starts with '/admin'
242
+ const canReview = await canReviewLogs(userId);
243
+ if (!canReview) {
244
+ throw new ErrorWithCode(
245
+ 'You cannot access this resource because you do not have the appropriate permissions.',
246
+ ReactKitErrorCode.NotAllowedToReviewLogs,
247
+ );
248
+ }
249
+
250
+ // Query for logs
251
+ const logs: Log[] = await _logCollection.find({ userId });
252
+
253
+ // Return logs
254
+ return logs;
255
+ },
256
+ }),
257
+ );
258
+ }
157
259
  };
158
260
 
159
261
  export default initServer;
@@ -0,0 +1,24 @@
1
+ import ParamType from './ParamType';
2
+
3
+ /**
4
+ * Column description for a column in the IntelliTable
5
+ * @author Gabe Abrams
6
+ */
7
+ type IntelliTableColumn = {
8
+ // Human-readable name of the column
9
+ title: string,
10
+ // Parameter name (property inside the data object)
11
+ param: string,
12
+ // Type of value
13
+ type: (
14
+ | ParamType.Boolean
15
+ | ParamType.Float
16
+ | ParamType.Int
17
+ | ParamType.String
18
+ | ParamType.JSON
19
+ ),
20
+ // If true, column starts hidden
21
+ startsHidden?: boolean,
22
+ };
23
+
24
+ export default IntelliTableColumn;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Type of a LogMetadata file
3
+ * @author Gabe Abrams
4
+ */
5
+ type LogMetadataType = {
6
+ Context?: {
7
+ [k: string]: (
8
+ | string
9
+ | {
10
+ _: string,
11
+ [k: string]: string
12
+ }
13
+ )
14
+ },
15
+ Tag?: {
16
+ [k: string]: string
17
+ },
18
+ Target?: {
19
+ [k: string]: string
20
+ },
21
+ };
22
+
23
+ export default LogMetadataType;
@@ -1,4 +1,4 @@
1
- // Highest error code = DRK10
1
+ // Highest error code = DRK11
2
2
 
3
3
  /**
4
4
  * List of error codes built into the react kit
@@ -15,6 +15,7 @@ enum ReactKitErrorCode {
15
15
  NoCACCLGetLaunchInfoFunction = 'DRK8',
16
16
  NotTTM = 'DRK9',
17
17
  NotAdmin = 'DRK10',
18
+ NotAllowedToReviewLogs = 'DRK11',
18
19
  }
19
20
 
20
21
  export default ReactKitErrorCode;