@redsift/dashboard 8.0.0-alpha.8 → 8.0.1

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 (156) hide show
  1. package/.env +2 -0
  2. package/coverage/clover.xml +509 -0
  3. package/coverage/coverage-final.json +22 -0
  4. package/coverage/lcov-report/ConnectedDataGrid/ConnectedDataGrid.tsx.html +193 -0
  5. package/coverage/lcov-report/ConnectedDataGrid/index.html +131 -0
  6. package/coverage/lcov-report/ConnectedDataGrid/index.ts.html +85 -0
  7. package/coverage/lcov-report/Dashboard/Dashboard.tsx.html +229 -0
  8. package/coverage/lcov-report/Dashboard/context.ts.html +118 -0
  9. package/coverage/lcov-report/Dashboard/index.html +176 -0
  10. package/coverage/lcov-report/Dashboard/index.ts.html +97 -0
  11. package/coverage/lcov-report/Dashboard/reducer.ts.html +139 -0
  12. package/coverage/lcov-report/Dashboard/types.ts.html +190 -0
  13. package/coverage/lcov-report/HorizontalBarChart/HorizontalBarChart.tsx.html +430 -0
  14. package/coverage/lcov-report/HorizontalBarChart/index.html +161 -0
  15. package/coverage/lcov-report/HorizontalBarChart/index.ts.html +88 -0
  16. package/coverage/lcov-report/HorizontalBarChart/styles.ts.html +217 -0
  17. package/coverage/lcov-report/HorizontalBarChart/types.ts.html +184 -0
  18. package/coverage/lcov-report/PieChart/PieChart.tsx.html +736 -0
  19. package/coverage/lcov-report/PieChart/index.html +161 -0
  20. package/coverage/lcov-report/PieChart/index.ts.html +88 -0
  21. package/coverage/lcov-report/PieChart/styles.ts.html +244 -0
  22. package/coverage/lcov-report/PieChart/types.ts.html +184 -0
  23. package/coverage/lcov-report/base.css +224 -0
  24. package/coverage/lcov-report/block-navigation.js +87 -0
  25. package/coverage/lcov-report/components/ChartEmptyState/ChartEmptyState.tsx.html +679 -0
  26. package/coverage/lcov-report/components/ChartEmptyState/index.html +146 -0
  27. package/coverage/lcov-report/components/ChartEmptyState/index.ts.html +91 -0
  28. package/coverage/lcov-report/components/ChartEmptyState/styles.ts.html +184 -0
  29. package/coverage/lcov-report/components/ConnectedDataGrid/ConnectedDataGrid.tsx.html +181 -0
  30. package/coverage/lcov-report/components/ConnectedDataGrid/index.html +131 -0
  31. package/coverage/lcov-report/components/ConnectedDataGrid/index.ts.html +85 -0
  32. package/coverage/lcov-report/components/CrossfilterRegistry/CrossfilterRegistry.ts.html +163 -0
  33. package/coverage/lcov-report/components/CrossfilterRegistry/index.html +131 -0
  34. package/coverage/lcov-report/components/CrossfilterRegistry/index.ts.html +88 -0
  35. package/coverage/lcov-report/components/Dashboard/Dashboard.tsx.html +289 -0
  36. package/coverage/lcov-report/components/Dashboard/context.ts.html +115 -0
  37. package/coverage/lcov-report/components/Dashboard/index.html +176 -0
  38. package/coverage/lcov-report/components/Dashboard/index.ts.html +97 -0
  39. package/coverage/lcov-report/components/Dashboard/reducer.ts.html +382 -0
  40. package/coverage/lcov-report/components/Dashboard/types.ts.html +226 -0
  41. package/coverage/lcov-report/components/DataGrid/DataGrid.tsx.html +202 -0
  42. package/coverage/lcov-report/components/DataGrid/index.html +131 -0
  43. package/coverage/lcov-report/components/DataGrid/index.ts.html +91 -0
  44. package/coverage/lcov-report/components/EmptyChart/EmptyChart.tsx.html +244 -0
  45. package/coverage/lcov-report/components/EmptyChart/index.html +146 -0
  46. package/coverage/lcov-report/components/EmptyChart/index.ts.html +91 -0
  47. package/coverage/lcov-report/components/EmptyChart/styles.ts.html +241 -0
  48. package/coverage/lcov-report/components/HorizontalBarChart/HorizontalBarChart.tsx.html +1063 -0
  49. package/coverage/lcov-report/components/HorizontalBarChart/index.html +161 -0
  50. package/coverage/lcov-report/components/HorizontalBarChart/index.ts.html +91 -0
  51. package/coverage/lcov-report/components/HorizontalBarChart/styles.ts.html +385 -0
  52. package/coverage/lcov-report/components/HorizontalBarChart/types.ts.html +328 -0
  53. package/coverage/lcov-report/components/PDFExportButton/PdfDocument.tsx.html +688 -0
  54. package/coverage/lcov-report/components/PDFExportButton/PdfExportButton.tsx.html +583 -0
  55. package/coverage/lcov-report/components/PDFExportButton/index.html +161 -0
  56. package/coverage/lcov-report/components/PDFExportButton/index.ts.html +88 -0
  57. package/coverage/lcov-report/components/PDFExportButton/styles.ts.html +532 -0
  58. package/coverage/lcov-report/components/PDFExportButton/utils.ts.html +283 -0
  59. package/coverage/lcov-report/components/PieChart/PieChart.tsx.html +1363 -0
  60. package/coverage/lcov-report/components/PieChart/index.html +161 -0
  61. package/coverage/lcov-report/components/PieChart/index.ts.html +91 -0
  62. package/coverage/lcov-report/components/PieChart/styles.ts.html +388 -0
  63. package/coverage/lcov-report/components/PieChart/types.ts.html +325 -0
  64. package/coverage/lcov-report/components/ResetButton/ResetButton.tsx.html +160 -0
  65. package/coverage/lcov-report/components/ResetButton/index.html +131 -0
  66. package/coverage/lcov-report/components/ResetButton/index.ts.html +91 -0
  67. package/coverage/lcov-report/components/ScatterPlot/ScatterPlot.tsx.html +2881 -0
  68. package/coverage/lcov-report/components/ScatterPlot/index.html +176 -0
  69. package/coverage/lcov-report/components/ScatterPlot/index.ts.html +91 -0
  70. package/coverage/lcov-report/components/ScatterPlot/styles.ts.html +505 -0
  71. package/coverage/lcov-report/components/ScatterPlot/types.ts.html +370 -0
  72. package/coverage/lcov-report/components/ScatterPlot/utils.ts.html +136 -0
  73. package/coverage/lcov-report/components/StaticPieChart/StaticPieChart.tsx.html +286 -0
  74. package/coverage/lcov-report/components/StaticPieChart/index.html +131 -0
  75. package/coverage/lcov-report/components/StaticPieChart/index.ts.html +88 -0
  76. package/coverage/lcov-report/components/TimeSeriesBarChart/TimeSeriesBarChart.tsx.html +1744 -0
  77. package/coverage/lcov-report/components/TimeSeriesBarChart/index.html +161 -0
  78. package/coverage/lcov-report/components/TimeSeriesBarChart/index.ts.html +91 -0
  79. package/coverage/lcov-report/components/TimeSeriesBarChart/styles.ts.html +361 -0
  80. package/coverage/lcov-report/components/TimeSeriesBarChart/types.ts.html +319 -0
  81. package/coverage/lcov-report/components/WithFilters/FilterableBarChart.tsx.html +628 -0
  82. package/coverage/lcov-report/components/WithFilters/FilterableDataGrid.tsx.html +220 -0
  83. package/coverage/lcov-report/components/WithFilters/FilterablePieChart.tsx.html +622 -0
  84. package/coverage/lcov-report/components/WithFilters/FilterableScatterPlot.tsx.html +1090 -0
  85. package/coverage/lcov-report/components/WithFilters/WithFilters.tsx.html +172 -0
  86. package/coverage/lcov-report/components/WithFilters/index.html +191 -0
  87. package/coverage/lcov-report/components/WithFilters/index.ts.html +91 -0
  88. package/coverage/lcov-report/components/index.html +116 -0
  89. package/coverage/lcov-report/components/index.ts.html +97 -0
  90. package/coverage/lcov-report/favicon.png +0 -0
  91. package/coverage/lcov-report/hooks/index.html +116 -0
  92. package/coverage/lcov-report/hooks/useCategoricalChartAsListbox.ts.html +478 -0
  93. package/coverage/lcov-report/hooks/useChartAsListbox.ts.html +655 -0
  94. package/coverage/lcov-report/index.html +206 -0
  95. package/coverage/lcov-report/prettify.css +1 -0
  96. package/coverage/lcov-report/prettify.js +2 -0
  97. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  98. package/coverage/lcov-report/sorter.js +196 -0
  99. package/coverage/lcov-report/utils/groupReduceCount.ts.html +94 -0
  100. package/coverage/lcov-report/utils/groupReduceSum.ts.html +97 -0
  101. package/coverage/lcov-report/utils/groupReducers/groupReduceCount.ts.html +100 -0
  102. package/coverage/lcov-report/utils/groupReducers/groupReduceSum.ts.html +103 -0
  103. package/coverage/lcov-report/utils/groupReducers/index.html +146 -0
  104. package/coverage/lcov-report/utils/groupReducers/index.ts.html +91 -0
  105. package/coverage/lcov-report/utils/index.html +116 -0
  106. package/coverage/lcov-report/utils/index.ts.html +88 -0
  107. package/coverage/lcov.info +1070 -0
  108. package/{index.js → dist/index.js} +144 -20
  109. package/dist/index.js.map +1 -0
  110. package/dist/package.json +113 -0
  111. package/index.ts +1 -0
  112. package/jest.config.js +3 -0
  113. package/package.json +6 -12
  114. package/rollup.config.js +13 -0
  115. package/src/components/ChartEmptyState/ChartEmptyState.stories.tsx +23 -0
  116. package/src/components/ChartEmptyState/ChartEmptyState.tsx +198 -0
  117. package/src/components/ChartEmptyState/index.ts +2 -0
  118. package/src/components/ChartEmptyState/styles.ts +33 -0
  119. package/src/components/ChartEmptyState/types.ts +15 -0
  120. package/src/components/CrossfilterRegistry/CrossfilterRegistry.ts +26 -0
  121. package/src/components/CrossfilterRegistry/index.ts +1 -0
  122. package/src/components/Dashboard/Dashboard.stories.tsx +602 -0
  123. package/src/components/Dashboard/Dashboard.test.tsx +19 -0
  124. package/src/components/Dashboard/Dashboard.tsx +68 -0
  125. package/src/components/Dashboard/__snapshots__/Dashboard.stories.storyshot +24646 -0
  126. package/src/components/Dashboard/context.ts +10 -0
  127. package/src/components/Dashboard/index.ts +4 -0
  128. package/src/components/Dashboard/reducer.ts +99 -0
  129. package/src/components/Dashboard/types.ts +47 -0
  130. package/src/components/PdfExportButton/PdfDocument.tsx +203 -0
  131. package/src/components/PdfExportButton/PdfExportButton.tsx +168 -0
  132. package/src/components/PdfExportButton/index.ts +3 -0
  133. package/src/components/PdfExportButton/styles.ts +151 -0
  134. package/src/components/PdfExportButton/types.ts +59 -0
  135. package/src/components/TimeSeriesBarChart/TimeSeriesBarChart.tsx +565 -0
  136. package/src/components/TimeSeriesBarChart/index.ts +4 -0
  137. package/src/components/TimeSeriesBarChart/styles.ts +94 -0
  138. package/src/components/TimeSeriesBarChart/types.ts +82 -0
  139. package/src/components/WithFilters/FilterableBarChart.tsx +181 -0
  140. package/src/components/WithFilters/FilterableDataGrid.tsx +45 -0
  141. package/src/components/WithFilters/FilterablePieChart.tsx +179 -0
  142. package/src/components/WithFilters/FilterableScatterPlot.tsx +335 -0
  143. package/src/components/WithFilters/WithFilters.tsx +29 -0
  144. package/src/components/WithFilters/index.ts +2 -0
  145. package/src/components/WithFilters/types.ts +45 -0
  146. package/src/hooks/useCategoricalChartAsListbox.ts +131 -0
  147. package/src/index.ts +8 -0
  148. package/src/types.ts +39 -0
  149. package/src/utils/groupReducers/groupReduceCount.ts +5 -0
  150. package/src/utils/groupReducers/groupReduceSum.ts +6 -0
  151. package/src/utils/groupReducers/index.ts +2 -0
  152. package/src/utils/index.ts +1 -0
  153. package/tsconfig.json +3 -0
  154. package/index.js.map +0 -1
  155. /package/{CONTRIBUTING.md → dist/CONTRIBUTING.md} +0 -0
  156. /package/{index.d.ts → dist/index.d.ts} +0 -0
