@cdc/dashboard 4.25.7 → 4.25.8
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/dist/cdcdashboard.js +35354 -35046
- package/examples/no-data-markup.json +206 -0
- package/examples/private/DEV-11072.json +7591 -0
- package/examples/private/brfs.json +2174 -0
- package/examples/private/example-1.json +1553 -0
- package/examples/private/filters.json +390 -0
- package/examples/private/footnote-issue.json +20943 -0
- package/examples/private/mv-columns.json +506 -0
- package/examples/private/oral-health.json +6171 -0
- package/examples/private/test-breaking-dash.json +942 -0
- package/examples/private/vehicle-issue.json +683 -0
- package/index.html +26 -24
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +13 -3
- package/src/CdcDashboardComponent.tsx +113 -56
- package/src/DashboardContext.tsx +2 -1
- package/src/components/DashboardEditors.tsx +10 -2
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +54 -9
- package/src/components/VisualizationRow.tsx +25 -4
- package/src/helpers/getFilteredData.ts +49 -50
- package/src/helpers/getVizConfig.ts +32 -15
- package/src/index.tsx +1 -0
- package/src/store/dashboard.actions.ts +2 -0
- package/src/store/dashboard.reducer.ts +4 -0
- package/src/types/ConfigRow.ts +1 -0
- package/src/types/InitialState.ts +1 -0
package/index.html
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
|
6
|
-
<style>
|
|
7
|
-
body {
|
|
8
|
-
margin: 0 auto !important;
|
|
9
|
-
display: flex;
|
|
10
|
-
flex-direction: column;
|
|
11
|
-
justify-content: center;
|
|
12
|
-
min-height: unset !important;
|
|
13
|
-
}
|
|
14
3
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
margin: 0 auto !important;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
min-height: unset !important;
|
|
14
|
+
}
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
.react-container+.react-container {
|
|
17
|
+
margin-top: 3rem;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
20
|
+
<link rel="stylesheet prefetch" href="examples/custom/css/respiratory.css" />
|
|
21
|
+
<link rel="stylesheet prefetch" href="https://www.cdc.gov/TemplatePackage/5.0/css/app.min.css?_=71669" />
|
|
22
|
+
</head>
|
|
23
|
+
|
|
24
|
+
<body>
|
|
25
|
+
<div class="react-container" data-config="/examples/private/oral-health.json"></div>
|
|
26
|
+
<script src="https://www.cdc.gov/TemplatePackage/contrib/libs/jquery/latest/jquery.min.js?_=91329"></script>
|
|
27
|
+
<script type="module" src="./src/index.tsx"></script>
|
|
28
|
+
</body>
|
|
29
|
+
|
|
30
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdc/dashboard",
|
|
3
|
-
"version": "4.25.
|
|
3
|
+
"version": "4.25.8",
|
|
4
4
|
"description": "React component for combining multiple visualizations into a single dashboard",
|
|
5
5
|
"moduleName": "CdcDashboard",
|
|
6
6
|
"main": "dist/cdcdashboard",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
},
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cdc/chart": "^4.25.
|
|
31
|
-
"@cdc/core": "^4.25.
|
|
32
|
-
"@cdc/data-bite": "^4.25.
|
|
33
|
-
"@cdc/filtered-text": "^4.25.
|
|
34
|
-
"@cdc/map": "^4.25.
|
|
35
|
-
"@cdc/markup-include": "^4.25.
|
|
36
|
-
"@cdc/waffle-chart": "^4.25.
|
|
30
|
+
"@cdc/chart": "^4.25.8",
|
|
31
|
+
"@cdc/core": "^4.25.8",
|
|
32
|
+
"@cdc/data-bite": "^4.25.8",
|
|
33
|
+
"@cdc/filtered-text": "^4.25.8",
|
|
34
|
+
"@cdc/map": "^4.25.8",
|
|
35
|
+
"@cdc/markup-include": "^4.25.8",
|
|
36
|
+
"@cdc/waffle-chart": "^4.25.8",
|
|
37
37
|
"js-base64": "^2.5.2",
|
|
38
38
|
"react-accessible-accordion": "^3.3.4",
|
|
39
39
|
"react-dnd": "^14.0.2",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"react": "^18.2.0",
|
|
45
45
|
"react-dom": "^18.2.0"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "e369994230b5e3facff224e1d89d5937528ac5a0"
|
|
48
48
|
}
|
package/src/CdcDashboard.tsx
CHANGED
|
@@ -14,13 +14,21 @@ import { coveUpdateWorker } from '@cdc/core/helpers/coveUpdateWorker'
|
|
|
14
14
|
import _ from 'lodash'
|
|
15
15
|
import { getQueryParams } from '@cdc/core/helpers/queryStringUtils'
|
|
16
16
|
import EditorContext from '../../editor/src/ConfigContext'
|
|
17
|
+
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
17
18
|
|
|
18
19
|
type MultiDashboardProps = Omit<WCMSProps, 'configUrl'> & {
|
|
19
20
|
configUrl?: string
|
|
20
21
|
config?: MultiDashboardConfig
|
|
22
|
+
interactionLabel?: string
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({
|
|
25
|
+
const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({
|
|
26
|
+
configUrl,
|
|
27
|
+
isEditor,
|
|
28
|
+
isDebug,
|
|
29
|
+
config,
|
|
30
|
+
interactionLabel = ''
|
|
31
|
+
}) => {
|
|
24
32
|
const [initial, setInitial] = useState<InitialState>(undefined)
|
|
25
33
|
const editorContext = useContext(EditorContext)
|
|
26
34
|
|
|
@@ -43,7 +51,6 @@ const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({ configUrl, isEdi
|
|
|
43
51
|
const versionedConfig = coveUpdateWorker(config)
|
|
44
52
|
return { ...initialState, config: versionedConfig, filteredData, data: datasets }
|
|
45
53
|
}
|
|
46
|
-
|
|
47
54
|
const loadConfig = async () => {
|
|
48
55
|
const _config: MultiDashboardConfig = config || editorContext.config || (await (await fetch(configUrl)).json())
|
|
49
56
|
const selected = getSelectedConfig(_config)
|
|
@@ -51,6 +58,7 @@ const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({ configUrl, isEdi
|
|
|
51
58
|
const { newConfig, datasets } =
|
|
52
59
|
selected !== null ? await loadMultiDashboard(_config, selected) : await loadSingleDashboard(_config)
|
|
53
60
|
setInitial(formatInitialState(newConfig, datasets))
|
|
61
|
+
publishAnalyticsEvent('dashboard_loaded', 'load', interactionLabel, 'dashboard')
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
useEffect(() => {
|
|
@@ -138,7 +146,9 @@ const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({ configUrl, isEdi
|
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
if (!initial) return <Loading />
|
|
141
|
-
return
|
|
149
|
+
return (
|
|
150
|
+
<CdcDashboard isEditor={isEditor} isDebug={isDebug} initialState={initial} interactionLabel={interactionLabel} />
|
|
151
|
+
)
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
export default MultiDashboardWrapper
|
|
@@ -13,6 +13,8 @@ import parse from 'html-react-parser'
|
|
|
13
13
|
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
14
14
|
import { GlobalContextProvider } from '@cdc/core/components/GlobalContext'
|
|
15
15
|
import { DashboardContext, DashboardDispatchContext } from './DashboardContext'
|
|
16
|
+
import { Visualization } from '@cdc/core/types/Visualization'
|
|
17
|
+
import { hasDashboardApplyBehavior } from './helpers/hasDashboardApplyBehavior'
|
|
16
18
|
|
|
17
19
|
import OverlayFrame from '@cdc/core/components/ui/OverlayFrame'
|
|
18
20
|
import Loading from '@cdc/core/components/Loading'
|
|
@@ -42,7 +44,6 @@ import MultiTabs from './components/MultiConfigTabs'
|
|
|
42
44
|
import _ from 'lodash'
|
|
43
45
|
import EditorContext from '../../editor/src/ConfigContext'
|
|
44
46
|
import { APIFilterDropdowns } from './components/DashboardFilters'
|
|
45
|
-
import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
|
|
46
47
|
import { ViewPort } from '@cdc/core/types/ViewPort'
|
|
47
48
|
import VisualizationRow from './components/VisualizationRow'
|
|
48
49
|
import { getVizConfig } from './helpers/getVizConfig'
|
|
@@ -52,7 +53,6 @@ import Layout from '@cdc/core/components/Layout'
|
|
|
52
53
|
import * as reloadURLHelpers from './helpers/reloadURLHelpers'
|
|
53
54
|
import { addValuesToDashboardFilters } from './helpers/addValuesToDashboardFilters'
|
|
54
55
|
import { DashboardFilters } from './types/DashboardFilters'
|
|
55
|
-
import DashboardSharedFilters from './components/DashboardFilters'
|
|
56
56
|
import { loadAPIFiltersFactory } from './helpers/loadAPIFilters'
|
|
57
57
|
import Loader from '@cdc/core/components/Loader'
|
|
58
58
|
import Alert from '@cdc/core/components/Alert'
|
|
@@ -62,9 +62,14 @@ import DashboardEditors from './components/DashboardEditors'
|
|
|
62
62
|
|
|
63
63
|
type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
|
|
64
64
|
initialState: InitialState
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export default function CdcDashboard({
|
|
65
|
+
} & { interactionLabel: string }
|
|
66
|
+
|
|
67
|
+
export default function CdcDashboard({
|
|
68
|
+
initialState,
|
|
69
|
+
isEditor = false,
|
|
70
|
+
isDebug = false,
|
|
71
|
+
interactionLabel = ''
|
|
72
|
+
}: DashboardProps) {
|
|
68
73
|
const [state, dispatch] = useReducer(dashboardReducer, initialState)
|
|
69
74
|
const [errorMessages, dispatchErrorMessages] = useReducer(errorMessagesReducer, [])
|
|
70
75
|
const editorContext = useContext(EditorContext)
|
|
@@ -77,10 +82,16 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
77
82
|
const isPreview = state.tabSelected === 'Dashboard Preview'
|
|
78
83
|
|
|
79
84
|
const inNoDataState = useMemo(() => {
|
|
85
|
+
const hasApplyBehavior = hasDashboardApplyBehavior(state.config.visualizations)
|
|
86
|
+
|
|
87
|
+
if (hasApplyBehavior && !state.filtersApplied) {
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
const vals = reloadURLHelpers.getDatasetKeys(state.config).map(key => state.data[key])
|
|
81
92
|
if (!vals.length) return true
|
|
82
93
|
return vals.some(val => val === undefined)
|
|
83
|
-
}, [state.data])
|
|
94
|
+
}, [state.data, state.config.visualizations, state.filtersApplied])
|
|
84
95
|
|
|
85
96
|
const vizRowColumnLocator = getVizRowColumnLocator(state.config.rows)
|
|
86
97
|
|
|
@@ -105,7 +116,12 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
105
116
|
const filters = newFilters || config.dashboard.sharedFilters
|
|
106
117
|
const datasetKeys = reloadURLHelpers.getDatasetKeys(config)
|
|
107
118
|
|
|
108
|
-
const
|
|
119
|
+
const emptyData = {}
|
|
120
|
+
const emptyFilteredData = {}
|
|
121
|
+
dispatch({ type: 'SET_DATA', payload: emptyData })
|
|
122
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: emptyFilteredData })
|
|
123
|
+
|
|
124
|
+
const newData = {} // Start with empty object instead of cloning existing data
|
|
109
125
|
const newDatasets = _.cloneDeep(config.datasets)
|
|
110
126
|
let dataWasFetched = false
|
|
111
127
|
let newFileName = ''
|
|
@@ -157,8 +173,11 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
157
173
|
})
|
|
158
174
|
|
|
159
175
|
const dataNeedsUpdate = reloadURLHelpers.isUpdateNeeded(filters, currentQSParams, updatedQSParams)
|
|
160
|
-
|
|
161
|
-
if (
|
|
176
|
+
const alreadyFetched = !!dataset.data
|
|
177
|
+
if (alreadyFetched && !newFilters && !dataNeedsUpdate) {
|
|
178
|
+
dataWasFetched = true
|
|
179
|
+
newData[datasetKey] = dataset.data
|
|
180
|
+
} else if (!!newFilters || dataNeedsUpdate) {
|
|
162
181
|
dataWasFetched = true
|
|
163
182
|
const dataUrlFinal = reloadURLHelpers.getDataURL(
|
|
164
183
|
{ ...currentQSParams, ...updatedQSParams },
|
|
@@ -223,7 +242,10 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
223
242
|
_newData
|
|
224
243
|
)
|
|
225
244
|
dispatch({ type: 'SET_FILTERED_DATA', payload: filteredData })
|
|
226
|
-
const visualizations = reloadURLHelpers.getVisualizationsWithFormattedData(
|
|
245
|
+
const visualizations = reloadURLHelpers.getVisualizationsWithFormattedData(
|
|
246
|
+
config.visualizations as Record<string, Visualization>,
|
|
247
|
+
newData
|
|
248
|
+
)
|
|
227
249
|
dispatch({
|
|
228
250
|
type: 'SET_CONFIG',
|
|
229
251
|
payload: {
|
|
@@ -305,13 +327,32 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
305
327
|
})
|
|
306
328
|
if (allValuesSelected) {
|
|
307
329
|
reloadURLData(newFilters)
|
|
308
|
-
setAPILoading(false)
|
|
309
330
|
} else {
|
|
310
331
|
setAPILoading(false)
|
|
311
332
|
}
|
|
312
333
|
})
|
|
313
334
|
}, [isEditor, isPreview, state.config?.activeDashboard])
|
|
314
335
|
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
return () => {
|
|
338
|
+
// Clear all data when component unmounts to prevent memory leaks
|
|
339
|
+
dispatch({ type: 'SET_DATA', payload: {} })
|
|
340
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
|
|
341
|
+
|
|
342
|
+
// Clear any pending API requests
|
|
343
|
+
setAPILoading(false)
|
|
344
|
+
|
|
345
|
+
// Force garbage collection hint if available
|
|
346
|
+
if (window.gc && typeof window.gc === 'function') {
|
|
347
|
+
try {
|
|
348
|
+
window.gc()
|
|
349
|
+
} catch (e) {
|
|
350
|
+
// Ignore gc errors
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}, [])
|
|
355
|
+
|
|
315
356
|
const updateChildConfig = (visualizationKey, newConfig) => {
|
|
316
357
|
const config = _.cloneDeep(state.config)
|
|
317
358
|
const updatedConfig = _.pick(config, ['visualizations', 'multiDashboards'])
|
|
@@ -394,6 +435,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
394
435
|
setSharedFilter={setSharedFilter}
|
|
395
436
|
apiFilterDropdowns={apiFilterDropdowns}
|
|
396
437
|
state={state}
|
|
438
|
+
interactionLabel={interactionLabel}
|
|
397
439
|
/>
|
|
398
440
|
</>
|
|
399
441
|
)
|
|
@@ -458,6 +500,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
458
500
|
apiFilterDropdowns={apiFilterDropdowns}
|
|
459
501
|
currentViewport={currentViewport}
|
|
460
502
|
inNoDataState={inNoDataState}
|
|
503
|
+
interactionLabel={interactionLabel}
|
|
461
504
|
isLastRow={index === filteredRows.length - 1}
|
|
462
505
|
/>
|
|
463
506
|
))}
|
|
@@ -473,6 +516,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
473
516
|
state={config}
|
|
474
517
|
text='Download Dashboard Image'
|
|
475
518
|
elementToCapture={imageId}
|
|
519
|
+
interactionLabel={interactionLabel}
|
|
476
520
|
/>
|
|
477
521
|
)}
|
|
478
522
|
{config.table?.downloadPdfButton && (
|
|
@@ -482,6 +526,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
482
526
|
state={config}
|
|
483
527
|
text='Download Dashboard PDF'
|
|
484
528
|
elementToCapture={imageId}
|
|
529
|
+
interactionLabel={interactionLabel}
|
|
485
530
|
/>
|
|
486
531
|
)}
|
|
487
532
|
</section>
|
|
@@ -500,61 +545,73 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
500
545
|
imageRef={imageId}
|
|
501
546
|
isDebug={isDebug}
|
|
502
547
|
isEditor={isEditor}
|
|
548
|
+
interactionLabel={interactionLabel}
|
|
503
549
|
/>
|
|
504
550
|
)}
|
|
505
551
|
{config.table?.show &&
|
|
506
552
|
config.datasets &&
|
|
507
|
-
Object.keys(config.datasets)
|
|
508
|
-
|
|
553
|
+
Object.keys(config.datasets)
|
|
554
|
+
.map(datasetKey => {
|
|
555
|
+
//For each dataset, find any shared filters that apply to all visualizations using the dataset
|
|
509
556
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
return config.visualizations[visualizationKey].dataKey === datasetKey
|
|
513
|
-
})
|
|
557
|
+
// Get the active dashboard configuration (for multidashboard support)
|
|
558
|
+
const activeConfig = config.multiDashboards ? config.multiDashboards[config.activeDashboard] : config
|
|
514
559
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
let allMatch = true
|
|
519
|
-
vizKeysUsingDataset.forEach(visualizationKey => {
|
|
520
|
-
if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) === -1) {
|
|
521
|
-
allMatch = false
|
|
522
|
-
}
|
|
560
|
+
//Gets list of visuailzations using the dataset (only from current dashboard tab)
|
|
561
|
+
const vizKeysUsingDataset: string[] = getVizKeys(activeConfig).filter(visualizationKey => {
|
|
562
|
+
return activeConfig.visualizations[visualizationKey].dataKey === datasetKey
|
|
523
563
|
})
|
|
524
|
-
|
|
525
|
-
|
|
564
|
+
|
|
565
|
+
// Skip this dataset if no visualizations in the current dashboard use it
|
|
566
|
+
if (vizKeysUsingDataset.length === 0) {
|
|
567
|
+
return null
|
|
526
568
|
}
|
|
527
|
-
})
|
|
528
569
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
className='multi-table-container'
|
|
537
|
-
id={`data-table-${datasetKey}`}
|
|
538
|
-
key={`data-table-${datasetKey}`}
|
|
539
|
-
>
|
|
540
|
-
<DataTable
|
|
541
|
-
config={config as TableConfig}
|
|
542
|
-
dataConfig={config.datasets[datasetKey]}
|
|
543
|
-
rawData={config.datasets[datasetKey].data?.[0]?.tableData || config.datasets[datasetKey].data}
|
|
544
|
-
runtimeData={
|
|
545
|
-
config.datasets[datasetKey].data?.[0]?.tableData ||
|
|
546
|
-
filteredTableData ||
|
|
547
|
-
config.datasets[datasetKey].data ||
|
|
548
|
-
[]
|
|
570
|
+
//Checks shared filters against list to see if all visualizations are represented
|
|
571
|
+
let applicableFilters: SharedFilter[] = []
|
|
572
|
+
activeConfig.dashboard.sharedFilters?.forEach(sharedFilter => {
|
|
573
|
+
let allMatch = true
|
|
574
|
+
vizKeysUsingDataset.forEach(visualizationKey => {
|
|
575
|
+
if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) === -1) {
|
|
576
|
+
allMatch = false
|
|
549
577
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
578
|
+
})
|
|
579
|
+
if (allMatch) {
|
|
580
|
+
applicableFilters.push(sharedFilter)
|
|
581
|
+
}
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
//Applys any applicable filters to the Table
|
|
585
|
+
const filteredTableData =
|
|
586
|
+
applicableFilters.length > 0
|
|
587
|
+
? filterData(applicableFilters, config.datasets[datasetKey].data)
|
|
588
|
+
: undefined
|
|
589
|
+
return (
|
|
590
|
+
<div
|
|
591
|
+
className='multi-table-container'
|
|
592
|
+
id={`data-table-${datasetKey}`}
|
|
593
|
+
key={`data-table-${datasetKey}`}
|
|
594
|
+
>
|
|
595
|
+
<DataTable
|
|
596
|
+
config={config as TableConfig}
|
|
597
|
+
dataConfig={config.datasets[datasetKey]}
|
|
598
|
+
rawData={config.datasets[datasetKey].data?.[0]?.tableData || config.datasets[datasetKey].data}
|
|
599
|
+
runtimeData={
|
|
600
|
+
config.datasets[datasetKey].data?.[0]?.tableData ||
|
|
601
|
+
filteredTableData ||
|
|
602
|
+
config.datasets[datasetKey].data ||
|
|
603
|
+
[]
|
|
604
|
+
}
|
|
605
|
+
expandDataTable={config.table.expanded}
|
|
606
|
+
tableTitle={datasetKey}
|
|
607
|
+
viewport={currentViewport}
|
|
608
|
+
tabbingId={datasetKey}
|
|
609
|
+
interactionLabel={interactionLabel}
|
|
610
|
+
/>
|
|
611
|
+
</div>
|
|
612
|
+
)
|
|
613
|
+
})
|
|
614
|
+
.filter(Boolean)}
|
|
558
615
|
</div>
|
|
559
616
|
</Layout.Responsive>
|
|
560
617
|
</>
|
package/src/DashboardContext.tsx
CHANGED
|
@@ -19,6 +19,7 @@ type DashboardEditorProps = {
|
|
|
19
19
|
setSharedFilter?: Function
|
|
20
20
|
apiFilterDropdowns?: APIFilterDropdowns
|
|
21
21
|
state: DashboardState
|
|
22
|
+
interactionLabel: string
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const DashboardEditors: React.FC<DashboardEditorProps> = ({
|
|
@@ -28,7 +29,8 @@ const DashboardEditors: React.FC<DashboardEditorProps> = ({
|
|
|
28
29
|
isDebug,
|
|
29
30
|
setSharedFilter,
|
|
30
31
|
apiFilterDropdowns,
|
|
31
|
-
state
|
|
32
|
+
state,
|
|
33
|
+
interactionLabel = ''
|
|
32
34
|
}) => {
|
|
33
35
|
const setsSharedFilter =
|
|
34
36
|
state.config.dashboard.sharedFilters &&
|
|
@@ -66,6 +68,7 @@ const DashboardEditors: React.FC<DashboardEditorProps> = ({
|
|
|
66
68
|
showLoader={false}
|
|
67
69
|
dashboardConfig={state.config}
|
|
68
70
|
datasets={state.config.datasets}
|
|
71
|
+
interactionLabel={interactionLabel}
|
|
69
72
|
/>
|
|
70
73
|
)
|
|
71
74
|
|
|
@@ -77,6 +80,7 @@ const DashboardEditors: React.FC<DashboardEditorProps> = ({
|
|
|
77
80
|
isEditor={true}
|
|
78
81
|
setConfig={_updateConfig}
|
|
79
82
|
isDashboard={true}
|
|
83
|
+
interactionLabel={interactionLabel}
|
|
80
84
|
/>
|
|
81
85
|
)
|
|
82
86
|
|
|
@@ -88,6 +92,7 @@ const DashboardEditors: React.FC<DashboardEditorProps> = ({
|
|
|
88
92
|
isEditor={true}
|
|
89
93
|
setConfig={_updateConfig}
|
|
90
94
|
isDashboard={true}
|
|
95
|
+
interactionLabel={interactionLabel}
|
|
91
96
|
/>
|
|
92
97
|
)
|
|
93
98
|
|
|
@@ -100,6 +105,7 @@ const DashboardEditors: React.FC<DashboardEditorProps> = ({
|
|
|
100
105
|
setConfig={_updateConfig}
|
|
101
106
|
isDashboard={true}
|
|
102
107
|
datasets={state.config.datasets}
|
|
108
|
+
interactionLabel={interactionLabel}
|
|
103
109
|
/>
|
|
104
110
|
)
|
|
105
111
|
|
|
@@ -111,9 +117,9 @@ const DashboardEditors: React.FC<DashboardEditorProps> = ({
|
|
|
111
117
|
isEditor={true}
|
|
112
118
|
setConfig={_updateConfig}
|
|
113
119
|
isDashboard={true}
|
|
120
|
+
interactionLabel={interactionLabel}
|
|
114
121
|
/>
|
|
115
122
|
)
|
|
116
|
-
|
|
117
123
|
case 'dashboardFilters':
|
|
118
124
|
return (
|
|
119
125
|
<DashboardSharedFilters
|
|
@@ -121,6 +127,7 @@ const DashboardEditors: React.FC<DashboardEditorProps> = ({
|
|
|
121
127
|
visualizationConfig={visualizationConfig}
|
|
122
128
|
apiFilterDropdowns={apiFilterDropdowns}
|
|
123
129
|
setConfig={_updateConfig}
|
|
130
|
+
interactionLabel={interactionLabel}
|
|
124
131
|
/>
|
|
125
132
|
)
|
|
126
133
|
|
|
@@ -132,6 +139,7 @@ const DashboardEditors: React.FC<DashboardEditorProps> = ({
|
|
|
132
139
|
isEditor={true}
|
|
133
140
|
updateConfig={_updateConfig}
|
|
134
141
|
datasets={state.config.datasets}
|
|
142
|
+
interactionLabel={interactionLabel}
|
|
135
143
|
/>
|
|
136
144
|
)
|
|
137
145
|
|
|
@@ -15,6 +15,7 @@ import * as apiFilterHelpers from '../../helpers/apiFilterHelpers'
|
|
|
15
15
|
import { applyQueuedActive } from '@cdc/core/components/Filters/helpers/applyQueuedActive'
|
|
16
16
|
import './dashboardfilter.styles.css'
|
|
17
17
|
import { updateChildFilters } from '../../helpers/updateChildFilters'
|
|
18
|
+
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
18
19
|
|
|
19
20
|
type SubOptions = { subOptions?: Record<'value' | 'text', string>[] }
|
|
20
21
|
|
|
@@ -32,6 +33,7 @@ type DashboardFiltersProps = {
|
|
|
32
33
|
isEditor?: boolean
|
|
33
34
|
setConfig: (config: DashboardFilters) => void
|
|
34
35
|
currentViewport?: ViewPort
|
|
36
|
+
interactionLabel: string
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
@@ -39,7 +41,8 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
39
41
|
visualizationConfig,
|
|
40
42
|
setConfig: updateConfig,
|
|
41
43
|
currentViewport,
|
|
42
|
-
isEditor = false
|
|
44
|
+
isEditor = false,
|
|
45
|
+
interactionLabel = ''
|
|
43
46
|
}) => {
|
|
44
47
|
const state = useContext(DashboardContext)
|
|
45
48
|
const { config: dashboardConfig, reloadURLData, loadAPIFilters, setAPIFilterDropdowns, setAPILoading } = state
|
|
@@ -47,7 +50,12 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
47
50
|
|
|
48
51
|
const applyFilters = e => {
|
|
49
52
|
e.preventDefault() // prevent form submission
|
|
50
|
-
|
|
53
|
+
|
|
54
|
+
const dashboardConfig = {
|
|
55
|
+
...state.config.dashboard,
|
|
56
|
+
sharedFilters: [...state.config.dashboard.sharedFilters] // Only clone the array we need to modify
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
const nonAutoLoadFilterIndexes = Object.values(state.config.visualizations)
|
|
52
60
|
.filter(v => v.type === 'dashboardFilters')
|
|
53
61
|
.reduce((acc, viz: DashboardFilters) => (!viz.autoLoad ? [...acc, viz.sharedFilterIndexes] : acc), [])
|
|
@@ -61,6 +69,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
61
69
|
})
|
|
62
70
|
if (allRequiredFiltersSelected) {
|
|
63
71
|
if (hasDashboardApplyBehavior(state.config.visualizations)) {
|
|
72
|
+
dispatch({ type: 'SET_FILTERS_APPLIED', payload: true })
|
|
64
73
|
const queryParams = getQueryParams()
|
|
65
74
|
let needsQueryUpdate = false
|
|
66
75
|
dashboardConfig.sharedFilters.forEach(sharedFilter => {
|
|
@@ -82,7 +91,21 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
82
91
|
}
|
|
83
92
|
setAPILoading(true)
|
|
84
93
|
dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
|
|
85
|
-
|
|
94
|
+
|
|
95
|
+
// Clear data when applying filters to force fresh reload
|
|
96
|
+
const emptyData = Object.keys(state.data).reduce((acc, key) => {
|
|
97
|
+
acc[key] = []
|
|
98
|
+
return acc
|
|
99
|
+
}, {})
|
|
100
|
+
|
|
101
|
+
const emptyFilteredData = Object.keys(state.filteredData).reduce((acc, key) => {
|
|
102
|
+
acc[key] = []
|
|
103
|
+
return acc
|
|
104
|
+
}, {})
|
|
105
|
+
|
|
106
|
+
dispatch({ type: 'SET_DATA', payload: emptyData })
|
|
107
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: emptyFilteredData })
|
|
108
|
+
|
|
86
109
|
loadAPIFilters(dashboardConfig.sharedFilters, apiFilterDropdowns)
|
|
87
110
|
.then(newFilters => {
|
|
88
111
|
reloadURLData(newFilters)
|
|
@@ -96,7 +119,14 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
const handleOnChange = (index: number, value: string | string[]) => {
|
|
99
|
-
const newConfig =
|
|
122
|
+
const newConfig = {
|
|
123
|
+
...dashboardConfig,
|
|
124
|
+
dashboard: {
|
|
125
|
+
...dashboardConfig.dashboard,
|
|
126
|
+
sharedFilters: [...dashboardConfig.dashboard.sharedFilters] // Only clone the array we modify
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
100
130
|
let [newSharedFilters, changedFilterIndexes] = changeFilterActive(
|
|
101
131
|
index,
|
|
102
132
|
value,
|
|
@@ -104,6 +134,13 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
104
134
|
visualizationConfig
|
|
105
135
|
)
|
|
106
136
|
|
|
137
|
+
publishAnalyticsEvent(
|
|
138
|
+
'dashboard_filter_changed',
|
|
139
|
+
'change',
|
|
140
|
+
`${interactionLabel}|key_${newSharedFilters?.[index]?.key}|value_${value}`,
|
|
141
|
+
'dashboard'
|
|
142
|
+
)
|
|
143
|
+
|
|
107
144
|
// sets the active filter option that the user just selected.
|
|
108
145
|
dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
|
|
109
146
|
|
|
@@ -125,9 +162,9 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
125
162
|
})
|
|
126
163
|
} else {
|
|
127
164
|
newSharedFilters[index].queuedActive = value
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
165
|
+
|
|
166
|
+
// Don't clear data immediately - keep existing data until new data loads
|
|
167
|
+
// Only update the filter dropdowns and prepare for reload
|
|
131
168
|
setAPIFilterDropdowns(loadingFilterMemo)
|
|
132
169
|
loadAPIFilters(newSharedFilters, loadingFilterMemo)
|
|
133
170
|
}
|
|
@@ -135,8 +172,16 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
135
172
|
if (newSharedFilters[index].type === 'urlfilter' && newSharedFilters[index].apiFilter) {
|
|
136
173
|
reloadURLData(newSharedFilters)
|
|
137
174
|
} else {
|
|
138
|
-
const clonedState =
|
|
139
|
-
|
|
175
|
+
const clonedState = {
|
|
176
|
+
...state,
|
|
177
|
+
config: {
|
|
178
|
+
...state.config,
|
|
179
|
+
dashboard: {
|
|
180
|
+
...state.config.dashboard,
|
|
181
|
+
sharedFilters: newSharedFilters
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
140
185
|
const newFilteredData = getFilteredData(clonedState)
|
|
141
186
|
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
142
187
|
dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
|