@cdc/dashboard 4.24.4 → 4.24.7
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 +179228 -141419
- package/examples/custom/css/respiratory.css +236 -0
- package/examples/custom/js/respiratory.js +242 -0
- package/examples/default-multi-dataset-shared-filter.json +1729 -0
- package/examples/ed-visits-county-file.json +618 -0
- package/examples/filtered-dash.json +6 -21
- package/index.html +12 -3
- package/package.json +12 -11
- package/src/CdcDashboard.tsx +5 -1
- package/src/CdcDashboardComponent.tsx +156 -334
- package/src/DashboardContext.tsx +9 -1
- package/src/_stories/Dashboard.stories.tsx +31 -3
- package/src/_stories/_mock/dashboard-gallery.json +534 -523
- package/src/_stories/_mock/markup-include.json +78 -0
- package/src/_stories/_mock/multi-dashboards.json +914 -0
- package/src/_stories/_mock/multi-viz.json +2 -3
- package/src/_stories/_mock/pivot-filter.json +15 -11
- package/src/_stories/_mock/standalone-table.json +2 -0
- package/src/components/CollapsibleVisualizationRow.tsx +44 -0
- package/src/components/Column.tsx +1 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +80 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +367 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +143 -0
- package/src/components/DashboardFilters/index.ts +3 -0
- package/src/components/DataDesignerModal.tsx +9 -9
- package/src/components/ExpandCollapseButtons.tsx +20 -0
- package/src/components/Header/Header.tsx +1 -97
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +4 -4
- package/src/components/MultiConfigTabs/MultiTabs.tsx +3 -2
- package/src/components/Row.tsx +52 -19
- package/src/components/Toggle/Toggle.tsx +2 -4
- package/src/components/VisualizationRow.tsx +96 -29
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
- package/src/components/VisualizationsPanel/index.ts +1 -0
- package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
- package/src/components/Widget.tsx +26 -90
- package/src/helpers/apiFilterHelpers.ts +51 -0
- package/src/helpers/changeFilterActive.ts +30 -0
- package/src/helpers/filterData.ts +16 -56
- package/src/helpers/generateValuesForFilter.ts +1 -1
- package/src/helpers/getAutoLoadVisualization.ts +11 -0
- package/src/helpers/getFilteredData.ts +4 -2
- package/src/helpers/getVizConfig.ts +23 -2
- package/src/helpers/getVizRowColumnLocator.ts +2 -1
- package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
- package/src/helpers/iconHash.tsx +3 -3
- package/src/helpers/mapDataToConfig.ts +29 -0
- package/src/helpers/processData.ts +2 -3
- package/src/helpers/reloadURLHelpers.ts +68 -0
- package/src/helpers/tests/filterData.test.ts +1 -93
- package/src/scss/editor-panel.scss +1 -1
- package/src/scss/grid.scss +34 -27
- package/src/scss/main.scss +41 -3
- package/src/scss/variables.scss +4 -0
- package/src/store/dashboard.actions.ts +9 -10
- package/src/store/dashboard.reducer.ts +41 -13
- package/src/types/APIFilter.ts +1 -4
- package/src/types/ConfigRow.ts +2 -0
- package/src/types/Dashboard.ts +1 -1
- package/src/types/DashboardConfig.ts +2 -4
- package/src/types/DashboardFilters.ts +7 -0
- package/src/types/InitialState.ts +1 -1
- package/src/types/MultiDashboard.ts +2 -2
- package/src/types/SharedFilter.ts +2 -5
- package/src/types/Tab.ts +1 -1
- package/LICENSE +0 -201
- package/src/components/EditorWrapper/EditorWrapper.tsx +0 -52
- package/src/components/EditorWrapper/editor-wrapper.style.css +0 -13
- package/src/components/Filters.tsx +0 -88
- package/src/components/Header/FilterModal.tsx +0 -506
- package/src/components/VisualizationsPanel.tsx +0 -72
- package/src/helpers/getApiFilterKey.ts +0 -5
|
@@ -18,7 +18,6 @@ import OverlayFrame from '@cdc/core/components/ui/OverlayFrame'
|
|
|
18
18
|
import Loading from '@cdc/core/components/Loading'
|
|
19
19
|
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
20
20
|
import getViewport from '@cdc/core/helpers/getViewport'
|
|
21
|
-
import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
|
|
22
21
|
|
|
23
22
|
import CdcMap from '@cdc/map'
|
|
24
23
|
import CdcChart from '@cdc/chart'
|
|
@@ -28,19 +27,16 @@ import CdcMarkupInclude from '@cdc/markup-include'
|
|
|
28
27
|
import CdcFilteredText from '@cdc/filtered-text'
|
|
29
28
|
|
|
30
29
|
import Grid from './components/Grid'
|
|
31
|
-
import Header
|
|
30
|
+
import Header from './components/Header/Header'
|
|
32
31
|
import DataTable from '@cdc/core/components/DataTable'
|
|
33
32
|
import MediaControls from '@cdc/core/components/MediaControls'
|
|
34
33
|
|
|
35
34
|
import './scss/main.scss'
|
|
36
35
|
import '@cdc/core/styles/v2/main.scss'
|
|
37
|
-
import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
|
|
38
|
-
import { capitalizeSplitAndJoin } from '@cdc/core/helpers/cove/string'
|
|
39
36
|
|
|
40
37
|
import VisualizationsPanel from './components/VisualizationsPanel'
|
|
41
38
|
import dashboardReducer from './store/dashboard.reducer'
|
|
42
39
|
import { filterData } from './helpers/filterData'
|
|
43
|
-
import { getFormattedData } from './helpers/getFormattedData'
|
|
44
40
|
import { getVizKeys } from './helpers/getVizKeys'
|
|
45
41
|
import Title from '@cdc/core/components/ui/Title'
|
|
46
42
|
import { type TableConfig } from '@cdc/core/components/DataTable/types/TableConfig'
|
|
@@ -48,23 +44,27 @@ import { type TableConfig } from '@cdc/core/components/DataTable/types/TableConf
|
|
|
48
44
|
// types
|
|
49
45
|
import { type SharedFilter } from './types/SharedFilter'
|
|
50
46
|
import { type APIFilter } from './types/APIFilter'
|
|
51
|
-
import { type Visualization } from '@cdc/core/types/Visualization'
|
|
52
47
|
import { type WCMSProps } from '@cdc/core/types/WCMSProps'
|
|
53
48
|
import { type InitialState } from './types/InitialState'
|
|
54
49
|
import MultiTabs from './components/MultiConfigTabs'
|
|
55
50
|
import _ from 'lodash'
|
|
56
51
|
import EditorContext from '../../editor/src/ConfigContext'
|
|
57
|
-
import {
|
|
58
|
-
import Filters, { APIFilterDropdowns, DropdownOptions } from './components/Filters'
|
|
59
|
-
import EditorWrapper from './components/EditorWrapper/EditorWrapper'
|
|
60
|
-
import DataTableEditorPanel from '@cdc/core/components/DataTable/components/DataTableEditorPanel'
|
|
52
|
+
import { APIFilterDropdowns, DropdownOptions } from './components/DashboardFilters'
|
|
61
53
|
import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
|
|
62
54
|
import { ViewPort } from '@cdc/core/types/ViewPort'
|
|
63
55
|
import VisualizationRow from './components/VisualizationRow'
|
|
64
56
|
import { getVizConfig } from './helpers/getVizConfig'
|
|
65
|
-
import {
|
|
57
|
+
import { getFilteredData } from './helpers/getFilteredData'
|
|
66
58
|
import { getVizRowColumnLocator } from './helpers/getVizRowColumnLocator'
|
|
67
59
|
import Layout from '@cdc/core/components/Layout'
|
|
60
|
+
import FootnotesStandAlone from '@cdc/core/components/Footnotes/FootnotesStandAlone'
|
|
61
|
+
import * as apiFilterHelpers from './helpers/apiFilterHelpers'
|
|
62
|
+
import * as reloadURLHelpers from './helpers/reloadURLHelpers'
|
|
63
|
+
import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
|
|
64
|
+
import { DashboardFilters } from './types/DashboardFilters'
|
|
65
|
+
import DashboardSharedFilters from './components/DashboardFilters'
|
|
66
|
+
import ExpandCollapseButtons from './components/ExpandCollapseButtons'
|
|
67
|
+
import { hasDashboardApplyBehavior } from './helpers/hasDashboardApplyBehavior'
|
|
68
68
|
|
|
69
69
|
type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
|
|
70
70
|
initialState: InitialState
|
|
@@ -76,13 +76,9 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
76
76
|
const [apiFilterDropdowns, setAPIFilterDropdowns] = useState<APIFilterDropdowns>({})
|
|
77
77
|
const [currentViewport, setCurrentViewport] = useState<ViewPort>('lg')
|
|
78
78
|
const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
|
|
79
|
+
const [allExpanded, setAllExpanded] = useState(true)
|
|
79
80
|
|
|
80
81
|
const isPreview = state.tabSelected === 'Dashboard Preview'
|
|
81
|
-
const replacements = {
|
|
82
|
-
'Remove Spaces': '',
|
|
83
|
-
'Keep Spaces': ' ',
|
|
84
|
-
'Replace With Underscore': '_'
|
|
85
|
-
}
|
|
86
82
|
|
|
87
83
|
const inNoDataState = useMemo(() => {
|
|
88
84
|
const vals = Object.values(state.data)
|
|
@@ -92,134 +88,97 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
92
88
|
|
|
93
89
|
const vizRowColumnLocator = getVizRowColumnLocator(state.config.rows)
|
|
94
90
|
|
|
95
|
-
const getAutoLoadVisualization = (): Visualization | undefined => {
|
|
96
|
-
const autoLoadViz = Object.values(state.config.visualizations).filter(vis => {
|
|
97
|
-
return vis.autoLoad && vis.type === 'filter-dropdowns'
|
|
98
|
-
})
|
|
99
|
-
if (autoLoadViz.length === 0) return
|
|
100
|
-
if (autoLoadViz.length > 1) throw new Error('Only one filter row can be autoloaded')
|
|
101
|
-
return autoLoadViz[0]
|
|
102
|
-
}
|
|
103
|
-
|
|
104
91
|
const transform = new DataTransform()
|
|
105
92
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (sharedFilter.active) return // a value has already been selected.
|
|
93
|
+
const autoLoadFilterIndexes = useMemo(() => {
|
|
94
|
+
return Object.values(state.config.visualizations)
|
|
95
|
+
.filter(v => v.type === 'dashboardFilters')
|
|
96
|
+
.reduce((acc, viz: DashboardFilters) => (viz.autoLoad ? [...acc, ...viz.sharedFilterIndexes] : acc), [])
|
|
97
|
+
}, [state.config.visualizations])
|
|
98
|
+
|
|
99
|
+
const setAutoLoadDefaultValue = (sharedFilterIndex: number, dropdownOptions: DropdownOptions, sharedFilters): SharedFilter => {
|
|
100
|
+
const sharedFilter = _.cloneDeep(sharedFilters[sharedFilterIndex])
|
|
101
|
+
if (!autoLoadFilterIndexes.length || !dropdownOptions) return sharedFilter // no autoLoading happening
|
|
102
|
+
if (!sharedFilter.active && autoLoadFilterIndexes.includes(sharedFilterIndex)) {
|
|
117
103
|
const filterParents = sharedFilters.filter(f => sharedFilter.parents?.includes(f.key))
|
|
118
|
-
const notAllParentFiltersSelected = filterParents.some(p => !p.active
|
|
119
|
-
if (filterParents && notAllParentFiltersSelected) return
|
|
120
|
-
|
|
121
|
-
const defaultValue =
|
|
104
|
+
const notAllParentFiltersSelected = filterParents.some(p => !(p.active || p.queuedActive))
|
|
105
|
+
if (filterParents && notAllParentFiltersSelected) return sharedFilter
|
|
106
|
+
// TODO get default value from query parameter
|
|
107
|
+
const defaultValue = dropdownOptions[0].value
|
|
122
108
|
sharedFilter.active = defaultValue
|
|
123
109
|
}
|
|
110
|
+
return sharedFilter
|
|
124
111
|
}
|
|
125
112
|
|
|
126
|
-
const loadAPIFilters = (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
113
|
+
const loadAPIFilters = (sharedFilters: SharedFilter[], dropdowns = apiFilterDropdowns, recursiveLimit = 3): Promise<SharedFilter[]> => {
|
|
114
|
+
if (!sharedFilters) return
|
|
115
|
+
sharedFilters = sharedFilters.map((filter, index) => setAutoLoadDefaultValue(index, dropdowns[filter.apiFilter?.apiEndpoint], sharedFilters))
|
|
116
|
+
const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
|
|
117
|
+
const loadingFilterMemo = apiFilterHelpers.getLoadingFilterMemo(sharedAPIFilters, dropdowns)
|
|
118
|
+
setAPIFilterDropdowns({ ...dropdowns, ...loadingFilterMemo })
|
|
119
|
+
const filterLookup = new Map(sharedAPIFilters.map(filter => [filter.apiFilter.apiEndpoint, filter.apiFilter]))
|
|
120
|
+
const toFetch = apiFilterHelpers.getToFetch(sharedAPIFilters, dropdowns)
|
|
121
|
+
const newDropdowns = _.cloneDeep(dropdowns)
|
|
122
|
+
return Promise.all(
|
|
123
|
+
Object.keys(toFetch).map(
|
|
124
|
+
endpoint =>
|
|
125
|
+
new Promise<void>(resolve => {
|
|
126
|
+
fetch(endpoint)
|
|
127
|
+
.then(resp => resp.json())
|
|
128
|
+
.then(data => {
|
|
129
|
+
const [_key, index] = toFetch[endpoint]
|
|
130
|
+
if (!Array.isArray(data)) throw new Error('COVE only supports response data in the shape Array<Object>')
|
|
131
|
+
const apiFilter = filterLookup.get(_key) as APIFilter
|
|
132
|
+
const _filterValues = apiFilterHelpers.getFilterValues(data, apiFilter)
|
|
133
|
+
newDropdowns[_key] = _filterValues
|
|
134
|
+
const newDefaultSelectedFilter = setAutoLoadDefaultValue(index, _filterValues, sharedFilters)
|
|
135
|
+
sharedFilters[index] = newDefaultSelectedFilter
|
|
136
|
+
})
|
|
137
|
+
.catch(console.error)
|
|
138
|
+
.finally(() => {
|
|
139
|
+
resolve()
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
).then(() => {
|
|
144
|
+
const finishedLoading = sharedFilters.reduce((acc, curr, index) => {
|
|
145
|
+
if (autoLoadFilterIndexes.includes(index) && !curr.active) return false
|
|
134
146
|
return acc
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
|
|
144
|
-
const { textSelector, valueSelector } = apiFilter
|
|
145
|
-
return data.map(v => ({ text: v[textSelector], value: v[valueSelector] }))
|
|
147
|
+
}, true)
|
|
148
|
+
if (finishedLoading || recursiveLimit === 0) {
|
|
149
|
+
setAPIFilterDropdowns(dropdowns => ({ ...dropdowns, ...newDropdowns }))
|
|
150
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
|
|
151
|
+
return sharedFilters
|
|
152
|
+
} else {
|
|
153
|
+
return loadAPIFilters(sharedFilters, newDropdowns, recursiveLimit - 1)
|
|
146
154
|
}
|
|
147
|
-
|
|
148
|
-
sharedAPIFilters.forEach((filter, index) => {
|
|
149
|
-
const baseEndpoint = filter.apiFilter.apiEndpoint
|
|
150
|
-
const _key = getApiFilterKey(filter.apiFilter)
|
|
151
|
-
const params = getParentParams(filter)
|
|
152
|
-
const notAllParentsSelected = params?.some(({ value }) => value === '')
|
|
153
|
-
|
|
154
|
-
if (notAllParentsSelected) return // don't send request for dependent children filter options
|
|
155
|
-
if (apiFilterDropdowns[_key] && !params) return // don't reload filter unless it's a child
|
|
156
|
-
const topLevelDataAlreadyLoaded = apiFilterDropdowns[_key] && !filter.parents
|
|
157
|
-
if (topLevelDataAlreadyLoaded) return // don't reload top level filters
|
|
158
|
-
|
|
159
|
-
const endpoint = baseEndpoint + (params ? gatherQueryParams(params) : '')
|
|
160
|
-
toFetch[endpoint] = [_key, index]
|
|
161
|
-
})
|
|
162
|
-
return Promise.all(
|
|
163
|
-
Object.keys(toFetch).map(
|
|
164
|
-
endpoint =>
|
|
165
|
-
new Promise<void>(resolve => {
|
|
166
|
-
fetch(endpoint)
|
|
167
|
-
.then(resp => resp.json())
|
|
168
|
-
.then(data => {
|
|
169
|
-
const [_key, index] = toFetch[endpoint]
|
|
170
|
-
if (!Array.isArray(data)) throw new Error('COVE only supports response data in the shape Array<Object>')
|
|
171
|
-
const apiFilter = filterLookup.get(_key) as APIFilter
|
|
172
|
-
const _filterValues = getFilterValues(data, apiFilter)
|
|
173
|
-
setAPIFilterDropdowns(dropdowns => ({ ...dropdowns, [_key]: _filterValues }))
|
|
174
|
-
setAutoLoadDefaultValue(index, _filterValues, dashboardConfigOverride)
|
|
175
|
-
})
|
|
176
|
-
.catch(console.error)
|
|
177
|
-
.finally(() => {
|
|
178
|
-
resolve()
|
|
179
|
-
})
|
|
180
|
-
})
|
|
181
|
-
)
|
|
182
|
-
)
|
|
183
|
-
}
|
|
155
|
+
})
|
|
184
156
|
}
|
|
185
157
|
|
|
186
|
-
const reloadURLData = async (
|
|
158
|
+
const reloadURLData = async (newFilters?: SharedFilter[]) => {
|
|
187
159
|
const config = _.cloneDeep(state.config)
|
|
188
160
|
if (!config.datasets) return
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
161
|
+
const filters = newFilters || config.dashboard.sharedFilters
|
|
162
|
+
const datasetKeys = Object.keys(config.datasets)
|
|
163
|
+
|
|
164
|
+
const newData = _.cloneDeep(state.data)
|
|
165
|
+
const newDatasets = _.cloneDeep(config.datasets)
|
|
166
|
+
let dataWasFetched = false
|
|
193
167
|
let newFileName = ''
|
|
194
|
-
|
|
168
|
+
|
|
195
169
|
for (let i = 0; i < datasetKeys.length; i++) {
|
|
196
170
|
const datasetKey = datasetKeys[i]
|
|
197
171
|
const dataset = config.datasets[datasetKey]
|
|
198
172
|
|
|
199
173
|
if (dataset.dataUrl && filters) {
|
|
200
174
|
const dataUrl = new URL(dataset.runtimeDataUrl || dataset.dataUrl, window.location.origin)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
let isUpdateNeeded = !!dashboardConfigOverride
|
|
204
|
-
|
|
175
|
+
const currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
|
|
176
|
+
const updatedQSParams = {}
|
|
205
177
|
filters.forEach(filter => {
|
|
206
178
|
// filter.active is always a string when filter.type is 'urlfilter'
|
|
207
179
|
if (filter.type === 'urlfilter' && !Array.isArray(filter.active)) {
|
|
208
180
|
if (filter.filterBy === 'File Name') {
|
|
209
|
-
|
|
210
|
-
if (filter.datasetKey === datasetKey) {
|
|
211
|
-
if (filter.fileName) {
|
|
212
|
-
// if a file name is found, ie, state_${query}, use that, ie. state_activeFilter.json
|
|
213
|
-
newFileName = capitalizeSplitAndJoin.call(String(filter.fileName), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces'])
|
|
214
|
-
} else {
|
|
215
|
-
// if no file name is entered use the default active filter. ie. /activeFilter.json
|
|
216
|
-
newFileName = filter.active
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (newFileName?.includes('${query}')) {
|
|
221
|
-
newFileName = newFileName.replace('${query}', capitalizeSplitAndJoin.call(String(filter.active), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces']))
|
|
222
|
-
}
|
|
181
|
+
newFileName = reloadURLHelpers.getNewFileName(newFileName, filter, datasetKey)
|
|
223
182
|
}
|
|
224
183
|
|
|
225
184
|
if (!!filter.queryParameter) {
|
|
@@ -229,62 +188,42 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
229
188
|
updatedQSParams[filter.queryParameter] = filter.active
|
|
230
189
|
}
|
|
231
190
|
}
|
|
232
|
-
}
|
|
233
|
-
})
|
|
234
191
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
192
|
+
if (filter.apiFilter) {
|
|
193
|
+
updatedQSParams[filter.apiFilter.valueSelector] = filter.active
|
|
194
|
+
}
|
|
238
195
|
}
|
|
239
196
|
})
|
|
240
197
|
|
|
241
|
-
if (isUpdateNeeded) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
198
|
+
if (!!newFilters || reloadURLHelpers.isUpdateNeeded(filters, currentQSParams, updatedQSParams)) {
|
|
199
|
+
dataWasFetched = true
|
|
200
|
+
const dataUrlFinal = reloadURLHelpers.getDataURL({ ...currentQSParams, ...updatedQSParams }, dataUrl, newFileName)
|
|
201
|
+
|
|
202
|
+
await fetchRemoteData(dataUrlFinal).then(responseData => {
|
|
203
|
+
let data: any[] = responseData
|
|
204
|
+
if (responseData && dataset.dataDescription) {
|
|
205
|
+
try {
|
|
206
|
+
data = transform.autoStandardize(data)
|
|
207
|
+
data = transform.developerStandardize(data, dataset.dataDescription)
|
|
208
|
+
} catch (e) {
|
|
209
|
+
//Data not able to be standardized, leave as is
|
|
210
|
+
}
|
|
246
211
|
}
|
|
212
|
+
newDatasets[datasetKey].data = data
|
|
213
|
+
newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
|
|
214
|
+
newData[datasetKey] = data
|
|
247
215
|
})
|
|
248
|
-
const _params = Object.keys(updatedQSParams).map(key => ({ key, value: updatedQSParams[key] }))
|
|
249
|
-
let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${gatherQueryParams(_params)}`
|
|
250
|
-
|
|
251
|
-
if (newFileName !== '') {
|
|
252
|
-
let fileExtension = dataUrl.pathname.split('.').pop()
|
|
253
|
-
let pathWithoutFilename = dataUrl.pathname.substring(0, dataUrl.pathname.lastIndexOf('/'))
|
|
254
|
-
dataUrlFinal = `${dataUrl.origin}${pathWithoutFilename}/${newFileName}.${fileExtension}${gatherQueryParams(_params)}`
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
let newDataset = await fetchRemoteData(`${dataUrlFinal}`)
|
|
258
|
-
|
|
259
|
-
if (newDataset && dataset.dataDescription) {
|
|
260
|
-
try {
|
|
261
|
-
newDataset = transform.autoStandardize(newDataset)
|
|
262
|
-
newDataset = transform.developerStandardize(newDataset, dataset.dataDescription)
|
|
263
|
-
} catch (e) {
|
|
264
|
-
//Data not able to be standardized, leave as is
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
newDatasets[datasetKey].data = newDataset
|
|
268
|
-
newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
|
|
269
|
-
newData[datasetKey] = newDataset
|
|
270
216
|
}
|
|
271
217
|
}
|
|
272
218
|
}
|
|
273
219
|
|
|
274
|
-
if (
|
|
275
|
-
const dashboardConfig = dashboardConfigOverride || config.dashboard
|
|
220
|
+
if (dataWasFetched) {
|
|
276
221
|
dispatch({ type: 'SET_DATA', payload: newData })
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
acc[vizKey].formattedData = newData[dataKey]
|
|
283
|
-
}
|
|
284
|
-
return acc
|
|
285
|
-
}, _.cloneDeep(config.visualizations))
|
|
286
|
-
|
|
287
|
-
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
222
|
+
const filtersWithNewValues = addValuesToFilters<SharedFilter>(filters, newData)
|
|
223
|
+
const dashboardConfig = newFilters ? { ...config.dashboard, sharedFilters: filtersWithNewValues } : config.dashboard
|
|
224
|
+
const filteredData = getFilteredData({ ...state, config: { ...state.config, dashboard: dashboardConfig } }, {}, newData)
|
|
225
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: filteredData })
|
|
226
|
+
const visualizations = reloadURLHelpers.getVisualizationsWithFormattedData(config.visualizations, newData)
|
|
288
227
|
dispatch({ type: 'SET_CONFIG', payload: { dashboard: dashboardConfig, datasets: newDatasets, visualizations } })
|
|
289
228
|
}
|
|
290
229
|
}
|
|
@@ -315,16 +254,21 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
315
254
|
const newFilteredData = getFilteredData({ ...state, config: newConfig }, filteredData)
|
|
316
255
|
|
|
317
256
|
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
257
|
+
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
318
258
|
dispatch({ type: 'SET_SHARED_FILTERS', payload: newConfig.dashboard.sharedFilters })
|
|
319
259
|
}
|
|
320
260
|
|
|
321
261
|
useEffect(() => {
|
|
262
|
+
if (isEditor && !isPreview) return
|
|
322
263
|
const { config } = state
|
|
323
|
-
if (config.
|
|
264
|
+
if (!hasDashboardApplyBehavior(config.visualizations)) {
|
|
324
265
|
reloadURLData()
|
|
325
266
|
}
|
|
326
|
-
|
|
327
|
-
|
|
267
|
+
|
|
268
|
+
const sharedFiltersWithValues = addValuesToFilters<SharedFilter>(config.dashboard.sharedFilters, state.data)
|
|
269
|
+
loadAPIFilters(sharedFiltersWithValues)
|
|
270
|
+
updateFilteredData()
|
|
271
|
+
}, [isEditor, isPreview, state.config?.activeDashboard])
|
|
328
272
|
|
|
329
273
|
const updateChildConfig = (visualizationKey, newConfig) => {
|
|
330
274
|
const config = _.cloneDeep(state.config)
|
|
@@ -347,129 +291,13 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
347
291
|
}
|
|
348
292
|
}
|
|
349
293
|
|
|
350
|
-
const
|
|
351
|
-
const dashboardConfig = _.cloneDeep(state.config.dashboard)
|
|
352
|
-
const autoLoadViz = getAutoLoadVisualization()
|
|
353
|
-
const nonAutoLoadFilterIndexes = autoLoadViz?.hide || []
|
|
354
|
-
const allRequiredFiltersSelected = !dashboardConfig.sharedFilters.some((filter, filterIndex) => {
|
|
355
|
-
if (nonAutoLoadFilterIndexes.includes(filterIndex)) {
|
|
356
|
-
!filter.active && !filter.queuedActive
|
|
357
|
-
} else {
|
|
358
|
-
// autoload filters don't need to be selected to apply filters
|
|
359
|
-
return false
|
|
360
|
-
}
|
|
361
|
-
})
|
|
362
|
-
if (allRequiredFiltersSelected) {
|
|
363
|
-
if (state.config.filterBehavior === FilterBehavior.Apply) {
|
|
364
|
-
const queryParams = getQueryParams()
|
|
365
|
-
let needsQueryUpdate = false
|
|
366
|
-
dashboardConfig.sharedFilters.forEach((sharedFilter, index) => {
|
|
367
|
-
if (sharedFilter.queuedActive) {
|
|
368
|
-
dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
|
|
369
|
-
delete dashboardConfig.sharedFilters[index].queuedActive
|
|
370
|
-
|
|
371
|
-
if (sharedFilter.setByQueryParameter && queryParams[sharedFilter.setByQueryParameter] !== sharedFilter.active) {
|
|
372
|
-
queryParams[sharedFilter.setByQueryParameter] = sharedFilter.active
|
|
373
|
-
needsQueryUpdate = true
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
if (needsQueryUpdate) {
|
|
379
|
-
updateQueryString(queryParams)
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
|
|
384
|
-
updateDataFilters()
|
|
385
|
-
loadAPIFilters(dashboardConfig)
|
|
386
|
-
.then(() => {
|
|
387
|
-
reloadURLData(dashboardConfig)
|
|
388
|
-
})
|
|
389
|
-
.catch(e => {
|
|
390
|
-
console.error(e)
|
|
391
|
-
})
|
|
392
|
-
} else {
|
|
393
|
-
// TODO noftify of required fields
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const changeFilterActive = (index: number, value: string | string[]) => {
|
|
398
|
-
const sharedFilters = _.cloneDeep(state.config.dashboard.sharedFilters)
|
|
399
|
-
const filterActive = sharedFilters[index]
|
|
400
|
-
const nonAutoLoadFilterIndexes = getAutoLoadVisualization()?.hide
|
|
401
|
-
const isAutoLoad = nonAutoLoadFilterIndexes && !nonAutoLoadFilterIndexes.includes(index)
|
|
402
|
-
|
|
403
|
-
if (state.config.filterBehavior !== FilterBehavior.Apply || isAutoLoad) {
|
|
404
|
-
sharedFilters[index].active = value
|
|
405
|
-
|
|
406
|
-
const queryParams = getQueryParams()
|
|
407
|
-
if (filterActive.setByQueryParameter && queryParams[filterActive.setByQueryParameter] !== filterActive.active) {
|
|
408
|
-
queryParams[filterActive.setByQueryParameter] = filterActive.active
|
|
409
|
-
updateQueryString(queryParams)
|
|
410
|
-
}
|
|
411
|
-
} else {
|
|
412
|
-
if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
|
|
413
|
-
sharedFilters[index].queuedActive = value
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
|
|
417
|
-
if (state.config.filterBehavior !== FilterBehavior.Apply) {
|
|
418
|
-
updateDataFilters(sharedFilters)
|
|
419
|
-
reloadURLData()
|
|
420
|
-
}
|
|
421
|
-
return sharedFilters
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const updateDataFilters = (sharedFilters = undefined) => {
|
|
294
|
+
const updateFilteredData = (sharedFilters = undefined) => {
|
|
425
295
|
const clonedState = _.cloneDeep(state)
|
|
426
296
|
if (sharedFilters) clonedState.config.dashboard.sharedFilters = sharedFilters
|
|
427
297
|
const newFilteredData = getFilteredData(clonedState)
|
|
428
298
|
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
429
299
|
}
|
|
430
300
|
|
|
431
|
-
const handleOnChange = (index: number, value: string | string[]) => {
|
|
432
|
-
const config = _.cloneDeep(state.config)
|
|
433
|
-
const newSharedFilters = changeFilterActive(index, value)
|
|
434
|
-
if (config.filterBehavior === FilterBehavior.Apply) {
|
|
435
|
-
const autoLoadViz = getAutoLoadVisualization()
|
|
436
|
-
const isAutoSelectFilter = !autoLoadViz?.hide.includes(index)
|
|
437
|
-
const missingFilterSelections = config.dashboard.sharedFilters.some(f => !f.active)
|
|
438
|
-
if (isAutoSelectFilter && !missingFilterSelections) {
|
|
439
|
-
// a dropdown has been selected that doesn't
|
|
440
|
-
// require the Go Button
|
|
441
|
-
reloadURLData({ sharedFilters: newSharedFilters })
|
|
442
|
-
} else {
|
|
443
|
-
// A parent filter was selected, reset filters by:
|
|
444
|
-
// set auto select filter dropdowns to null
|
|
445
|
-
const autoSelectFilters = config.dashboard.sharedFilters.filter((_, _index) => !autoLoadViz?.hide.includes(_index))
|
|
446
|
-
const dropdownFilterKeys = autoSelectFilters.map(filter => getApiFilterKey(filter.apiFilter!))
|
|
447
|
-
const newApiDropdowns = { ...apiFilterDropdowns }
|
|
448
|
-
dropdownFilterKeys.forEach(key => (newApiDropdowns[key] = null))
|
|
449
|
-
setAPIFilterDropdowns(newApiDropdowns)
|
|
450
|
-
// remove active from sharedFilters that are autoLoading
|
|
451
|
-
const dashboardConfig = { ...config.dashboard }
|
|
452
|
-
if (config.filterBehavior !== FilterBehavior.Apply) {
|
|
453
|
-
dashboardConfig.sharedFilters[index].active = value
|
|
454
|
-
} else {
|
|
455
|
-
if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
|
|
456
|
-
dashboardConfig.sharedFilters[index].queuedActive = value
|
|
457
|
-
}
|
|
458
|
-
const newSharedFilters = config.dashboard.sharedFilters.map((filter, _index) => {
|
|
459
|
-
const _isAutoSelectFilter = !autoLoadViz?.hide.includes(_index)
|
|
460
|
-
if (_isAutoSelectFilter) filter.active = ''
|
|
461
|
-
return filter
|
|
462
|
-
})
|
|
463
|
-
const _newConfig = { dashboard: { ...config.dashboard, sharedFilters: newSharedFilters } }
|
|
464
|
-
dispatch({ type: 'SET_CONFIG', payload: _newConfig })
|
|
465
|
-
// setData to empty object because we no longer have a data state.
|
|
466
|
-
dispatch({ type: 'SET_DATA', payload: {} })
|
|
467
|
-
dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
|
|
468
|
-
loadAPIFilters(_newConfig.dashboard)
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
301
|
const resizeObserver = new ResizeObserver(entries => {
|
|
474
302
|
for (let entry of entries) {
|
|
475
303
|
let newViewport = getViewport(entry.contentRect.width)
|
|
@@ -484,18 +312,9 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
484
312
|
}
|
|
485
313
|
}, [])
|
|
486
314
|
|
|
487
|
-
const setPreview = shouldPreview => dispatch({ type: 'SET_PREVIEW', payload: shouldPreview })
|
|
488
|
-
|
|
489
315
|
// Prevent render if loading
|
|
490
316
|
if (state.loading) return <Loading />
|
|
491
317
|
|
|
492
|
-
const GoButton = ({ autoLoad }: { autoLoad?: boolean }) => {
|
|
493
|
-
if (state.config.filterBehavior === FilterBehavior.Apply && !autoLoad) {
|
|
494
|
-
return <button onClick={applyFilters}>GO!</button>
|
|
495
|
-
}
|
|
496
|
-
return null
|
|
497
|
-
}
|
|
498
|
-
|
|
499
318
|
let body: JSX.Element | null = null
|
|
500
319
|
// Editor mode
|
|
501
320
|
if (isEditor && !isPreview) {
|
|
@@ -504,7 +323,8 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
504
323
|
getVizKeys(state.config).forEach(visualizationKey => {
|
|
505
324
|
const rowNumber = vizRowColumnLocator[visualizationKey]?.row
|
|
506
325
|
const visualizationConfig = getVizConfig(visualizationKey, rowNumber, state.config, state.data, state.filteredData)
|
|
507
|
-
|
|
326
|
+
visualizationConfig.uid = visualizationKey
|
|
327
|
+
if (visualizationConfig.type === 'footnotes') visualizationConfig.formattedData = undefined
|
|
508
328
|
const setsSharedFilter = state.config.dashboard.sharedFilters && state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey).length > 0
|
|
509
329
|
const setSharedFilterValue = setsSharedFilter ? state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey)[0].active : undefined
|
|
510
330
|
|
|
@@ -590,13 +410,12 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
590
410
|
</>
|
|
591
411
|
)
|
|
592
412
|
break
|
|
593
|
-
case '
|
|
413
|
+
case 'dashboardFilters':
|
|
594
414
|
const hideFilter = visualizationConfig.autoLoad && inNoDataState
|
|
595
415
|
body = !hideFilter ? (
|
|
596
416
|
<>
|
|
597
417
|
<Header visualizationKey={visualizationKey} subEditor='Filter Dropdowns' />
|
|
598
|
-
<
|
|
599
|
-
<GoButton autoLoad={visualizationConfig.autoLoad} />
|
|
418
|
+
<DashboardSharedFilters isEditor={true} visualizationConfig={visualizationConfig} apiFilterDropdowns={apiFilterDropdowns} setConfig={_updateConfig} />
|
|
600
419
|
</>
|
|
601
420
|
) : (
|
|
602
421
|
<></>
|
|
@@ -604,9 +423,18 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
604
423
|
break
|
|
605
424
|
case 'table':
|
|
606
425
|
body = (
|
|
607
|
-
|
|
608
|
-
<
|
|
609
|
-
|
|
426
|
+
<>
|
|
427
|
+
<Header visualizationKey={visualizationKey} subEditor='Table' />
|
|
428
|
+
<DataTableStandAlone visualizationKey={visualizationKey} config={visualizationConfig} isEditor={true} updateConfig={_updateConfig} />
|
|
429
|
+
</>
|
|
430
|
+
)
|
|
431
|
+
break
|
|
432
|
+
case 'footnotes':
|
|
433
|
+
body = (
|
|
434
|
+
<>
|
|
435
|
+
<Header visualizationKey={visualizationKey} subEditor='Footnotes' />
|
|
436
|
+
<FootnotesStandAlone visualizationKey={visualizationKey} config={{ ...visualizationConfig, datasets: state.config.datasets }} isEditor={true} updateConfig={_updateConfig} />
|
|
437
|
+
</>
|
|
610
438
|
)
|
|
611
439
|
break
|
|
612
440
|
default:
|
|
@@ -621,7 +449,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
621
449
|
<DndProvider backend={HTML5Backend}>
|
|
622
450
|
<div className='header-container'>
|
|
623
451
|
<Header />
|
|
624
|
-
<VisualizationsPanel
|
|
452
|
+
<VisualizationsPanel />
|
|
625
453
|
</div>
|
|
626
454
|
|
|
627
455
|
<div className='layout-container'>
|
|
@@ -633,6 +461,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
633
461
|
} else {
|
|
634
462
|
const { config } = state
|
|
635
463
|
const { title, description } = config.dashboard || {}
|
|
464
|
+
|
|
636
465
|
body = (
|
|
637
466
|
<>
|
|
638
467
|
{isEditor && <Header />}
|
|
@@ -642,22 +471,13 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
642
471
|
<Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
|
|
643
472
|
{/* Description */}
|
|
644
473
|
{description && <div className='subtext'>{parse(description)}</div>}
|
|
645
|
-
|
|
646
|
-
{/* Filters */}
|
|
647
|
-
{config.dashboard.sharedFilters && Object.values(config.visualizations || {}).filter(viz => viz.visualizationType === 'filter-dropdowns').length === 0 && (
|
|
648
|
-
<>
|
|
649
|
-
<Filters filters={state.config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
|
|
650
|
-
<GoButton />
|
|
651
|
-
</>
|
|
652
|
-
)}
|
|
653
|
-
|
|
654
474
|
{/* Visualizations */}
|
|
655
475
|
{config.rows &&
|
|
656
476
|
config.rows
|
|
657
477
|
.filter(row => row.columns.filter(col => col.widget).length !== 0)
|
|
658
478
|
.map((row, index) => {
|
|
659
479
|
if (row.multiVizColumn && (isPreview || !isEditor)) {
|
|
660
|
-
const filteredData = getFilteredData(state)
|
|
480
|
+
const filteredData = getFilteredData(state, _.cloneDeep(state.data))
|
|
661
481
|
const data = filteredData[index] ?? row.formattedData
|
|
662
482
|
const dataGroups = {}
|
|
663
483
|
data.forEach(d => {
|
|
@@ -665,29 +485,31 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
665
485
|
if (!dataGroups[groupKey]) dataGroups[groupKey] = []
|
|
666
486
|
dataGroups[groupKey].push(d)
|
|
667
487
|
})
|
|
668
|
-
return Object.keys(dataGroups).map(groupName => {
|
|
669
|
-
const dataValue = dataGroups[groupName]
|
|
670
|
-
return (
|
|
671
|
-
<React.Fragment key={`row__${index}__${groupName}`}>
|
|
672
|
-
<h1 className='h4'>{groupName}</h1>
|
|
673
|
-
<VisualizationRow
|
|
674
|
-
filteredDataOverride={dataValue}
|
|
675
|
-
row={row}
|
|
676
|
-
rowIndex={index}
|
|
677
|
-
setSharedFilter={setSharedFilter}
|
|
678
|
-
updateChildConfig={updateChildConfig}
|
|
679
|
-
applyFilters={applyFilters}
|
|
680
|
-
apiFilterDropdowns={apiFilterDropdowns}
|
|
681
|
-
handleOnChange={handleOnChange}
|
|
682
|
-
currentViewport={currentViewport}
|
|
683
|
-
/>
|
|
684
|
-
</React.Fragment>
|
|
685
|
-
)
|
|
686
|
-
})
|
|
687
|
-
} else {
|
|
688
488
|
return (
|
|
689
|
-
|
|
489
|
+
<>
|
|
490
|
+
{/* Expand/Collapse All */}
|
|
491
|
+
{row.expandCollapseAllButtons === true && <ExpandCollapseButtons setAllExpanded={setAllExpanded} />}
|
|
492
|
+
{Object.keys(dataGroups).map(groupName => {
|
|
493
|
+
const dataValue = dataGroups[groupName]
|
|
494
|
+
return (
|
|
495
|
+
<VisualizationRow
|
|
496
|
+
key={`row__${index}__${groupName}`}
|
|
497
|
+
allExpanded={allExpanded}
|
|
498
|
+
filteredDataOverride={dataValue}
|
|
499
|
+
groupName={groupName}
|
|
500
|
+
row={row}
|
|
501
|
+
rowIndex={index}
|
|
502
|
+
setSharedFilter={setSharedFilter}
|
|
503
|
+
updateChildConfig={updateChildConfig}
|
|
504
|
+
apiFilterDropdowns={apiFilterDropdowns}
|
|
505
|
+
currentViewport={currentViewport}
|
|
506
|
+
/>
|
|
507
|
+
)
|
|
508
|
+
})}
|
|
509
|
+
</>
|
|
690
510
|
)
|
|
511
|
+
} else {
|
|
512
|
+
return <VisualizationRow key={`row__${index}`} allExpanded={false} groupName={''} row={row} rowIndex={index} setSharedFilter={setSharedFilter} updateChildConfig={updateChildConfig} apiFilterDropdowns={apiFilterDropdowns} currentViewport={currentViewport} />
|
|
691
513
|
}
|
|
692
514
|
})}
|
|
693
515
|
|
|
@@ -765,7 +587,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
765
587
|
|
|
766
588
|
return (
|
|
767
589
|
<GlobalContextProvider>
|
|
768
|
-
<DashboardContext.Provider value={{ ...state, setParentConfig: editorContext.setTempConfig, outerContainerRef, isDebug }}>
|
|
590
|
+
<DashboardContext.Provider value={{ ...state, setParentConfig: editorContext.setTempConfig, outerContainerRef, isDebug, loadAPIFilters, reloadURLData }}>
|
|
769
591
|
<DashboardDispatchContext.Provider value={dispatch}>
|
|
770
592
|
<div className={dashboardContainerClasses.join(' ')} ref={outerContainerRef} data-download-id={imageId}>
|
|
771
593
|
{body}
|