@@ -0,0 +1,602 @@
1
+ import React, { RefObject, useRef, useState } from 'react';
2
+ import { timeFormat as d3timeFormat } from 'd3';
3
+
4
+ import mock from '../../../../../.storybook/static/mocks/bakery.json';
5
+ import reportEventsMock from '../../../../../.storybook/static/mocks/oninbox-report-events.json';
6
+
7
+ import { Flexbox, RedsiftColorOninboxPrimary } from '@redsift/design-system';
8
+ import { TimeSeriesBarChart } from '../TimeSeriesBarChart';
9
+ import { Dashboard } from './Dashboard';
10
+ import { PdfExportButton } from '../PdfExportButton';
11
+ import {
12
+ GridColumns,
13
+ CONTAINS_ANY_OF,
14
+ IS_ANY_OF,
15
+ IS_BETWEEN,
16
+ ENDS_WITH_ANY_OF,
17
+ GridSelectionModel,
18
+ GridFilterModel,
19
+ DataGrid,
20
+ } from '@redsift/table';
21
+ import { COUNT, SUM } from '../../utils';
22
+ import { JSONArray } from '../../types';
23
+ import { WithFilters } from '../WithFilters';
24
+ import { NaturallyOrderedValue } from 'crossfilter2';
25
+ import { BarChart, BarDatum, PieChart, ScatterPlot } from '@redsift/charts';
26
+
27
+ export default {
28
+ title: 'Dashboard/Provider',
29
+ };
30
+
31
+ export const WithDataGrid = () => {
32
+ const data = mock.map((row, index) => ({ id: index, ...row }));
33
+
34
+ const columns = [
35
+ {
36
+ field: 'TransactionNumber',
37
+ headerName: 'Transaction Number',
38
+ filterable: false,
39
+ width: 250,
40
+ },
41
+ {
42
+ field: 'Items',
43
+ headerName: 'Items',
44
+ filterOperators: [IS_ANY_OF],
45
+ width: 250,
46
+ },
47
+ {
48
+ field: 'Daypart',
49
+ headerName: 'Daypart',
50
+ filterOperators: [IS_ANY_OF],
51
+ width: 250,
52
+ },
53
+ {
54
+ field: 'DayType',
55
+ headerName: 'DayType',
56
+ filterOperators: [IS_ANY_OF],
57
+ width: 250,
58
+ },
59
+ {
60
+ field: 'x',
61
+ headerName: 'x',
62
+ type: 'number',
63
+ filterOperators: [IS_BETWEEN],
64
+ },
65
+ {
66
+ field: 'y',
67
+ headerName: 'y',
68
+ type: 'number',
69
+ filterOperators: [IS_BETWEEN],
70
+ },
71
+ ];
72
+
73
+ return (
74
+ <Dashboard data={data}>
75
+ <Flexbox
76
+ flexDirection="row"
77
+ flexWrap="wrap"
78
+ justifyContent="space-evenly"
79
+ >
80
+ <Flexbox flexDirection="column" flexWrap="wrap">
81
+ <WithFilters
82
+ dimension={(d) => d.DayType}
83
+ group={COUNT}
84
+ datagridCategoryDimFilter={{
85
+ field: 'DayType',
86
+ operator: 'isAnyOf',
87
+ }}
88
+ >
89
+ <PieChart
90
+ title="DayType"
91
+ size="small"
92
+ variant="spacedDonut"
93
+ theme="dark"
94
+ />
95
+ </WithFilters>
96
+ <WithFilters
97
+ dimension={(d) => d.Daypart}
98
+ group={COUNT}
99
+ datagridCategoryDimFilter={{
100
+ field: 'Daypart',
101
+ operator: 'isAnyOf',
102
+ }}
103
+ >
104
+ <BarChart title="Daypart" size="small" theme="monochrome" />
105
+ </WithFilters>
106
+ </Flexbox>
107
+ <WithFilters
108
+ dimension={(d) => [+d.x, +d.y, d.Items]}
109
+ group={COUNT}
110
+ datagridCoordinatesCategoryDimFilter={[
111
+ {
112
+ field: 'x',
113
+ operator: 'isBetween',
114
+ },
115
+ {
116
+ field: 'y',
117
+ operator: 'isBetween',
118
+ },
119
+ {
120
+ field: 'Items',
121
+ operator: 'isAnyOf',
122
+ },
123
+ ]}
124
+ >
125
+ <ScatterPlot title="Items" variant="gridded" />
126
+ </WithFilters>
127
+ </Flexbox>
128
+ <WithFilters>
129
+ <DataGrid columns={columns} />
130
+ </WithFilters>
131
+ </Dashboard>
132
+ );
133
+ };
134
+
135
+ export const WithDifferentFilteringOperators = () => {
136
+ const data = [
137
+ {
138
+ id: 1,
139
+ sender: 'pierredupuis@redsift.io',
140
+ cost: 900,
141
+ reportedBy: ['Anne', 'Neil'],
142
+ },
143
+ {
144
+ id: 2,
145
+ sender: 'pierredupuis@redsift.io',
146
+ cost: 800,
147
+ reportedBy: ['Jose'],
148
+ },
149
+ {
150
+ id: 3,
151
+ sender: 'pierredupuis@gmail.com',
152
+ cost: 700,
153
+ reportedBy: ['Anne', 'Jose'],
154
+ },
155
+ {
156
+ id: 4,
157
+ sender: 'pierredupuis@hotmail.fr',
158
+ cost: 600,
159
+ reportedBy: ['Kitty'],
160
+ },
161
+ {
162
+ id: 5,
163
+ sender: 'anotherpierre@redsift.io',
164
+ cost: 500,
165
+ reportedBy: ['Miguel', 'Kitty'],
166
+ },
167
+ {
168
+ id: 6,
169
+ sender: 'pierredupuis@hotmail.fr',
170
+ cost: 400,
171
+ reportedBy: ['Anne', 'Neil'],
172
+ },
173
+ ];
174
+
175
+ const columns = [
176
+ {
177
+ field: 'sender',
178
+ headerName: 'Sender',
179
+ width: 250,
180
+ filterOperators: [ENDS_WITH_ANY_OF, IS_ANY_OF],
181
+ },
182
+ {
183
+ field: 'cost',
184
+ headerName: 'Cost',
185
+ width: 250,
186
+ filterable: false,
187
+ },
188
+ {
189
+ field: 'reportedBy',
190
+ headerName: 'Reported By',
191
+ width: 250,
192
+ filterOperators: [CONTAINS_ANY_OF],
193
+ },
194
+ ];
195
+
196
+ return (
197
+ <Dashboard data={data}>
198
+ <Flexbox flexDirection="row" justifyContent="space-evenly">
199
+ <WithFilters
200
+ dimension={(d) => d.sender.split('@')[1]}
201
+ group={COUNT}
202
+ datagridCategoryDimFilter={{
203
+ field: 'sender',
204
+ operator: 'endsWithAnyOf',
205
+ }}
206
+ >
207
+ <BarChart title="Sender domains" />
208
+ </WithFilters>
209
+ <WithFilters
210
+ dimension={(d) => d.sender}
211
+ group={SUM('cost')}
212
+ datagridCategoryDimFilter={{
213
+ field: 'sender',
214
+ operator: 'isAnyOf',
215
+ }}
216
+ >
217
+ <BarChart title="Cost per sender" />
218
+ </WithFilters>
219
+ <WithFilters
220
+ dimension={(d) => d.reportedBy}
221
+ group={COUNT}
222
+ datagridCategoryDimFilter={{
223
+ field: 'reportedBy',
224
+ operator: 'containsAnyOf',
225
+ }}
226
+ isDimensionArray
227
+ >
228
+ <BarChart title="Reporters" />
229
+ </WithFilters>
230
+ </Flexbox>
231
+ <DataGrid columns={columns} />
232
+ </Dashboard>
233
+ );
234
+ };
235
+
236
+ type ReportEvent = {
237
+ id: number;
238
+ sender: string;
239
+ subject: string;
240
+ userActions: string[];
241
+ userClassification: 'safe' | 'spam' | 'phishing';
242
+ userEmail: string;
243
+ userReasons: string[];
244
+ reportDateTime: string;
245
+ };
246
+
247
+ type AggregatedEmail = {
248
+ id: number;
249
+ sender: string;
250
+ subject: string;
251
+ userActions: string[];
252
+ userClassifications: Array<'safe' | 'spam' | 'phishing'>;
253
+ userEmails: string[];
254
+ userReasons: string[];
255
+ reportDateTimes: string[];
256
+ markedAs: string;
257
+ phishing: number;
258
+ spam: number;
259
+ other: number;
260
+ };
261
+
262
+ const reportEvents: ReportEvent[] = reportEventsMock.map(
263
+ (reportEvent, index) => {
264
+ return {
265
+ id: index,
266
+ sender: reportEvent.message.sender,
267
+ subject: reportEvent.message.subject,
268
+ userActions: reportEvent.userActions
269
+ .split(',')
270
+ .filter((element) => element),
271
+ userClassification:
272
+ reportEvent.userClassification === 'dangerous'
273
+ ? 'phishing'
274
+ : reportEvent.userClassification === 'spam'
275
+ ? 'spam'
276
+ : 'safe',
277
+ userEmail: reportEvent.userEmail,
278
+ userReasons: reportEvent.userReasons
279
+ .split(',')
280
+ .filter((element) => element),
281
+ reportDateTime: d3timeFormat('%Y-%m-%d %H:%M:%S')(
282
+ new Date(reportEvent.reportMetadata.dateTime)
283
+ ),
284
+ };
285
+ }
286
+ );
287
+
288
+ const aggregateReportsPerEmail = (reportEvents: ReportEvent[]) =>
289
+ reportEvents
290
+ .reduce((acc: AggregatedEmail[], curr: ReportEvent) => {
291
+ if (
292
+ !acc ||
293
+ acc.length === 0 ||
294
+ acc.findIndex(
295
+ (e) => e.sender === curr.sender && e.subject === curr.subject
296
+ ) === -1
297
+ ) {
298
+ acc.push({
299
+ id: acc.length,
300
+ sender: curr.sender,
301
+ subject: curr.subject,
302
+ userActions: [...curr.userActions],
303
+ userClassifications: [curr.userClassification],
304
+ userEmails: [curr.userEmail],
305
+ userReasons: [...curr.userReasons],
306
+ reportDateTimes: [curr.reportDateTime],
307
+ markedAs: '',
308
+ phishing: 0,
309
+ spam: 0,
310
+ other: 0,
311
+ });
312
+ } else {
313
+ const emailIndex = acc.findIndex(
314
+ (e) => e.sender === curr.sender && e.subject === curr.subject
315
+ );
316
+ acc[emailIndex] = {
317
+ ...acc[emailIndex],
318
+ userActions: [...acc[emailIndex].userActions, ...curr.userActions],
319
+ userClassifications: [
320
+ ...acc[emailIndex].userClassifications,
321
+ curr.userClassification,
322
+ ],
323
+ userEmails: [...acc[emailIndex].userEmails, curr.userEmail],
324
+ userReasons: [...acc[emailIndex].userReasons, ...curr.userReasons],
325
+ reportDateTimes: [
326
+ ...acc[emailIndex].reportDateTimes,
327
+ curr.reportDateTime,
328
+ ],
329
+ };
330
+ }
331
+ return acc;
332
+ }, [])
333
+ .map((reportEvent) => {
334
+ const computedReportEvent = reportEvent;
335
+
336
+ const phishing = reportEvent.userClassifications.filter(
337
+ (c: string) => c === 'phishing'
338
+ )?.length;
339
+ const spam = reportEvent.userClassifications.filter(
340
+ (c: string) => c === 'spam'
341
+ )?.length;
342
+ const other = reportEvent.userClassifications.length - phishing - spam;
343
+
344
+ const max = Math.max(Math.max(phishing, spam), other);
345
+
346
+ reportEvent.markedAs =
347
+ max === phishing ? 'Phishing' : max === spam ? 'Spam' : 'Safe / Other';
348
+ reportEvent.phishing = phishing;
349
+ reportEvent.spam = spam;
350
+ reportEvent.other = other;
351
+ return computedReportEvent;
352
+ });
353
+
354
+ const getDateRange = (timeFilters: Date[][] | undefined) => {
355
+ if (timeFilters && timeFilters[0] && timeFilters[0][0]) {
356
+ return `${d3timeFormat('%Y-%m-%d')(timeFilters[0][0])} - ${d3timeFormat(
357
+ '%Y-%m-%d'
358
+ )(timeFilters[0][1])}`;
359
+ }
360
+ };
361
+
362
+ export const OninboxRemediationPage = () => {
363
+ const componentRef = useRef<HTMLDivElement>();
364
+ const [reportEventsAggregatedPerEmail, setReportEventsAggregatedPerEmail] =
365
+ useState(aggregateReportsPerEmail(reportEvents));
366
+ const [selectionModel, setSelectionModel] = useState<GridSelectionModel>();
367
+ const pieFilters = useRef<NaturallyOrderedValue[]>([]);
368
+ const domainFilters = useRef<NaturallyOrderedValue[]>([]);
369
+ const actionFilters = useRef<NaturallyOrderedValue[]>([]);
370
+ const [timeFilters, setTimeFilters] = useState<Date[][]>([]);
371
+
372
+ const appliedFilters = [
373
+ ...pieFilters.current,
374
+ ...domainFilters.current,
375
+ ...actionFilters.current,
376
+ ].join(', ');
377
+
378
+ const dateRange = getDateRange(timeFilters);
379
+ let introduction = 'OnINBOX Report for Cameron\nReport Status: Resolved';
380
+ if (dateRange) {
381
+ introduction = `${introduction}\nDate Range: ${dateRange}`;
382
+ }
383
+ if (appliedFilters) {
384
+ introduction = `${introduction}\nApplied Filters: ${appliedFilters}`;
385
+ }
386
+
387
+ const columns: GridColumns = [
388
+ {
389
+ field: 'sender',
390
+ headerName: 'Sender',
391
+ width: 300,
392
+ filterOperators: [ENDS_WITH_ANY_OF],
393
+ },
394
+ {
395
+ field: 'subject',
396
+ headerName: 'Subject',
397
+ width: 300,
398
+ filterable: false,
399
+ },
400
+ {
401
+ field: 'markedAs',
402
+ headerName: 'Marked As',
403
+ width: 200,
404
+ filterOperators: [IS_ANY_OF],
405
+ },
406
+ {
407
+ field: 'phishing',
408
+ headerName: 'Phishing',
409
+ width: 100,
410
+ filterable: false,
411
+ },
412
+ {
413
+ field: 'spam',
414
+ headerName: 'Spam',
415
+ width: 100,
416
+ filterable: false,
417
+ },
418
+ {
419
+ field: 'other',
420
+ headerName: 'Safe / Other',
421
+ width: 100,
422
+ filterable: false,
423
+ },
424
+ {
425
+ field: 'userReasons',
426
+ headerName: 'User Activity',
427
+ width: 300,
428
+ filterOperators: [CONTAINS_ANY_OF],
429
+ },
430
+ ];
431
+
432
+ const debounce = (fn: Function, ms = 300) => {
433
+ let timeoutId: ReturnType<typeof setTimeout>;
434
+ return function (this: unknown, ...args: unknown[]) {
435
+ clearTimeout(timeoutId);
436
+ timeoutId = setTimeout(() => fn.apply(this, args), ms);
437
+ };
438
+ };
439
+
440
+ return (
441
+ <div
442
+ style={{
443
+ position: 'relative',
444
+ background: '#ffffff',
445
+ }}
446
+ ref={componentRef as RefObject<HTMLDivElement>}
447
+ >
448
+ <TimeSeriesBarChart
449
+ data={reportEvents}
450
+ dateTimeFieldName="reportDateTime"
451
+ dateTimeFormat="%Y-%m-%d %H:%M:%S"
452
+ dateTimeGroup="day"
453
+ dimension={(d) => d.reportDateTime as number}
454
+ onFilter={debounce((filters?: Date[][], allFiltered?: JSONArray) => {
455
+ if (filters) {
456
+ setTimeFilters(filters);
457
+ }
458
+ setReportEventsAggregatedPerEmail([
459
+ ...aggregateReportsPerEmail(allFiltered as ReportEvent[]),
460
+ ]);
461
+ })}
462
+ size="large"
463
+ stackedCategory="userClassification"
464
+ theme={{
465
+ success: 'safe',
466
+ warning: 'spam',
467
+ danger: 'phishing',
468
+ }}
469
+ title="Report Overview"
470
+ xAxisLabel="ReportedDate"
471
+ yAxisLabel="Reports"
472
+ />
473
+
474
+ <Dashboard data={reportEventsAggregatedPerEmail}>
475
+ <div
476
+ style={{
477
+ position: 'absolute',
478
+ top: '-10px',
479
+ right: '-10px',
480
+ }}
481
+ >
482
+ <PdfExportButton
483
+ introduction={introduction}
484
+ variant="secondary"
485
+ logo="images/oninbox.png"
486
+ primaryColor={RedsiftColorOninboxPrimary}
487
+ componentRef={componentRef as RefObject<HTMLDivElement>}
488
+ >
489
+ Download full report
490
+ </PdfExportButton>
491
+ </div>
492
+ <Flexbox flexDirection="row" justifyContent="space-evenly">
493
+ <WithFilters
494
+ dimension={(d) => d.markedAs}
495
+ group={COUNT}
496
+ datagridCategoryDimFilter={{
497
+ field: 'markedAs',
498
+ operator: 'isAnyOf',
499
+ }}
500
+ onFilter={(filters) => {
501
+ if (filters) {
502
+ pieFilters.current = filters;
503
+ }
504
+ }}
505
+ >
506
+ <PieChart
507
+ title="Emails Reported As"
508
+ theme={{
509
+ success: 'Safe / Other',
510
+ warning: 'Spam',
511
+ danger: 'Phishing',
512
+ }}
513
+ variant="spaced"
514
+ />
515
+ </WithFilters>
516
+ <WithFilters
517
+ dimension={(d: AggregatedEmail) => d.sender.split('@')?.[1]}
518
+ group={COUNT}
519
+ datagridCategoryDimFilter={{
520
+ field: 'sender',
521
+ operator: 'endsWithAnyOf',
522
+ }}
523
+ onFilter={(filters) => {
524
+ if (filters) {
525
+ domainFilters.current = filters;
526
+ }
527
+ }}
528
+ >
529
+ <BarChart
530
+ theme="monochrome"
531
+ title="Top Reported Domains"
532
+ caping={3}
533
+ others={false}
534
+ />
535
+ </WithFilters>
536
+ <WithFilters
537
+ dimension={(d: AggregatedEmail) => d.userReasons}
538
+ group={COUNT}
539
+ datagridCategoryDimFilter={{
540
+ field: 'userReasons',
541
+ operator: 'containsAnyOf',
542
+ }}
543
+ isDimensionArray
544
+ onFilter={(filters) => {
545
+ if (filters) {
546
+ actionFilters.current = filters;
547
+ }
548
+ }}
549
+ >
550
+ <BarChart
551
+ title="User Activity"
552
+ theme="monochrome"
553
+ caping={5}
554
+ others={false}
555
+ labelDecorator={(datum: BarDatum) =>
556
+ ({
557
+ 'clicked-links':
558
+ 'I clicked one or multiple links within the email',
559
+ 'opened-attachments': 'I opened one or multiple attachments',
560
+ 'responded-email': 'I responded to this email',
561
+ 'prior-emails':
562
+ 'I have received emails from this sender before',
563
+ 'trusted-sender': 'I trust this sender',
564
+ 'trusted-content':
565
+ 'The contents of this email appear to be trustworthy',
566
+ 'unsolicited-emails':
567
+ 'I have never requested to receive emails from this domain',
568
+ sender: 'I no longer wish to receive emails from this sender',
569
+ 'company-or-domain':
570
+ 'I no longer wish to receive emails from this domain',
571
+ 'internal-impersonation':
572
+ 'The sender is impersonating somebody internally',
573
+ 'external-impersonation':
574
+ 'The sender is impersonating somebody externally',
575
+ 'suspicious-attachments':
576
+ 'The email contains suspicious attachments',
577
+ 'malicious-links':
578
+ 'The email is requesting I open on potentially malicious links',
579
+ 'personal-info':
580
+ 'The email is asking for personal information such as bank details, password, or other personal information',
581
+ money:
582
+ 'The email is asking to send money or crypto, pay an invoice or requesting gift card',
583
+ }[datum.data.key]!)
584
+ }
585
+ />
586
+ </WithFilters>
587
+ </Flexbox>
588
+ <WithFilters
589
+ onFilter={(filterModel: GridFilterModel) => console.log(filterModel)}
590
+ >
591
+ <DataGrid
592
+ columns={columns}
593
+ checkboxSelection
594
+ onSelectionModelChange={setSelectionModel}
595
+ selectionModel={selectionModel}
596
+ />
597
+ </WithFilters>
598
+ {JSON.stringify(selectionModel)}
599
+ </Dashboard>
600
+ </div>
601
+ );
602
+ };
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+
4
+ import { Dashboard } from '.';
5
+
6
+ describe('Dashboard', () => {
7
+ it('supports custom className', () => {
8
+ const tree = render(<Dashboard className="test-class" data={[]} />);
9
+ const component = tree.asFragment().firstChild;
10
+ expect(component).toHaveAttribute(
11
+ 'class',
12
+ expect.stringContaining(Dashboard.className!)
13
+ );
14
+ expect(component).toHaveAttribute(
15
+ 'class',
16
+ expect.stringContaining('test-class')
17
+ );
18
+ });
19
+ });
@@ -0,0 +1,68 @@
1
+ import React, {
2
+ forwardRef,
3
+ RefObject,
4
+ useMemo,
5
+ useReducer,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import classNames from 'classnames';
10
+ import { useGridApiRef } from '@mui/x-data-grid-pro';
11
+
12
+ import { Comp } from '@redsift/design-system';
13
+ import { DashboardContext } from './context';
14
+ import { DashboardContextProps, DashboardProps } from './types';
15
+ import { DashboardReducer } from './reducer';
16
+ import { CrossfilterRegistry } from '../CrossfilterRegistry';
17
+
18
+ const COMPONENT_NAME = 'Dashboard';
19
+ const CLASSNAME = 'redsift-dashboard-container';
20
+ const DEFAULT_PROPS: Partial<DashboardProps> = {};
21
+
22
+ export const Dashboard: Comp<DashboardProps, HTMLDivElement> = forwardRef(
23
+ (props, ref) => {
24
+ const {
25
+ children,
26
+ className,
27
+ data,
28
+ dataGridApiRef: propsDataGridApiRef,
29
+ ...forwardedProps
30
+ } = props;
31
+
32
+ const providerRef = ref || useRef<HTMLDivElement>();
33
+ const dataGridApiRef = propsDataGridApiRef || useGridApiRef();
34
+ const [updateContext, setUpdateContext] = useState(false);
35
+
36
+ const [state, dispatch] = useReducer(DashboardReducer, {
37
+ tableFilters: [],
38
+ });
39
+
40
+ const value = useMemo<DashboardContextProps>(
41
+ () => ({
42
+ dashboardRef: providerRef as RefObject<HTMLDivElement>,
43
+ data,
44
+ dataGridApiRef,
45
+ dispatch,
46
+ ndx: CrossfilterRegistry.get(data),
47
+ state,
48
+ toggleUpdateContext: () => setUpdateContext(!updateContext),
49
+ }),
50
+ [data, state, updateContext]
51
+ );
52
+
53
+ return (
54
+ <div
55
+ {...forwardedProps}
56
+ className={classNames(Dashboard.className, className)}
57
+ ref={providerRef as RefObject<HTMLDivElement>}
58
+ >
59
+ <DashboardContext.Provider value={value as DashboardContextProps}>
60
+ {children}
61
+ </DashboardContext.Provider>
62
+ </div>
63
+ );
64
+ }
65
+ );
66
+ Dashboard.className = CLASSNAME;
67
+ Dashboard.defaultProps = DEFAULT_PROPS;
68
+ Dashboard.displayName = COMPONENT_NAME;