@cdc/dashboard 4.23.11 → 4.24.2
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 +109007 -98738
- package/examples/DEV-6574.json +2224 -0
- package/examples/filters/Alabama.json +72 -0
- package/examples/filters/Alaska.json +1737 -0
- package/examples/filters/Arkansas.json +4713 -0
- package/examples/filters/California.json +212 -0
- package/examples/filters/Colorado.json +1500 -0
- package/examples/filters/Connecticut.json +559 -0
- package/examples/filters/Delaware.json +63 -0
- package/examples/filters/DistrictofColumbia.json +63 -0
- package/examples/filters/Florida.json +4217 -0
- package/examples/filters/States.json +146 -0
- package/examples/test.json +752 -0
- package/examples/zika.json +2274 -0
- package/index.html +5 -3
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +124 -963
- package/src/CdcDashboardComponent.tsx +903 -0
- package/src/_stories/Dashboard.stories.tsx +2 -2
- package/src/components/Column.tsx +15 -12
- package/src/components/Header/Header.tsx +694 -0
- package/src/components/Header/index.tsx +1 -676
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +106 -0
- package/src/components/MultiConfigTabs/MultiTabs.tsx +30 -0
- package/src/components/MultiConfigTabs/index.tsx +8 -0
- package/src/components/MultiConfigTabs/multiconfigtabs.styles.css +32 -0
- package/src/components/Widget.tsx +25 -9
- package/src/helpers/filterData.ts +73 -73
- package/src/helpers/generateValuesForFilter.ts +25 -29
- package/src/helpers/getUpdateConfig.ts +6 -2
- package/src/helpers/processData.ts +13 -0
- package/src/helpers/processDataLegacy.ts +14 -0
- package/src/{index.jsx → index.tsx} +2 -2
- package/src/scss/editor-panel.scss +14 -11
- package/src/scss/grid.scss +4 -6
- package/src/scss/main.scss +2 -8
- package/src/store/dashboard.actions.ts +10 -4
- package/src/store/dashboard.reducer.ts +74 -3
- package/src/types/ConfigRow.ts +6 -0
- package/src/types/Dashboard.ts +11 -0
- package/src/types/DashboardConfig.ts +23 -0
- package/src/types/InitialState.ts +10 -0
- package/src/types/MultiDashboard.ts +11 -0
- package/src/types/SharedFilter.ts +31 -20
- package/src/types/Config.ts +0 -27
package/src/CdcDashboard.tsx
CHANGED
|
@@ -1,963 +1,124 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import '
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const setAutoLoadDefaultValue = (sharedFilterIndex: number, filterDropdowns: DropdownOptions) => {
|
|
128
|
-
if (!state.config) return
|
|
129
|
-
const autoLoadViz = getAutoLoadVisualization()
|
|
130
|
-
if (!autoLoadViz) return // no autoLoading happening
|
|
131
|
-
const notIncludedInAutoLoad = autoLoadViz.hide
|
|
132
|
-
if (notIncludedInAutoLoad.includes(sharedFilterIndex)) {
|
|
133
|
-
// we don't want to auto load it
|
|
134
|
-
return
|
|
135
|
-
} else {
|
|
136
|
-
const sharedFilter = state.config.dashboard.sharedFilters[sharedFilterIndex]
|
|
137
|
-
if (sharedFilter.active) return // a value has already been selected.
|
|
138
|
-
const filterParents = state.config.dashboard.sharedFilters.filter(f => sharedFilter.parents?.includes(f.key))
|
|
139
|
-
const notAllParentFiltersSelected = filterParents.some(p => !p.active)
|
|
140
|
-
if (filterParents && notAllParentFiltersSelected) return
|
|
141
|
-
const defaultFilterDropdown = filterDropdowns.find(({ value }) => value === sharedFilter.apiFilter!.defaultValue)
|
|
142
|
-
let defaultValue = defaultFilterDropdown?.value || filterDropdowns[0].value
|
|
143
|
-
changeFilterActive(sharedFilterIndex, defaultValue)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const loadAPIFilters = async () => {
|
|
148
|
-
if (state.config?.dashboard?.sharedFilters) {
|
|
149
|
-
const sharedAPIFilters = state.config.dashboard.sharedFilters.filter(f => f.apiFilter)
|
|
150
|
-
const loadingFilterMemo: APIFilterDropdowns = sharedAPIFilters.reduce((acc, curr) => {
|
|
151
|
-
const _key = getApiFilterKey(curr.apiFilter!)
|
|
152
|
-
if (apiFilterDropdowns[_key] != null) return acc // don't overwrite fetched data.
|
|
153
|
-
acc[_key] = null
|
|
154
|
-
return acc
|
|
155
|
-
}, {})
|
|
156
|
-
setAPIFilterDropdowns({ ...apiFilterDropdowns, ...loadingFilterMemo })
|
|
157
|
-
const filterLookup = new Map(sharedAPIFilters.map(filter => [getApiFilterKey(filter.apiFilter!), filter.apiFilter!]))
|
|
158
|
-
const getParentParams = (childFilter: SharedFilter): Record<'key' | 'value', string>[] | null => {
|
|
159
|
-
const _parents = sharedAPIFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
|
|
160
|
-
if (!_parents.length) return null
|
|
161
|
-
return _parents.map(({ queryParameter, active }) => ({ key: queryParameter || '', value: active || '' }))
|
|
162
|
-
}
|
|
163
|
-
const getFilterValues = (filterData: Object | Array<Object>, apiFilter: APIFilter): DropdownOptions => {
|
|
164
|
-
const { textSelector, valueSelector, heirarchyLookup } = apiFilter
|
|
165
|
-
if (heirarchyLookup) {
|
|
166
|
-
const heirarchy = heirarchyLookup!.split('.')
|
|
167
|
-
const selector = heirarchy.shift() // pop first element
|
|
168
|
-
return getFilterValues(selector ? filterData[selector] : filterData, { ...apiFilter, heirarchyLookup: heirarchy.join('.') })
|
|
169
|
-
}
|
|
170
|
-
if (!Array.isArray(filterData)) throw new Error('the filter data has requires a heirarchy path to access the filter values, This should be in the format key.subkey.subsubkey')
|
|
171
|
-
return filterData.map(v => ({ text: v[textSelector], value: v[valueSelector] }))
|
|
172
|
-
}
|
|
173
|
-
state.config.dashboard.sharedFilters.forEach(async (filter, index) => {
|
|
174
|
-
if (!filter.apiFilter) return
|
|
175
|
-
const baseEndpoint = filter.apiFilter.apiEndpoint
|
|
176
|
-
const _key = getApiFilterKey(filter.apiFilter)
|
|
177
|
-
const params = getParentParams(filter)
|
|
178
|
-
const notAllParentsSelected = params?.some(({ value }) => value === '')
|
|
179
|
-
if (notAllParentsSelected) return // don't send request for dependent children filter options
|
|
180
|
-
if (apiFilterDropdowns[_key] && !params && filter.filterBy === 'Query String') return // don't reload filter unless it's a child
|
|
181
|
-
const endpoint = baseEndpoint + (params ? gatherQueryParams(params) : '')
|
|
182
|
-
fetch(endpoint)
|
|
183
|
-
.then(resp => resp.json())
|
|
184
|
-
.then(data => {
|
|
185
|
-
const apiFilter = filterLookup.get(_key) as APIFilter
|
|
186
|
-
const _filterValues = getFilterValues(data, apiFilter)
|
|
187
|
-
setAPIFilterDropdowns(dropdowns => ({ ...dropdowns, [_key]: _filterValues }))
|
|
188
|
-
setAutoLoadDefaultValue(index, _filterValues)
|
|
189
|
-
})
|
|
190
|
-
})
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const reloadURLData = async () => {
|
|
195
|
-
const { config } = state
|
|
196
|
-
if (config && config.datasets) {
|
|
197
|
-
let newData = { ...state.data }
|
|
198
|
-
let newDatasets = { ...config.datasets }
|
|
199
|
-
let datasetsNeedsUpdate = false
|
|
200
|
-
let datasetKeys = Object.keys(config.datasets)
|
|
201
|
-
let newFileName = ''
|
|
202
|
-
|
|
203
|
-
for (let i = 0; i < datasetKeys.length; i++) {
|
|
204
|
-
const datasetKey = datasetKeys[i]
|
|
205
|
-
const dataset = config.datasets[datasetKey]
|
|
206
|
-
if (dataset.dataUrl && config.dashboard && config.dashboard.sharedFilters) {
|
|
207
|
-
const dataUrl = new URL(dataset.runtimeDataUrl || dataset.dataUrl, window.location.origin)
|
|
208
|
-
let currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
|
|
209
|
-
let updatedQSParams = {}
|
|
210
|
-
|
|
211
|
-
let isUpdateNeeded = false
|
|
212
|
-
|
|
213
|
-
config.dashboard.sharedFilters.forEach(filter => {
|
|
214
|
-
if (filter.filterBy === 'File Name') {
|
|
215
|
-
// if no file name is entered use the default active filter. ie. /activeFilter.json
|
|
216
|
-
if (!filter.fileName && filter.datasetKey === datasetKey) newFileName = filter.active
|
|
217
|
-
// if a file name is found, ie, state_${query}, use that, ie. state_activeFilter.json
|
|
218
|
-
if (filter.datasetKey === datasetKey && filter.fileName) newFileName = capitalizeSplitAndJoin.call(String(filter.fileName), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces'])
|
|
219
|
-
if (newFileName && newFileName.includes('${query}')) {
|
|
220
|
-
newFileName = newFileName.replace('${query}', capitalizeSplitAndJoin.call(String(filter.active), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces']))
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (filter.type === 'urlfilter' && !!filter.queryParameter) {
|
|
225
|
-
if (updatedQSParams[filter.queryParameter]) {
|
|
226
|
-
updatedQSParams[filter.queryParameter] = updatedQSParams[filter.queryParameter] + filter.active
|
|
227
|
-
} else {
|
|
228
|
-
updatedQSParams[filter.queryParameter] = filter.active
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (filter.filterBy === 'File Name') {
|
|
232
|
-
isUpdateNeeded = true
|
|
233
|
-
}
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
Object.keys(updatedQSParams).forEach(updatedParam => {
|
|
237
|
-
if (decodeURIComponent(updatedQSParams[updatedParam]) !== currentQSParams[updatedParam]) {
|
|
238
|
-
isUpdateNeeded = true
|
|
239
|
-
}
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
if (!isUpdateNeeded) return
|
|
243
|
-
|
|
244
|
-
Object.keys(currentQSParams).forEach(currentParam => {
|
|
245
|
-
if (!updatedQSParams[currentParam]) {
|
|
246
|
-
updatedQSParams[currentParam] = currentQSParams[currentParam]
|
|
247
|
-
}
|
|
248
|
-
})
|
|
249
|
-
const _params = Object.keys(updatedQSParams).map(key => ({ key, value: updatedQSParams[key] }))
|
|
250
|
-
let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${gatherQueryParams(_params)}`
|
|
251
|
-
|
|
252
|
-
if (newFileName !== '') {
|
|
253
|
-
let fileExtension = dataUrl.pathname.split('.').pop()
|
|
254
|
-
let pathWithoutFilename = dataUrl.pathname.substring(0, dataUrl.pathname.lastIndexOf('/'))
|
|
255
|
-
dataUrlFinal = `${dataUrl.origin}${pathWithoutFilename}/${newFileName}.${fileExtension}${gatherQueryParams(_params)}`
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
let newDataset = await fetchRemoteData(`${dataUrlFinal}`)
|
|
259
|
-
|
|
260
|
-
if (newDataset && dataset.dataDescription) {
|
|
261
|
-
try {
|
|
262
|
-
newDataset = transform.autoStandardize(newDataset)
|
|
263
|
-
newDataset = transform.developerStandardize(newDataset, dataset.dataDescription)
|
|
264
|
-
} catch (e) {
|
|
265
|
-
//Data not able to be standardized, leave as is
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
|
|
270
|
-
newData[datasetKey] = newDataset
|
|
271
|
-
datasetsNeedsUpdate = true
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (datasetsNeedsUpdate) {
|
|
276
|
-
dispatch({ type: 'SET_DATA', payload: newData })
|
|
277
|
-
|
|
278
|
-
let newFilteredData = {}
|
|
279
|
-
let newConfig = { ...config }
|
|
280
|
-
getVizKeys(config).forEach(key => {
|
|
281
|
-
let dataKey = config.visualizations[key].dataKey
|
|
282
|
-
|
|
283
|
-
let applicableFilters = config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(key) !== -1)
|
|
284
|
-
if (applicableFilters.length > 0) {
|
|
285
|
-
newFilteredData[key] = filterData(applicableFilters, newData[dataKey], state.config?.filterBehavior)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (newData[dataKey]) {
|
|
289
|
-
newConfig.visualizations[key].formattedData = newData[dataKey]
|
|
290
|
-
}
|
|
291
|
-
})
|
|
292
|
-
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
293
|
-
newConfig.datasets = newDatasets
|
|
294
|
-
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const loadConfig = async () => {
|
|
300
|
-
dispatch({ type: 'SET_LOADING', payload: true })
|
|
301
|
-
let response: Config = configObj || (await (await fetch(configUrl)).json())
|
|
302
|
-
let newConfig = { ...defaults, ...response }
|
|
303
|
-
let datasets = {}
|
|
304
|
-
|
|
305
|
-
if (response.datasets) {
|
|
306
|
-
await Promise.all(
|
|
307
|
-
Object.keys(response.datasets).map(async key => {
|
|
308
|
-
datasets[key] = await processData(response.datasets[key], response.filterBehavior)
|
|
309
|
-
})
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
getVizKeys(newConfig).forEach(vizKey => {
|
|
313
|
-
newConfig.visualizations[vizKey].formattedData = datasets[newConfig.visualizations[vizKey].dataKey]
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
Object.keys(datasets).forEach(key => {
|
|
317
|
-
newConfig.datasets[key].data = datasets[key]
|
|
318
|
-
})
|
|
319
|
-
} else {
|
|
320
|
-
let dataKey = newConfig.dataFileName || 'backwards-compatibility'
|
|
321
|
-
datasets[dataKey] = await processDataLegacy(response)
|
|
322
|
-
|
|
323
|
-
let datasetsFull = {}
|
|
324
|
-
datasetsFull[dataKey] = {
|
|
325
|
-
data: datasets[dataKey],
|
|
326
|
-
dataDescription: newConfig.dataDescription
|
|
327
|
-
}
|
|
328
|
-
newConfig.datasets = datasetsFull
|
|
329
|
-
|
|
330
|
-
getVizKeys(newConfig).forEach(vizKey => {
|
|
331
|
-
newConfig.visualizations[vizKey].dataKey = dataKey
|
|
332
|
-
newConfig.visualizations[vizKey].dataDescription = newConfig.dataDescription
|
|
333
|
-
newConfig.visualizations[vizKey].formattedData = newConfig.formattedData
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
newConfig.data = []
|
|
337
|
-
newConfig.dataUrl = ''
|
|
338
|
-
newConfig.dataFileName = ''
|
|
339
|
-
newConfig.dataFileSourceType = ''
|
|
340
|
-
newConfig.dataDescription = {}
|
|
341
|
-
newConfig.formattedData = []
|
|
342
|
-
|
|
343
|
-
if (newConfig.dashboard && newConfig.dashboard.filters) {
|
|
344
|
-
newConfig.dashboard.sharedFilters = newConfig.dashboard.sharedFilters || []
|
|
345
|
-
newConfig.dashboard.filters.forEach(filter => {
|
|
346
|
-
newConfig.dashboard.sharedFilters.push({ ...filter, key: filter.label, showDropdown: true, usedBy: getVizKeys(newConfig) })
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
newConfig.dashboard.filters = undefined
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
dispatch({ type: 'SET_DATA', payload: datasets })
|
|
354
|
-
dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig, datasets] })
|
|
355
|
-
dispatch({ type: 'SET_LOADING', payload: false })
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter) => {
|
|
359
|
-
if (!sharedFilter.parents?.length) {
|
|
360
|
-
return 1
|
|
361
|
-
} else {
|
|
362
|
-
let parent = filters.find(filter => sharedFilter.parents!.includes(filter.key))
|
|
363
|
-
if (!parent) return 1
|
|
364
|
-
return 1 + findFilterTier(filters, parent)
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const setSharedFilter = (key, datum) => {
|
|
369
|
-
const { config } = state
|
|
370
|
-
if (!config) return
|
|
371
|
-
let newConfig = { ...config }
|
|
372
|
-
let newFilteredData = { ...state.filteredData }
|
|
373
|
-
for (let i = 0; i < newConfig.dashboard.sharedFilters.length; i++) {
|
|
374
|
-
const filter = newConfig.dashboard.sharedFilters[i]
|
|
375
|
-
if (filter.setBy === key) {
|
|
376
|
-
if (!!filter.columnName) {
|
|
377
|
-
filter.active = datum[filter.columnName]
|
|
378
|
-
}
|
|
379
|
-
break
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
getVizKeys(newConfig).forEach(visualizationKey => {
|
|
384
|
-
let applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1)
|
|
385
|
-
|
|
386
|
-
if (applicableFilters.length > 0) {
|
|
387
|
-
const visualization = newConfig.visualizations[visualizationKey]
|
|
388
|
-
|
|
389
|
-
const formattedData = visualization.dataDescription ? getFormattedData(state.data[visualization.dataKey] || visualization.data, visualization.dataDescription) : undefined
|
|
390
|
-
|
|
391
|
-
newFilteredData[visualizationKey] = filterData(applicableFilters, formattedData || state.data[visualization.dataKey], state.config?.filterBehavior)
|
|
392
|
-
}
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
396
|
-
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Load data when component first mounts
|
|
400
|
-
useEffect(() => {
|
|
401
|
-
loadConfig()
|
|
402
|
-
}, [])
|
|
403
|
-
|
|
404
|
-
// Pass up to <CdcEditor /> if it exists when config state changes
|
|
405
|
-
useEffect(() => {
|
|
406
|
-
if (setParentConfig && isEditor) {
|
|
407
|
-
setParentConfig(state.config)
|
|
408
|
-
}
|
|
409
|
-
}, [state.config])
|
|
410
|
-
|
|
411
|
-
useEffect(() => {
|
|
412
|
-
const { config } = state
|
|
413
|
-
if (config && config.filterBehavior !== FilterBehavior.Apply) {
|
|
414
|
-
reloadURLData()
|
|
415
|
-
}
|
|
416
|
-
loadAPIFilters()
|
|
417
|
-
}, [JSON.stringify(state.config?.dashboard ? state.config.dashboard.sharedFilters : undefined)])
|
|
418
|
-
|
|
419
|
-
const updateChildConfig = (visualizationKey, newConfig) => {
|
|
420
|
-
const { config } = state
|
|
421
|
-
if (!config) return
|
|
422
|
-
let updatedConfig = { ...config }
|
|
423
|
-
|
|
424
|
-
updatedConfig.visualizations[visualizationKey] = newConfig
|
|
425
|
-
updatedConfig.visualizations[visualizationKey].formattedData = config.visualizations[visualizationKey].formattedData
|
|
426
|
-
|
|
427
|
-
dispatch({ type: 'SET_CONFIG', payload: config })
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const applyFilters = () => {
|
|
431
|
-
if (!state.config) return
|
|
432
|
-
const allFiltersSelected = !state.config.dashboard.sharedFilters.some(filter => !filter.active)
|
|
433
|
-
if (allFiltersSelected) {
|
|
434
|
-
reloadURLData()
|
|
435
|
-
} else {
|
|
436
|
-
// TODO noftify of required fields
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const changeFilterActive = (index: number, value: string) => {
|
|
441
|
-
const { config } = state
|
|
442
|
-
if (!config) return
|
|
443
|
-
let dashboardConfig = { ...config.dashboard }
|
|
444
|
-
|
|
445
|
-
dashboardConfig.sharedFilters[index].active = value
|
|
446
|
-
|
|
447
|
-
dispatch({ type: 'SET_CONFIG', payload: { ...config, dashboard: dashboardConfig } })
|
|
448
|
-
if (config.filterBehavior !== FilterBehavior.Apply) {
|
|
449
|
-
let newFilteredData = {}
|
|
450
|
-
getVizKeys(config).forEach(key => {
|
|
451
|
-
let applicableFilters = dashboardConfig.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(key) !== -1)
|
|
452
|
-
if (applicableFilters.length > 0) {
|
|
453
|
-
const visualization = config.visualizations[key]
|
|
454
|
-
const _data = state.data[visualization.dataKey] || visualization.data
|
|
455
|
-
const formattedData = visualization.dataDescription ? getFormattedData(_data, visualization.dataDescription) : _data
|
|
456
|
-
|
|
457
|
-
newFilteredData[key] = filterData(applicableFilters, formattedData, config.filterBehavior)
|
|
458
|
-
}
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
const { active, resetLabel } = dashboardConfig.sharedFilters[index]
|
|
462
|
-
const _filteredData = active === resetLabel ? state.data : newFilteredData
|
|
463
|
-
dispatch({ type: 'SET_FILTERED_DATA', payload: _filteredData })
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const handleOnChange = (index: number, value: string) => {
|
|
468
|
-
const { config } = state
|
|
469
|
-
if (!config) return
|
|
470
|
-
changeFilterActive(index, value)
|
|
471
|
-
if (config.filterBehavior === FilterBehavior.Apply) {
|
|
472
|
-
const autoLoadViz = getAutoLoadVisualization()
|
|
473
|
-
if (!autoLoadViz) return // nothing left to do for regular filter behavior.
|
|
474
|
-
const isAutoSelectFilter = !autoLoadViz.hide.includes(index)
|
|
475
|
-
const missingFilterSelections = config.dashboard.sharedFilters.some(f => !f.active)
|
|
476
|
-
if (isAutoSelectFilter && !missingFilterSelections) {
|
|
477
|
-
// a dropdown has been selected that doesn't
|
|
478
|
-
// require the Go Button
|
|
479
|
-
reloadURLData()
|
|
480
|
-
} else {
|
|
481
|
-
// A parent filter was selected, reset filters by:
|
|
482
|
-
// set auto select filter dropdowns to null
|
|
483
|
-
const autoSelectFilters = config.dashboard.sharedFilters.filter((_, _index) => !autoLoadViz?.hide.includes(_index))
|
|
484
|
-
const dropdownFilterKeys = autoSelectFilters.map(filter => getApiFilterKey(filter.apiFilter!))
|
|
485
|
-
const newApiDropdowns = { ...apiFilterDropdowns }
|
|
486
|
-
dropdownFilterKeys.forEach(key => (newApiDropdowns[key] = null))
|
|
487
|
-
setAPIFilterDropdowns(newApiDropdowns)
|
|
488
|
-
// remove active from sharedFilters that are autoLoading
|
|
489
|
-
const dashboardConfig = { ...config.dashboard }
|
|
490
|
-
dashboardConfig.sharedFilters[index].active = value
|
|
491
|
-
const newSharedFilters = config.dashboard.sharedFilters.map((filter, _index) => {
|
|
492
|
-
const _isAutoSelectFilter = !autoLoadViz?.hide.includes(_index)
|
|
493
|
-
if (_isAutoSelectFilter) filter.active = ''
|
|
494
|
-
return filter
|
|
495
|
-
})
|
|
496
|
-
const _newConfig = { ...config, dashboard: { ...config.dashboard, sharedFilters: newSharedFilters } }
|
|
497
|
-
dispatch({ type: 'SET_CONFIG', payload: _newConfig })
|
|
498
|
-
// setData to empty object because we no longer have a data state.
|
|
499
|
-
dispatch({ type: 'SET_DATA', payload: {} })
|
|
500
|
-
dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const Filters = ({ hide, autoLoad }: { hide?: number[]; autoLoad?: boolean }) => {
|
|
506
|
-
const { config } = state
|
|
507
|
-
if (!config) return <></>
|
|
508
|
-
const isLegacyFilter = !config.filterBehavior
|
|
509
|
-
const isAutoLoadRow = config.filterBehavior === FilterBehavior.Apply && autoLoad
|
|
510
|
-
return (
|
|
511
|
-
<>
|
|
512
|
-
{config.dashboard.sharedFilters.map((singleFilter, filterIndex) => {
|
|
513
|
-
if ((singleFilter.type !== 'urlfilter' && !singleFilter.showDropdown) || (hide && hide.indexOf(filterIndex) !== -1)) return <></>
|
|
514
|
-
const values: JSX.Element[] = []
|
|
515
|
-
if (singleFilter.resetLabel) {
|
|
516
|
-
values.push(
|
|
517
|
-
<option key={`${singleFilter.resetLabel}-option`} value={singleFilter.resetLabel}>
|
|
518
|
-
{singleFilter.resetLabel}
|
|
519
|
-
</option>
|
|
520
|
-
)
|
|
521
|
-
}
|
|
522
|
-
const _key = singleFilter.apiFilter ? getApiFilterKey(singleFilter.apiFilter) : undefined
|
|
523
|
-
if (_key && apiFilterDropdowns[_key]) {
|
|
524
|
-
// URL Filter
|
|
525
|
-
apiFilterDropdowns[_key]!.forEach(({ text, value }, index) => {
|
|
526
|
-
values.push(
|
|
527
|
-
<option key={`${value}-option-${index}`} value={value}>
|
|
528
|
-
{text}
|
|
529
|
-
</option>
|
|
530
|
-
)
|
|
531
|
-
})
|
|
532
|
-
} else {
|
|
533
|
-
// Data Filter
|
|
534
|
-
singleFilter.values?.forEach((filterOption, index) => {
|
|
535
|
-
const labeledOpt = singleFilter.labels && singleFilter.labels[filterOption]
|
|
536
|
-
values.push(
|
|
537
|
-
<option key={`${singleFilter.key}-option-${index}`} value={filterOption}>
|
|
538
|
-
{labeledOpt || filterOption}
|
|
539
|
-
</option>
|
|
540
|
-
)
|
|
541
|
-
})
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return (
|
|
545
|
-
<div className='cove-dashboard-filters' key={`${singleFilter.key}-filtersection-${filterIndex}`}>
|
|
546
|
-
<section className='dashboard-filters-section'>
|
|
547
|
-
<label htmlFor={`filter-${filterIndex}`}>{singleFilter.key}</label>
|
|
548
|
-
<select
|
|
549
|
-
id={`filter-${filterIndex}`}
|
|
550
|
-
className='filter-select'
|
|
551
|
-
data-index='0'
|
|
552
|
-
value={singleFilter.active}
|
|
553
|
-
onChange={val => {
|
|
554
|
-
handleOnChange(filterIndex, val.target.value)
|
|
555
|
-
}}
|
|
556
|
-
>
|
|
557
|
-
{values}
|
|
558
|
-
</select>
|
|
559
|
-
</section>
|
|
560
|
-
</div>
|
|
561
|
-
)
|
|
562
|
-
})}
|
|
563
|
-
|
|
564
|
-
{!isLegacyFilter && !isAutoLoadRow && <button onClick={applyFilters}>GO!</button>}
|
|
565
|
-
</>
|
|
566
|
-
)
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const resizeObserver = new ResizeObserver(entries => {
|
|
570
|
-
for (let entry of entries) {
|
|
571
|
-
let newViewport = getViewport(entry.contentRect.width)
|
|
572
|
-
|
|
573
|
-
setCurrentViewport(newViewport)
|
|
574
|
-
}
|
|
575
|
-
})
|
|
576
|
-
|
|
577
|
-
const outerContainerRef = useCallback(node => {
|
|
578
|
-
if (node !== null) {
|
|
579
|
-
resizeObserver.observe(node)
|
|
580
|
-
}
|
|
581
|
-
}, [])
|
|
582
|
-
|
|
583
|
-
const setPreview = shouldPreview => dispatch({ type: 'SET_PREVIEW', payload: shouldPreview })
|
|
584
|
-
|
|
585
|
-
// Prevent render if loading
|
|
586
|
-
if (state.loading || !state.config) return <Loading />
|
|
587
|
-
|
|
588
|
-
let body: JSX.Element | null = null
|
|
589
|
-
// Editor mode
|
|
590
|
-
if (isEditor && !state.preview) {
|
|
591
|
-
let subVisualizationEditing = false
|
|
592
|
-
|
|
593
|
-
getVizKeys(state.config).forEach(visualizationKey => {
|
|
594
|
-
let visualizationConfig = { ...state.config?.visualizations[visualizationKey] }
|
|
595
|
-
|
|
596
|
-
const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
|
|
597
|
-
|
|
598
|
-
if (state.filteredData && state.filteredData[visualizationKey]) {
|
|
599
|
-
visualizationConfig.data = state.filteredData[visualizationKey]
|
|
600
|
-
if (visualizationConfig.formattedData) {
|
|
601
|
-
visualizationConfig.originalFormattedData = visualizationConfig.formattedData
|
|
602
|
-
visualizationConfig.formattedData = visualizationConfig.data
|
|
603
|
-
}
|
|
604
|
-
} else {
|
|
605
|
-
visualizationConfig.data = state.data[dataKey]
|
|
606
|
-
if (visualizationConfig.formattedData) {
|
|
607
|
-
visualizationConfig.originalFormattedData = visualizationConfig.formattedData
|
|
608
|
-
visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const setsSharedFilter = state.config?.dashboard.sharedFilters && state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey).length > 0
|
|
613
|
-
const setSharedFilterValue = setsSharedFilter ? state.config?.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey)[0].active : undefined
|
|
614
|
-
|
|
615
|
-
if (visualizationConfig.editing) {
|
|
616
|
-
subVisualizationEditing = true
|
|
617
|
-
|
|
618
|
-
const _updateConfig = newConfig => {
|
|
619
|
-
let dataCorrectedConfig = visualizationConfig.originalFormattedData ? { ...newConfig, formattedData: visualizationConfig.originalFormattedData } : newConfig
|
|
620
|
-
updateChildConfig(visualizationKey, dataCorrectedConfig)
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
switch (visualizationConfig.type) {
|
|
624
|
-
case 'chart':
|
|
625
|
-
body = (
|
|
626
|
-
<>
|
|
627
|
-
<Header visualizationKey={visualizationKey} subEditor='Chart' />
|
|
628
|
-
<CdcChart
|
|
629
|
-
key={visualizationKey}
|
|
630
|
-
config={visualizationConfig}
|
|
631
|
-
isEditor={true}
|
|
632
|
-
isDebug={isDebug}
|
|
633
|
-
setConfig={_updateConfig}
|
|
634
|
-
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
635
|
-
setSharedFilterValue={setSharedFilterValue}
|
|
636
|
-
dashboardConfig={state.config}
|
|
637
|
-
isDashboard={true}
|
|
638
|
-
configUrl={undefined}
|
|
639
|
-
setEditing={undefined}
|
|
640
|
-
hostname={undefined}
|
|
641
|
-
link={undefined}
|
|
642
|
-
/>
|
|
643
|
-
</>
|
|
644
|
-
)
|
|
645
|
-
break
|
|
646
|
-
case 'map':
|
|
647
|
-
body = (
|
|
648
|
-
<>
|
|
649
|
-
<Header visualizationKey={visualizationKey} subEditor='Map' />
|
|
650
|
-
<CdcMap
|
|
651
|
-
key={visualizationKey}
|
|
652
|
-
config={visualizationConfig}
|
|
653
|
-
isEditor={true}
|
|
654
|
-
isDebug={isDebug}
|
|
655
|
-
setConfig={_updateConfig}
|
|
656
|
-
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
657
|
-
setSharedFilterValue={setSharedFilterValue}
|
|
658
|
-
isDashboard={true}
|
|
659
|
-
showLoader={false}
|
|
660
|
-
dashboardConfig={state.config}
|
|
661
|
-
/>
|
|
662
|
-
</>
|
|
663
|
-
)
|
|
664
|
-
break
|
|
665
|
-
case 'data-bite':
|
|
666
|
-
visualizationConfig = { ...visualizationConfig, newViz: true }
|
|
667
|
-
body = (
|
|
668
|
-
<>
|
|
669
|
-
<Header visualizationKey={visualizationKey} subEditor='Data Bite' />
|
|
670
|
-
<CdcDataBite key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} />
|
|
671
|
-
</>
|
|
672
|
-
)
|
|
673
|
-
break
|
|
674
|
-
case 'waffle-chart':
|
|
675
|
-
body = (
|
|
676
|
-
<>
|
|
677
|
-
<Header visualizationKey={visualizationKey} subEditor='Waffle Chart' />
|
|
678
|
-
<CdcWaffleChart key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
|
|
679
|
-
</>
|
|
680
|
-
)
|
|
681
|
-
break
|
|
682
|
-
case 'markup-include':
|
|
683
|
-
body = (
|
|
684
|
-
<>
|
|
685
|
-
<Header visualizationKey={visualizationKey} subEditor='Markup Include' />
|
|
686
|
-
<CdcMarkupInclude key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
|
|
687
|
-
</>
|
|
688
|
-
)
|
|
689
|
-
break
|
|
690
|
-
case 'filtered-text':
|
|
691
|
-
body = (
|
|
692
|
-
<>
|
|
693
|
-
<Header visualizationKey={visualizationKey} subEditor='Filtered Text' />
|
|
694
|
-
<CdcFilteredText key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
|
|
695
|
-
</>
|
|
696
|
-
)
|
|
697
|
-
break
|
|
698
|
-
case 'filter-dropdowns':
|
|
699
|
-
const hideFilter = visualizationConfig.autoLoad && inNoDataState
|
|
700
|
-
body = !hideFilter ? (
|
|
701
|
-
<>
|
|
702
|
-
<Header visualizationKey={visualizationKey} subEditor='Filter Dropdowns' />
|
|
703
|
-
<Filters hide={visualizationConfig.hide} autoLoad={visualizationConfig.autoLoad} />
|
|
704
|
-
</>
|
|
705
|
-
) : (
|
|
706
|
-
<></>
|
|
707
|
-
)
|
|
708
|
-
break
|
|
709
|
-
default:
|
|
710
|
-
body = <></>
|
|
711
|
-
break
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
if (!subVisualizationEditing) {
|
|
717
|
-
body = (
|
|
718
|
-
<DndProvider backend={HTML5Backend}>
|
|
719
|
-
<Header setPreview={setPreview} />
|
|
720
|
-
<div className='layout-container'>
|
|
721
|
-
<VisualizationsPanel loadConfig={loadConfig} config={state.config} />
|
|
722
|
-
<Grid />
|
|
723
|
-
</div>
|
|
724
|
-
</DndProvider>
|
|
725
|
-
)
|
|
726
|
-
}
|
|
727
|
-
} else {
|
|
728
|
-
const { config } = state
|
|
729
|
-
const { title, description } = config?.dashboard || {}
|
|
730
|
-
body = (
|
|
731
|
-
<>
|
|
732
|
-
{isEditor && <Header setPreview={setPreview} />}
|
|
733
|
-
<div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''}`}>
|
|
734
|
-
<Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
|
|
735
|
-
{/* Description */}
|
|
736
|
-
{description && <div className='subtext'>{parse(description)}</div>}
|
|
737
|
-
{/* Filters */}
|
|
738
|
-
{config.dashboard.sharedFilters && Object.values(config?.visualizations || {}).filter(viz => viz.visualizationType === 'filter-dropdowns').length === 0 && <Filters hide={undefined} autoLoad={undefined} />}
|
|
739
|
-
|
|
740
|
-
{/* Visualizations */}
|
|
741
|
-
{config.rows &&
|
|
742
|
-
config.rows
|
|
743
|
-
.filter(row => row.filter(col => col.widget).length !== 0)
|
|
744
|
-
.map((row, index) => {
|
|
745
|
-
return (
|
|
746
|
-
<div className={`dashboard-row ${row.equalHeight ? 'equal-height' : ''}`} key={`row__${index}`}>
|
|
747
|
-
{row.map((col, colIndex) => {
|
|
748
|
-
if (col.width) {
|
|
749
|
-
if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`dashboard-col dashboard-col-${col.width}`}></div>
|
|
750
|
-
|
|
751
|
-
let visualizationConfig = { ...config.visualizations[col.widget] }
|
|
752
|
-
|
|
753
|
-
const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
|
|
754
|
-
|
|
755
|
-
if (state.filteredData && state.filteredData[col.widget]) {
|
|
756
|
-
visualizationConfig.data = state.filteredData[col.widget]
|
|
757
|
-
if (visualizationConfig.formattedData) {
|
|
758
|
-
visualizationConfig.originalFormattedData = visualizationConfig.formattedData
|
|
759
|
-
visualizationConfig.formattedData = visualizationConfig.data
|
|
760
|
-
}
|
|
761
|
-
} else {
|
|
762
|
-
visualizationConfig.data = state.data[dataKey]
|
|
763
|
-
if (visualizationConfig.formattedData) {
|
|
764
|
-
visualizationConfig.originalFormattedData = visualizationConfig.formattedData
|
|
765
|
-
visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const setsSharedFilter = config.dashboard.sharedFilters && config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget).length > 0
|
|
770
|
-
const setSharedFilterValue = setsSharedFilter ? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget)[0].active : undefined
|
|
771
|
-
const tableLink = (
|
|
772
|
-
<a href={`#data-table-${visualizationConfig.dataKey}`} className='margin-left-href'>
|
|
773
|
-
{visualizationConfig.dataKey} (Go to Table)
|
|
774
|
-
</a>
|
|
775
|
-
)
|
|
776
|
-
const hideFilter = visualizationConfig.autoLoad && inNoDataState
|
|
777
|
-
return (
|
|
778
|
-
<React.Fragment key={`vis__${index}__${colIndex}`}>
|
|
779
|
-
<div className={`dashboard-col dashboard-col-${col.width}`}>
|
|
780
|
-
{visualizationConfig.type === 'chart' && (
|
|
781
|
-
<CdcChart
|
|
782
|
-
key={col.widget}
|
|
783
|
-
config={visualizationConfig}
|
|
784
|
-
dashboardConfig={config}
|
|
785
|
-
isEditor={false}
|
|
786
|
-
setConfig={newConfig => {
|
|
787
|
-
updateChildConfig(col.widget, newConfig)
|
|
788
|
-
}}
|
|
789
|
-
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
790
|
-
isDashboard={true}
|
|
791
|
-
link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
|
|
792
|
-
configUrl={undefined}
|
|
793
|
-
setEditing={undefined}
|
|
794
|
-
hostname={undefined}
|
|
795
|
-
setSharedFilterValue={undefined}
|
|
796
|
-
/>
|
|
797
|
-
)}
|
|
798
|
-
{visualizationConfig.type === 'map' && (
|
|
799
|
-
<CdcMap
|
|
800
|
-
key={col.widget}
|
|
801
|
-
config={visualizationConfig}
|
|
802
|
-
isEditor={false}
|
|
803
|
-
setConfig={newConfig => {
|
|
804
|
-
updateChildConfig(col.widget, newConfig)
|
|
805
|
-
}}
|
|
806
|
-
showLoader={false}
|
|
807
|
-
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
808
|
-
setSharedFilterValue={setSharedFilterValue}
|
|
809
|
-
isDashboard={true}
|
|
810
|
-
link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
|
|
811
|
-
/>
|
|
812
|
-
)}
|
|
813
|
-
{visualizationConfig.type === 'data-bite' && (
|
|
814
|
-
<CdcDataBite
|
|
815
|
-
key={col.widget}
|
|
816
|
-
config={visualizationConfig}
|
|
817
|
-
isEditor={false}
|
|
818
|
-
setConfig={newConfig => {
|
|
819
|
-
updateChildConfig(col.widget, newConfig)
|
|
820
|
-
}}
|
|
821
|
-
isDashboard={true}
|
|
822
|
-
/>
|
|
823
|
-
)}
|
|
824
|
-
{visualizationConfig.type === 'waffle-chart' && (
|
|
825
|
-
<CdcWaffleChart
|
|
826
|
-
key={col.widget}
|
|
827
|
-
config={visualizationConfig}
|
|
828
|
-
isEditor={false}
|
|
829
|
-
setConfig={newConfig => {
|
|
830
|
-
updateChildConfig(col.widget, newConfig)
|
|
831
|
-
}}
|
|
832
|
-
isDashboard={true}
|
|
833
|
-
configUrl={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
|
|
834
|
-
/>
|
|
835
|
-
)}
|
|
836
|
-
{visualizationConfig.type === 'markup-include' && (
|
|
837
|
-
<CdcMarkupInclude
|
|
838
|
-
key={col.widget}
|
|
839
|
-
config={visualizationConfig}
|
|
840
|
-
isEditor={false}
|
|
841
|
-
setConfig={newConfig => {
|
|
842
|
-
updateChildConfig(col.widget, newConfig)
|
|
843
|
-
}}
|
|
844
|
-
isDashboard={true}
|
|
845
|
-
configUrl={undefined}
|
|
846
|
-
/>
|
|
847
|
-
)}
|
|
848
|
-
{visualizationConfig.type === 'filtered-text' && (
|
|
849
|
-
<CdcFilteredText
|
|
850
|
-
key={col.widget}
|
|
851
|
-
config={visualizationConfig}
|
|
852
|
-
isEditor={false}
|
|
853
|
-
setConfig={newConfig => {
|
|
854
|
-
updateChildConfig(col.widget, newConfig)
|
|
855
|
-
}}
|
|
856
|
-
isDashboard={true}
|
|
857
|
-
configUrl={undefined}
|
|
858
|
-
/>
|
|
859
|
-
)}
|
|
860
|
-
{visualizationConfig.type === 'filter-dropdowns' && !hideFilter && <Filters hide={visualizationConfig.hide} autoLoad={visualizationConfig.autoLoad} />}
|
|
861
|
-
</div>
|
|
862
|
-
</React.Fragment>
|
|
863
|
-
)
|
|
864
|
-
}
|
|
865
|
-
return <React.Fragment key={`vis__${index}__${colIndex}`}></React.Fragment>
|
|
866
|
-
})}
|
|
867
|
-
</div>
|
|
868
|
-
)
|
|
869
|
-
})}
|
|
870
|
-
|
|
871
|
-
{/* Image or PDF Inserts */}
|
|
872
|
-
<section className='download-buttons'>
|
|
873
|
-
{config.table?.downloadImageButton && <MediaControls.Button title='Download Dashboard as Image' type='image' state={config} text='Download Dashboard Image' elementToCapture={imageId} />}
|
|
874
|
-
{config.table?.downloadPdfButton && <MediaControls.Button title='Download Dashboard as PDF' type='pdf' state={config} text='Download Dashboard PDF' elementToCapture={imageId} />}
|
|
875
|
-
</section>
|
|
876
|
-
|
|
877
|
-
{/* Data Table */}
|
|
878
|
-
{config?.table?.show && config?.data && (
|
|
879
|
-
<DataTable
|
|
880
|
-
config={config}
|
|
881
|
-
rawData={config.data}
|
|
882
|
-
runtimeData={config.data || []}
|
|
883
|
-
expandDataTable={config.table.expanded}
|
|
884
|
-
showDownloadButton={config.table.download}
|
|
885
|
-
tableTitle={config.dashboard.title || ''}
|
|
886
|
-
viewport={currentViewport}
|
|
887
|
-
tabbingId={config.dashboard.title || ''}
|
|
888
|
-
outerContainerRef={outerContainerRef}
|
|
889
|
-
imageRef={imageId}
|
|
890
|
-
isDebug={isDebug}
|
|
891
|
-
isEditor={isEditor}
|
|
892
|
-
/>
|
|
893
|
-
)}
|
|
894
|
-
{config.table?.show &&
|
|
895
|
-
config.datasets &&
|
|
896
|
-
Object.keys(config.datasets).map(datasetKey => {
|
|
897
|
-
//For each dataset, find any shared filters that apply to all visualizations using the dataset
|
|
898
|
-
//Apply these filters to the table
|
|
899
|
-
let filteredTableData
|
|
900
|
-
if (config.dashboard.sharedFilters && config.dashboard.sharedFilters.length > 0) {
|
|
901
|
-
//Gets list of visuailzations using the dataset
|
|
902
|
-
let vizKeysUsingDataset: string[] = []
|
|
903
|
-
getVizKeys(config).forEach(visualizationKey => {
|
|
904
|
-
if (config.visualizations[visualizationKey].dataKey === datasetKey) {
|
|
905
|
-
vizKeysUsingDataset.push(visualizationKey)
|
|
906
|
-
}
|
|
907
|
-
})
|
|
908
|
-
|
|
909
|
-
//Checks shared filters against list to see if all visualizations are represented
|
|
910
|
-
let applicableFilters: SharedFilter[] = []
|
|
911
|
-
config.dashboard.sharedFilters.forEach(sharedFilter => {
|
|
912
|
-
let allMatch = true
|
|
913
|
-
vizKeysUsingDataset.forEach(visualizationKey => {
|
|
914
|
-
if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) === -1) {
|
|
915
|
-
allMatch = false
|
|
916
|
-
}
|
|
917
|
-
})
|
|
918
|
-
if (allMatch) {
|
|
919
|
-
applicableFilters.push(sharedFilter)
|
|
920
|
-
}
|
|
921
|
-
})
|
|
922
|
-
|
|
923
|
-
//Applys any applicable filters
|
|
924
|
-
if (applicableFilters.length > 0) {
|
|
925
|
-
filteredTableData = filterData(applicableFilters, config.datasets[datasetKey].data, state.config?.filterBehavior)
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
return (
|
|
930
|
-
<div className='multi-table-container' id={`data-table-${datasetKey}`} key={`data-table-${datasetKey}`}>
|
|
931
|
-
<DataTable
|
|
932
|
-
config={config as TableConfig}
|
|
933
|
-
dataConfig={config.datasets[datasetKey]}
|
|
934
|
-
rawData={config.datasets[datasetKey].data}
|
|
935
|
-
runtimeData={filteredTableData || config.datasets[datasetKey].data || []}
|
|
936
|
-
expandDataTable={config.table.expanded}
|
|
937
|
-
tableTitle={datasetKey}
|
|
938
|
-
viewport={currentViewport}
|
|
939
|
-
tabbingId={datasetKey}
|
|
940
|
-
/>
|
|
941
|
-
</div>
|
|
942
|
-
)
|
|
943
|
-
})}
|
|
944
|
-
</div>
|
|
945
|
-
</>
|
|
946
|
-
)
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
const dashboardContainerClasses = ['cdc-open-viz-module', 'type-dashboard', `${currentViewport}`]
|
|
950
|
-
|
|
951
|
-
return (
|
|
952
|
-
<GlobalContextProvider>
|
|
953
|
-
<DashboardContext.Provider value={{ ...state, setParentConfig, outerContainerRef, isDebug }}>
|
|
954
|
-
<DashboardDispatchContext.Provider value={dispatch}>
|
|
955
|
-
<div className={dashboardContainerClasses.join(' ')} ref={outerContainerRef} data-download-id={imageId}>
|
|
956
|
-
{body}
|
|
957
|
-
</div>
|
|
958
|
-
<OverlayFrame />
|
|
959
|
-
</DashboardDispatchContext.Provider>
|
|
960
|
-
</DashboardContext.Provider>
|
|
961
|
-
</GlobalContextProvider>
|
|
962
|
-
)
|
|
963
|
-
}
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import CdcDashboard from './CdcDashboardComponent'
|
|
3
|
+
import { MultiDashboardConfig } from './types/MultiDashboard'
|
|
4
|
+
import Loading from '@cdc/core/components/Loading'
|
|
5
|
+
import defaults from './data/initial-state'
|
|
6
|
+
import { processData } from './helpers/processData'
|
|
7
|
+
import { getVizKeys } from './helpers/getVizKeys'
|
|
8
|
+
import { processDataLegacy } from './helpers/processDataLegacy'
|
|
9
|
+
import { WCMSProps } from '@cdc/core/types/WCMSProps'
|
|
10
|
+
import { initialState } from './DashboardContext'
|
|
11
|
+
import { getUpdateConfig } from './helpers/getUpdateConfig'
|
|
12
|
+
import { InitialState } from './types/InitialState'
|
|
13
|
+
import { DashboardConfig } from './types/DashboardConfig'
|
|
14
|
+
import _ from 'lodash'
|
|
15
|
+
|
|
16
|
+
type MultiDashboardProps = Omit<WCMSProps, 'configUrl'> & {
|
|
17
|
+
configUrl?: string
|
|
18
|
+
config?: MultiDashboardConfig
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({ configUrl, config: editorConfig, isEditor, isDebug }) => {
|
|
22
|
+
const [initial, setInitial] = useState<InitialState>(undefined)
|
|
23
|
+
console.log('multi dashboard wrapper')
|
|
24
|
+
|
|
25
|
+
const getSelectedConfig = (config: MultiDashboardConfig, selectedConfig?: string): number | null => {
|
|
26
|
+
if (!config.multiDashboards) return null
|
|
27
|
+
// TODO: if query parameter select based on query parameter
|
|
28
|
+
if (selectedConfig) {
|
|
29
|
+
const foundConfig = Object.values(config.multiDashboards).findIndex(({ label }) => {
|
|
30
|
+
return label === selectedConfig
|
|
31
|
+
})
|
|
32
|
+
if (foundConfig > -1) return foundConfig
|
|
33
|
+
}
|
|
34
|
+
// else select the first available
|
|
35
|
+
return 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const formatInitialState = (newConfig: MultiDashboardConfig | DashboardConfig, datasets: Record<string, Object[]>) => {
|
|
39
|
+
const [config, filteredData] = getUpdateConfig(initialState)(newConfig, datasets)
|
|
40
|
+
return { ...initialState, config, filteredData, data: datasets }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const loadConfig = async (selectedConfig?: string) => {
|
|
44
|
+
const _config: MultiDashboardConfig = editorConfig || (await (await fetch(configUrl)).json())
|
|
45
|
+
const selected = getSelectedConfig(_config, selectedConfig)
|
|
46
|
+
|
|
47
|
+
const { newConfig, datasets } = selected !== null ? await loadMultiDashboard(_config, selected) : await loadSingleDashboard(_config)
|
|
48
|
+
setInitial(formatInitialState(newConfig, datasets))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
loadConfig()
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
const loadData = async (initialConfig: DashboardConfig | MultiDashboardConfig) => {
|
|
56
|
+
let newConfig = { ...initialConfig }
|
|
57
|
+
let datasets: Record<string, Object[]> = {}
|
|
58
|
+
await Promise.all(
|
|
59
|
+
Object.keys(initialConfig.datasets).map(async key => {
|
|
60
|
+
datasets[key] = await processData(initialConfig.datasets[key], initialConfig.filterBehavior)
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
getVizKeys(newConfig).forEach(vizKey => {
|
|
65
|
+
newConfig.visualizations[vizKey].formattedData = datasets[newConfig.visualizations[vizKey].dataKey]
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
Object.keys(datasets).forEach(key => {
|
|
69
|
+
newConfig.datasets[key].data = datasets[key]
|
|
70
|
+
})
|
|
71
|
+
return { newConfig, datasets }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const loadSingleDashboard = async config => {
|
|
75
|
+
let newConfig = { ...defaults, ...config } as DashboardConfig
|
|
76
|
+
|
|
77
|
+
if (config.datasets) {
|
|
78
|
+
return await loadData(newConfig)
|
|
79
|
+
} else {
|
|
80
|
+
const dataKey = newConfig.dataFileName || 'backwards-compatibility'
|
|
81
|
+
const data = await processDataLegacy(config)
|
|
82
|
+
|
|
83
|
+
const datasetsFull = {}
|
|
84
|
+
datasetsFull[dataKey] = {
|
|
85
|
+
data,
|
|
86
|
+
dataDescription: newConfig.dataDescription
|
|
87
|
+
}
|
|
88
|
+
newConfig.datasets = datasetsFull
|
|
89
|
+
|
|
90
|
+
getVizKeys(newConfig).forEach(vizKey => {
|
|
91
|
+
const newData = { dataKey, ..._.pick(newConfig, 'dataDescription', 'formattedData') }
|
|
92
|
+
newConfig.visualizations[vizKey] = { ...newConfig.visualizations[vizKey], ...newData }
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const blankFields = { data: [], dataUrl: '', dataFileName: '', dataFileSourceType: '', dataDescription: [], formattedData: [] }
|
|
96
|
+
newConfig = { ...newConfig, ...blankFields }
|
|
97
|
+
|
|
98
|
+
if (newConfig.dashboard.filters) {
|
|
99
|
+
const dashboard = { ...newConfig.dashboard }
|
|
100
|
+
// replace filters with sharedFilters
|
|
101
|
+
if (!dashboard.sharedFilters) dashboard.sharedFilters = []
|
|
102
|
+
const filters = dashboard.filters.map(filter => {
|
|
103
|
+
return { ...filter, key: filter.label, showDropdown: true, usedBy: getVizKeys(newConfig) }
|
|
104
|
+
})
|
|
105
|
+
dashboard.sharedFilters = [...dashboard.sharedFilters, ...filters]
|
|
106
|
+
newConfig.dashboard = { ...dashboard, filters: undefined }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const datasets: Record<string, Object[]> = { [dataKey]: data }
|
|
110
|
+
return { newConfig, datasets }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const loadMultiDashboard = async (multiConfig: MultiDashboardConfig, selectedConfig: number) => {
|
|
115
|
+
const selectedDashboard = multiConfig.multiDashboards[selectedConfig]
|
|
116
|
+
let newConfig = { ...defaults, ...multiConfig, ...selectedDashboard, multiDashboards: multiConfig.multiDashboards, activeDashboard: selectedConfig } as MultiDashboardConfig
|
|
117
|
+
return await loadData(newConfig)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!initial) return <Loading />
|
|
121
|
+
return <CdcDashboard isEditor={isEditor} isDebug={isDebug} initialState={initial} />
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default MultiDashboardWrapper
|