@cdc/dashboard 4.24.2 → 4.24.4
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 +128512 -99417
- package/examples/chart-data.json +5409 -0
- package/examples/full-dash-test.json +14643 -0
- package/examples/full-dashboard.json +10036 -0
- package/examples/sankey.json +5218 -0
- package/index.html +4 -3
- package/package.json +11 -10
- package/src/CdcDashboard.tsx +129 -124
- package/src/CdcDashboardComponent.tsx +316 -441
- package/src/DashboardContext.tsx +4 -1
- package/src/_stories/Dashboard.stories.tsx +79 -36
- package/src/_stories/_mock/api-filter-chart.json +11 -35
- package/src/_stories/_mock/api-filter-map.json +17 -31
- package/src/_stories/_mock/dashboard-gallery.json +523 -534
- package/src/_stories/_mock/multi-viz.json +378 -0
- package/src/_stories/_mock/pivot-filter.json +161 -0
- package/src/_stories/_mock/standalone-table.json +122 -0
- package/src/_stories/_mock/toggle-example.json +4035 -0
- package/src/components/DataDesignerModal.tsx +145 -0
- package/src/components/EditorWrapper/EditorWrapper.tsx +52 -0
- package/src/components/EditorWrapper/editor-wrapper.style.css +13 -0
- package/src/components/Filters.tsx +88 -0
- package/src/components/Grid.tsx +3 -1
- package/src/components/Header/FilterModal.tsx +506 -0
- package/src/components/Header/Header.tsx +25 -465
- package/src/components/Row.tsx +65 -29
- package/src/components/Toggle/Toggle.tsx +36 -0
- package/src/components/Toggle/index.tsx +1 -0
- package/src/components/Toggle/toggle-style.css +34 -0
- package/src/components/VisualizationRow.tsx +174 -0
- package/src/components/VisualizationsPanel.tsx +13 -3
- package/src/components/Widget.tsx +28 -126
- package/src/helpers/filterData.ts +75 -50
- package/src/helpers/generateValuesForFilter.ts +2 -12
- package/src/helpers/getApiFilterKey.ts +5 -0
- package/src/helpers/getFilteredData.ts +39 -0
- package/src/helpers/getUpdateConfig.ts +39 -22
- package/src/helpers/getVizConfig.ts +31 -0
- package/src/helpers/getVizRowColumnLocator.ts +9 -0
- package/src/helpers/iconHash.tsx +34 -0
- package/src/helpers/tests/filterData.test.ts +149 -0
- package/src/images/icon-toggle.svg +1 -0
- package/src/scss/grid.scss +10 -3
- package/src/scss/main.scss +11 -0
- package/src/store/dashboard.actions.ts +35 -3
- package/src/store/dashboard.reducer.ts +33 -2
- package/src/types/APIFilter.ts +4 -5
- package/src/types/ConfigRow.ts +13 -2
- package/src/types/DataSet.ts +11 -8
- package/src/types/InitialState.ts +2 -1
- package/src/types/SharedFilter.ts +6 -3
- package/src/types/Tab.ts +1 -0
|
@@ -18,6 +18,7 @@ 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'
|
|
21
22
|
|
|
22
23
|
import CdcMap from '@cdc/map'
|
|
23
24
|
import CdcChart from '@cdc/chart'
|
|
@@ -53,13 +54,17 @@ import { type InitialState } from './types/InitialState'
|
|
|
53
54
|
import MultiTabs from './components/MultiConfigTabs'
|
|
54
55
|
import _ from 'lodash'
|
|
55
56
|
import EditorContext from '../../editor/src/ConfigContext'
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
import { getApiFilterKey } from './helpers/getApiFilterKey'
|
|
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'
|
|
61
|
+
import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
|
|
62
|
+
import { ViewPort } from '@cdc/core/types/ViewPort'
|
|
63
|
+
import VisualizationRow from './components/VisualizationRow'
|
|
64
|
+
import { getVizConfig } from './helpers/getVizConfig'
|
|
65
|
+
import { getApplicableFilters, getFilteredData } from './helpers/getFilteredData'
|
|
66
|
+
import { getVizRowColumnLocator } from './helpers/getVizRowColumnLocator'
|
|
67
|
+
import Layout from '@cdc/core/components/Layout'
|
|
63
68
|
|
|
64
69
|
type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
|
|
65
70
|
initialState: InitialState
|
|
@@ -67,12 +72,12 @@ type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
|
|
|
67
72
|
|
|
68
73
|
export default function CdcDashboard({ initialState, isEditor = false, isDebug = false }: DashboardProps) {
|
|
69
74
|
const [state, dispatch] = useReducer(dashboardReducer, initialState)
|
|
70
|
-
console.log('state', state)
|
|
71
75
|
const editorContext = useContext(EditorContext)
|
|
72
76
|
const [apiFilterDropdowns, setAPIFilterDropdowns] = useState<APIFilterDropdowns>({})
|
|
73
|
-
const [currentViewport, setCurrentViewport] = useState('lg')
|
|
77
|
+
const [currentViewport, setCurrentViewport] = useState<ViewPort>('lg')
|
|
74
78
|
const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
|
|
75
79
|
|
|
80
|
+
const isPreview = state.tabSelected === 'Dashboard Preview'
|
|
76
81
|
const replacements = {
|
|
77
82
|
'Remove Spaces': '',
|
|
78
83
|
'Keep Spaces': ' ',
|
|
@@ -85,6 +90,8 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
85
90
|
return vals.some(val => val === undefined)
|
|
86
91
|
}, [state.data])
|
|
87
92
|
|
|
93
|
+
const vizRowColumnLocator = getVizRowColumnLocator(state.config.rows)
|
|
94
|
+
|
|
88
95
|
const getAutoLoadVisualization = (): Visualization | undefined => {
|
|
89
96
|
const autoLoadViz = Object.values(state.config.visualizations).filter(vis => {
|
|
90
97
|
return vis.autoLoad && vis.type === 'filter-dropdowns'
|
|
@@ -96,11 +103,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
96
103
|
|
|
97
104
|
const transform = new DataTransform()
|
|
98
105
|
|
|
99
|
-
const
|
|
100
|
-
return apiEndpoint + (heirarchyLookup || '')
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const setAutoLoadDefaultValue = (sharedFilterIndex: number, filterDropdowns: DropdownOptions) => {
|
|
106
|
+
const setAutoLoadDefaultValue = (sharedFilterIndex: number, filterDropdowns: DropdownOptions, dashboardConfigOverride) => {
|
|
104
107
|
const autoLoadViz = getAutoLoadVisualization()
|
|
105
108
|
if (!autoLoadViz) return // no autoLoading happening
|
|
106
109
|
const notIncludedInAutoLoad = autoLoadViz.hide
|
|
@@ -108,20 +111,22 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
108
111
|
// we don't want to auto load it
|
|
109
112
|
return
|
|
110
113
|
} else {
|
|
111
|
-
const
|
|
114
|
+
const sharedFilters = dashboardConfigOverride.sharedFilters
|
|
115
|
+
const sharedFilter = sharedFilters[sharedFilterIndex]
|
|
112
116
|
if (sharedFilter.active) return // a value has already been selected.
|
|
113
|
-
const filterParents =
|
|
114
|
-
const notAllParentFiltersSelected = filterParents.some(p => !p.active)
|
|
117
|
+
const filterParents = sharedFilters.filter(f => sharedFilter.parents?.includes(f.key))
|
|
118
|
+
const notAllParentFiltersSelected = filterParents.some(p => !p.active && !p.queuedActive)
|
|
115
119
|
if (filterParents && notAllParentFiltersSelected) return
|
|
116
120
|
const defaultFilterDropdown = filterDropdowns.find(({ value }) => value === sharedFilter.apiFilter!.defaultValue)
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
const defaultValue = defaultFilterDropdown?.value || filterDropdowns[0].value
|
|
122
|
+
sharedFilter.active = defaultValue
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
const loadAPIFilters =
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
const loadAPIFilters = (dashboardConfigOverride = undefined) => {
|
|
127
|
+
const sharedFilters = (dashboardConfigOverride || state.config.dashboard).sharedFilters
|
|
128
|
+
if (sharedFilters) {
|
|
129
|
+
const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
|
|
125
130
|
const loadingFilterMemo: APIFilterDropdowns = sharedAPIFilters.reduce((acc, curr) => {
|
|
126
131
|
const _key = getApiFilterKey(curr.apiFilter!)
|
|
127
132
|
if (apiFilterDropdowns[_key] != null) return acc // don't overwrite fetched data.
|
|
@@ -133,89 +138,108 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
133
138
|
const getParentParams = (childFilter: SharedFilter): Record<'key' | 'value', string>[] | null => {
|
|
134
139
|
const _parents = sharedAPIFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
|
|
135
140
|
if (!_parents.length) return null
|
|
136
|
-
return _parents.map(({ queryParameter, queuedActive }) => ({ key: queryParameter || '', value: queuedActive || '' }))
|
|
141
|
+
return _parents.map(({ queryParameter, active, queuedActive }) => ({ key: queryParameter || '', value: active || queuedActive || '' }))
|
|
137
142
|
}
|
|
138
|
-
const getFilterValues = (
|
|
139
|
-
const { textSelector, valueSelector
|
|
140
|
-
|
|
141
|
-
const heirarchy = heirarchyLookup!.split('.')
|
|
142
|
-
const selector = heirarchy.shift() // pop first element
|
|
143
|
-
return getFilterValues(selector ? filterData[selector] : filterData, { ...apiFilter, heirarchyLookup: heirarchy.join('.') })
|
|
144
|
-
}
|
|
145
|
-
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')
|
|
146
|
-
return filterData.map(v => ({ text: v[textSelector], value: v[valueSelector] }))
|
|
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
146
|
}
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
const toFetch = {}
|
|
148
|
+
sharedAPIFilters.forEach((filter, index) => {
|
|
150
149
|
const baseEndpoint = filter.apiFilter.apiEndpoint
|
|
151
150
|
const _key = getApiFilterKey(filter.apiFilter)
|
|
152
151
|
const params = getParentParams(filter)
|
|
153
152
|
const notAllParentsSelected = params?.some(({ value }) => value === '')
|
|
153
|
+
|
|
154
154
|
if (notAllParentsSelected) return // don't send request for dependent children filter options
|
|
155
|
-
if (apiFilterDropdowns[_key] && !params
|
|
156
|
-
const
|
|
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
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
.then(data => {
|
|
161
|
-
const apiFilter = filterLookup.get(_key) as APIFilter
|
|
162
|
-
const _filterValues = getFilterValues(data, apiFilter)
|
|
163
|
-
setAPIFilterDropdowns(dropdowns => ({ ...dropdowns, [_key]: _filterValues }))
|
|
164
|
-
setAutoLoadDefaultValue(index, _filterValues)
|
|
165
|
-
})
|
|
159
|
+
const endpoint = baseEndpoint + (params ? gatherQueryParams(params) : '')
|
|
160
|
+
toFetch[endpoint] = [_key, index]
|
|
166
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
|
+
)
|
|
167
183
|
}
|
|
168
184
|
}
|
|
169
185
|
|
|
170
|
-
const reloadURLData = async () => {
|
|
171
|
-
const
|
|
172
|
-
if (config.datasets)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
const reloadURLData = async (dashboardConfigOverride = undefined) => {
|
|
187
|
+
const config = _.cloneDeep(state.config)
|
|
188
|
+
if (!config.datasets) return
|
|
189
|
+
let newData = { ...state.data }
|
|
190
|
+
let newDatasets = { ...config.datasets }
|
|
191
|
+
let datasetsNeedsUpdate = false
|
|
192
|
+
let datasetKeys = Object.keys(config.datasets)
|
|
193
|
+
let newFileName = ''
|
|
194
|
+
const filters = (dashboardConfigOverride || config.dashboard)?.sharedFilters
|
|
195
|
+
for (let i = 0; i < datasetKeys.length; i++) {
|
|
196
|
+
const datasetKey = datasetKeys[i]
|
|
197
|
+
const dataset = config.datasets[datasetKey]
|
|
198
|
+
|
|
199
|
+
if (dataset.dataUrl && filters) {
|
|
200
|
+
const dataUrl = new URL(dataset.runtimeDataUrl || dataset.dataUrl, window.location.origin)
|
|
201
|
+
let currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
|
|
202
|
+
let updatedQSParams = {}
|
|
203
|
+
let isUpdateNeeded = !!dashboardConfigOverride
|
|
204
|
+
|
|
205
|
+
filters.forEach(filter => {
|
|
206
|
+
// filter.active is always a string when filter.type is 'urlfilter'
|
|
207
|
+
if (filter.type === 'urlfilter' && !Array.isArray(filter.active)) {
|
|
189
208
|
if (filter.filterBy === 'File Name') {
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
209
|
+
isUpdateNeeded = true
|
|
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}')) {
|
|
195
221
|
newFileName = newFileName.replace('${query}', capitalizeSplitAndJoin.call(String(filter.active), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces']))
|
|
196
222
|
}
|
|
197
223
|
}
|
|
198
224
|
|
|
199
|
-
if (
|
|
225
|
+
if (!!filter.queryParameter) {
|
|
200
226
|
if (updatedQSParams[filter.queryParameter]) {
|
|
201
227
|
updatedQSParams[filter.queryParameter] = updatedQSParams[filter.queryParameter] + filter.active
|
|
202
228
|
} else {
|
|
203
229
|
updatedQSParams[filter.queryParameter] = filter.active
|
|
204
230
|
}
|
|
205
231
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
Object.keys(updatedQSParams).forEach(updatedParam => {
|
|
212
|
-
if (decodeURIComponent(updatedQSParams[updatedParam]) !== currentQSParams[updatedParam]) {
|
|
213
|
-
isUpdateNeeded = true
|
|
214
|
-
}
|
|
215
|
-
})
|
|
232
|
+
}
|
|
233
|
+
})
|
|
216
234
|
|
|
217
|
-
|
|
235
|
+
Object.keys(updatedQSParams).forEach(updatedParam => {
|
|
236
|
+
if (decodeURIComponent(updatedQSParams[updatedParam]) !== currentQSParams[updatedParam]) {
|
|
237
|
+
isUpdateNeeded = true
|
|
238
|
+
}
|
|
239
|
+
})
|
|
218
240
|
|
|
241
|
+
if (isUpdateNeeded) {
|
|
242
|
+
datasetsNeedsUpdate = true
|
|
219
243
|
Object.keys(currentQSParams).forEach(currentParam => {
|
|
220
244
|
if (!updatedQSParams[currentParam]) {
|
|
221
245
|
updatedQSParams[currentParam] = currentQSParams[currentParam]
|
|
@@ -243,32 +267,25 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
243
267
|
newDatasets[datasetKey].data = newDataset
|
|
244
268
|
newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
|
|
245
269
|
newData[datasetKey] = newDataset
|
|
246
|
-
datasetsNeedsUpdate = true
|
|
247
270
|
}
|
|
248
271
|
}
|
|
272
|
+
}
|
|
249
273
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
let newFilteredData = {}
|
|
254
|
-
let newConfig = { ...config }
|
|
255
|
-
getVizKeys(config).forEach(key => {
|
|
256
|
-
let dataKey = config.visualizations[key].dataKey
|
|
274
|
+
if (datasetsNeedsUpdate) {
|
|
275
|
+
const dashboardConfig = dashboardConfigOverride || config.dashboard
|
|
276
|
+
dispatch({ type: 'SET_DATA', payload: newData })
|
|
257
277
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
})
|
|
278
|
+
const newFilteredData = getFilteredData(state, {}, newData)
|
|
279
|
+
const visualizations = Object.keys(config.visualizations).reduce((acc, vizKey) => {
|
|
280
|
+
const dataKey = config.visualizations[vizKey].dataKey
|
|
281
|
+
if (newData[dataKey]) {
|
|
282
|
+
acc[vizKey].formattedData = newData[dataKey]
|
|
283
|
+
}
|
|
284
|
+
return acc
|
|
285
|
+
}, _.cloneDeep(config.visualizations))
|
|
267
286
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
271
|
-
}
|
|
287
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
288
|
+
dispatch({ type: 'SET_CONFIG', payload: { dashboard: dashboardConfig, datasets: newDatasets, visualizations } })
|
|
272
289
|
}
|
|
273
290
|
}
|
|
274
291
|
|
|
@@ -283,9 +300,8 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
283
300
|
}
|
|
284
301
|
|
|
285
302
|
const setSharedFilter = (key, datum) => {
|
|
286
|
-
const { config } = state
|
|
287
|
-
|
|
288
|
-
let newFilteredData = { ...state.filteredData }
|
|
303
|
+
const { config: newConfig, filteredData } = _.cloneDeep(state)
|
|
304
|
+
|
|
289
305
|
for (let i = 0; i < newConfig.dashboard.sharedFilters.length; i++) {
|
|
290
306
|
const filter = newConfig.dashboard.sharedFilters[i]
|
|
291
307
|
if (filter.setBy === key) {
|
|
@@ -296,20 +312,10 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
296
312
|
}
|
|
297
313
|
}
|
|
298
314
|
|
|
299
|
-
|
|
300
|
-
let applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1)
|
|
301
|
-
|
|
302
|
-
if (applicableFilters.length > 0) {
|
|
303
|
-
const visualization = newConfig.visualizations[visualizationKey]
|
|
304
|
-
|
|
305
|
-
const formattedData = visualization.dataDescription ? getFormattedData(state.data[visualization.dataKey] || visualization.data, visualization.dataDescription) : undefined
|
|
306
|
-
|
|
307
|
-
newFilteredData[visualizationKey] = filterData(applicableFilters, formattedData || state.data[visualization.dataKey], state.config.filterBehavior)
|
|
308
|
-
}
|
|
309
|
-
})
|
|
315
|
+
const newFilteredData = getFilteredData({ ...state, config: newConfig }, filteredData)
|
|
310
316
|
|
|
311
317
|
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
312
|
-
dispatch({ type: '
|
|
318
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: newConfig.dashboard.sharedFilters })
|
|
313
319
|
}
|
|
314
320
|
|
|
315
321
|
useEffect(() => {
|
|
@@ -318,19 +324,19 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
318
324
|
reloadURLData()
|
|
319
325
|
}
|
|
320
326
|
loadAPIFilters()
|
|
321
|
-
}, [
|
|
327
|
+
}, [])
|
|
322
328
|
|
|
323
329
|
const updateChildConfig = (visualizationKey, newConfig) => {
|
|
324
|
-
const
|
|
325
|
-
|
|
330
|
+
const config = _.cloneDeep(state.config)
|
|
331
|
+
const updatedConfig = _.pick(config, ['visualizations', 'multiDashboards'])
|
|
326
332
|
updatedConfig.visualizations[visualizationKey] = newConfig
|
|
327
333
|
updatedConfig.visualizations[visualizationKey].formattedData = config.visualizations[visualizationKey].formattedData
|
|
328
334
|
if (config.multiDashboards) {
|
|
329
335
|
const activeDashboard = config.activeDashboard
|
|
330
336
|
const multiDashboards = [...config.multiDashboards]
|
|
331
337
|
const label = multiDashboards[activeDashboard].label
|
|
332
|
-
const toSave = _.pick(
|
|
333
|
-
multiDashboards[activeDashboard] =
|
|
338
|
+
const toSave = { label, visualizations: updatedConfig.visualizations, ..._.pick(config, ['dashboard', 'rows']) }
|
|
339
|
+
multiDashboards[activeDashboard] = toSave
|
|
334
340
|
updatedConfig.multiDashboards = multiDashboards
|
|
335
341
|
}
|
|
336
342
|
|
|
@@ -342,74 +348,97 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
342
348
|
}
|
|
343
349
|
|
|
344
350
|
const applyFilters = () => {
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
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) {
|
|
348
363
|
if (state.config.filterBehavior === FilterBehavior.Apply) {
|
|
349
|
-
|
|
364
|
+
const queryParams = getQueryParams()
|
|
365
|
+
let needsQueryUpdate = false
|
|
366
|
+
dashboardConfig.sharedFilters.forEach((sharedFilter, index) => {
|
|
350
367
|
if (sharedFilter.queuedActive) {
|
|
351
368
|
dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
|
|
352
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
|
+
}
|
|
353
375
|
}
|
|
354
376
|
})
|
|
377
|
+
|
|
378
|
+
if (needsQueryUpdate) {
|
|
379
|
+
updateQueryString(queryParams)
|
|
380
|
+
}
|
|
355
381
|
}
|
|
356
382
|
|
|
357
|
-
dispatch({ type: '
|
|
383
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
|
|
358
384
|
updateDataFilters()
|
|
359
|
-
|
|
385
|
+
loadAPIFilters(dashboardConfig)
|
|
386
|
+
.then(() => {
|
|
387
|
+
reloadURLData(dashboardConfig)
|
|
388
|
+
})
|
|
389
|
+
.catch(e => {
|
|
390
|
+
console.error(e)
|
|
391
|
+
})
|
|
360
392
|
} else {
|
|
361
393
|
// TODO noftify of required fields
|
|
362
394
|
}
|
|
363
395
|
}
|
|
364
396
|
|
|
365
|
-
const changeFilterActive = (index: number, value: string) => {
|
|
366
|
-
const
|
|
367
|
-
|
|
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)
|
|
368
402
|
|
|
369
|
-
if (config.filterBehavior !== FilterBehavior.Apply) {
|
|
370
|
-
|
|
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
|
+
}
|
|
371
411
|
} else {
|
|
372
|
-
|
|
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
|
|
373
414
|
}
|
|
374
415
|
|
|
375
|
-
dispatch({ type: '
|
|
376
|
-
if (config.filterBehavior !== FilterBehavior.Apply) {
|
|
377
|
-
updateDataFilters()
|
|
416
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
|
|
417
|
+
if (state.config.filterBehavior !== FilterBehavior.Apply) {
|
|
418
|
+
updateDataFilters(sharedFilters)
|
|
378
419
|
reloadURLData()
|
|
379
420
|
}
|
|
421
|
+
return sharedFilters
|
|
380
422
|
}
|
|
381
423
|
|
|
382
|
-
const updateDataFilters = () => {
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
let newFilteredData = {}
|
|
387
|
-
getVizKeys(config).forEach(key => {
|
|
388
|
-
let applicableFilters = dashboardConfig.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(key) !== -1)
|
|
389
|
-
if (applicableFilters.length > 0) {
|
|
390
|
-
const visualization = config.visualizations[key]
|
|
391
|
-
const _data = state.data[visualization.dataKey] || visualization.data
|
|
392
|
-
const formattedData = visualization.dataDescription ? getFormattedData(_data, visualization.dataDescription) : _data
|
|
393
|
-
|
|
394
|
-
newFilteredData[key] = filterData(applicableFilters, formattedData, config.filterBehavior)
|
|
395
|
-
}
|
|
396
|
-
})
|
|
397
|
-
|
|
424
|
+
const updateDataFilters = (sharedFilters = undefined) => {
|
|
425
|
+
const clonedState = _.cloneDeep(state)
|
|
426
|
+
if (sharedFilters) clonedState.config.dashboard.sharedFilters = sharedFilters
|
|
427
|
+
const newFilteredData = getFilteredData(clonedState)
|
|
398
428
|
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
399
429
|
}
|
|
400
430
|
|
|
401
|
-
const handleOnChange = (index: number, value: string) => {
|
|
402
|
-
const
|
|
403
|
-
changeFilterActive(index, value)
|
|
431
|
+
const handleOnChange = (index: number, value: string | string[]) => {
|
|
432
|
+
const config = _.cloneDeep(state.config)
|
|
433
|
+
const newSharedFilters = changeFilterActive(index, value)
|
|
404
434
|
if (config.filterBehavior === FilterBehavior.Apply) {
|
|
405
435
|
const autoLoadViz = getAutoLoadVisualization()
|
|
406
|
-
|
|
407
|
-
const isAutoSelectFilter = !autoLoadViz.hide.includes(index)
|
|
436
|
+
const isAutoSelectFilter = !autoLoadViz?.hide.includes(index)
|
|
408
437
|
const missingFilterSelections = config.dashboard.sharedFilters.some(f => !f.active)
|
|
409
438
|
if (isAutoSelectFilter && !missingFilterSelections) {
|
|
410
439
|
// a dropdown has been selected that doesn't
|
|
411
440
|
// require the Go Button
|
|
412
|
-
reloadURLData()
|
|
441
|
+
reloadURLData({ sharedFilters: newSharedFilters })
|
|
413
442
|
} else {
|
|
414
443
|
// A parent filter was selected, reset filters by:
|
|
415
444
|
// set auto select filter dropdowns to null
|
|
@@ -423,6 +452,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
423
452
|
if (config.filterBehavior !== FilterBehavior.Apply) {
|
|
424
453
|
dashboardConfig.sharedFilters[index].active = value
|
|
425
454
|
} else {
|
|
455
|
+
if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
|
|
426
456
|
dashboardConfig.sharedFilters[index].queuedActive = value
|
|
427
457
|
}
|
|
428
458
|
const newSharedFilters = config.dashboard.sharedFilters.map((filter, _index) => {
|
|
@@ -430,77 +460,16 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
430
460
|
if (_isAutoSelectFilter) filter.active = ''
|
|
431
461
|
return filter
|
|
432
462
|
})
|
|
433
|
-
const _newConfig = {
|
|
463
|
+
const _newConfig = { dashboard: { ...config.dashboard, sharedFilters: newSharedFilters } }
|
|
434
464
|
dispatch({ type: 'SET_CONFIG', payload: _newConfig })
|
|
435
465
|
// setData to empty object because we no longer have a data state.
|
|
436
466
|
dispatch({ type: 'SET_DATA', payload: {} })
|
|
437
467
|
dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
|
|
468
|
+
loadAPIFilters(_newConfig.dashboard)
|
|
438
469
|
}
|
|
439
470
|
}
|
|
440
471
|
}
|
|
441
472
|
|
|
442
|
-
const Filters = ({ hide, autoLoad }: { hide?: number[]; autoLoad?: boolean }) => {
|
|
443
|
-
const { config } = state
|
|
444
|
-
const isLegacyFilter = !config.filterBehavior
|
|
445
|
-
return (
|
|
446
|
-
<>
|
|
447
|
-
{config.dashboard.sharedFilters.map((singleFilter, filterIndex) => {
|
|
448
|
-
if ((singleFilter.type !== 'urlfilter' && !singleFilter.showDropdown) || (hide && hide.indexOf(filterIndex) !== -1)) return <></>
|
|
449
|
-
const values: JSX.Element[] = []
|
|
450
|
-
if (singleFilter.resetLabel) {
|
|
451
|
-
values.push(
|
|
452
|
-
<option key={`${singleFilter.resetLabel}-option`} value={singleFilter.resetLabel}>
|
|
453
|
-
{singleFilter.resetLabel}
|
|
454
|
-
</option>
|
|
455
|
-
)
|
|
456
|
-
}
|
|
457
|
-
const _key = singleFilter.apiFilter ? getApiFilterKey(singleFilter.apiFilter) : undefined
|
|
458
|
-
if (_key && apiFilterDropdowns[_key]) {
|
|
459
|
-
// URL Filter
|
|
460
|
-
apiFilterDropdowns[_key]!.forEach(({ text, value }, index) => {
|
|
461
|
-
values.push(
|
|
462
|
-
<option key={`${value}-option-${index}`} value={value}>
|
|
463
|
-
{text}
|
|
464
|
-
</option>
|
|
465
|
-
)
|
|
466
|
-
})
|
|
467
|
-
} else {
|
|
468
|
-
// Data Filter
|
|
469
|
-
singleFilter.values?.forEach((filterOption, index) => {
|
|
470
|
-
const labeledOpt = singleFilter.labels && singleFilter.labels[filterOption]
|
|
471
|
-
values.push(
|
|
472
|
-
<option key={`${singleFilter.key}-option-${index}`} value={filterOption}>
|
|
473
|
-
{labeledOpt || filterOption}
|
|
474
|
-
</option>
|
|
475
|
-
)
|
|
476
|
-
})
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return (
|
|
480
|
-
<div className='cove-dashboard-filters' key={`${singleFilter.key}-filtersection-${filterIndex}`}>
|
|
481
|
-
<section className='dashboard-filters-section'>
|
|
482
|
-
<label htmlFor={`filter-${filterIndex}`}>{singleFilter.key}</label>
|
|
483
|
-
<select
|
|
484
|
-
id={`filter-${filterIndex}`}
|
|
485
|
-
className='filter-select'
|
|
486
|
-
data-index='0'
|
|
487
|
-
value={singleFilter.queuedActive || singleFilter.active}
|
|
488
|
-
onChange={val => {
|
|
489
|
-
handleOnChange(filterIndex, val.target.value)
|
|
490
|
-
}}
|
|
491
|
-
>
|
|
492
|
-
{values}
|
|
493
|
-
</select>
|
|
494
|
-
</section>
|
|
495
|
-
</div>
|
|
496
|
-
)
|
|
497
|
-
})}
|
|
498
|
-
|
|
499
|
-
{!isLegacyFilter && config.filterBehavior === FilterBehavior.Apply && <button onClick={applyFilters}>GO!</button>}
|
|
500
|
-
</>
|
|
501
|
-
)
|
|
502
|
-
}
|
|
503
|
-
|
|
504
473
|
const resizeObserver = new ResizeObserver(entries => {
|
|
505
474
|
for (let entry of entries) {
|
|
506
475
|
let newViewport = getViewport(entry.contentRect.width)
|
|
@@ -520,29 +489,21 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
520
489
|
// Prevent render if loading
|
|
521
490
|
if (state.loading) return <Loading />
|
|
522
491
|
|
|
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
|
+
|
|
523
499
|
let body: JSX.Element | null = null
|
|
524
500
|
// Editor mode
|
|
525
|
-
if (isEditor && !
|
|
501
|
+
if (isEditor && !isPreview) {
|
|
526
502
|
let subVisualizationEditing = false
|
|
527
503
|
|
|
528
504
|
getVizKeys(state.config).forEach(visualizationKey => {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
|
|
532
|
-
|
|
533
|
-
if (state.filteredData && state.filteredData[visualizationKey]) {
|
|
534
|
-
visualizationConfig.data = state.filteredData[visualizationKey]
|
|
535
|
-
if (visualizationConfig.formattedData) {
|
|
536
|
-
visualizationConfig.originalFormattedData = visualizationConfig.formattedData
|
|
537
|
-
visualizationConfig.formattedData = visualizationConfig.data
|
|
538
|
-
}
|
|
539
|
-
} else {
|
|
540
|
-
visualizationConfig.data = state.data[dataKey]
|
|
541
|
-
if (visualizationConfig.formattedData) {
|
|
542
|
-
visualizationConfig.originalFormattedData = visualizationConfig.formattedData
|
|
543
|
-
visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
|
|
544
|
-
}
|
|
545
|
-
}
|
|
505
|
+
const rowNumber = vizRowColumnLocator[visualizationKey]?.row
|
|
506
|
+
const visualizationConfig = getVizConfig(visualizationKey, rowNumber, state.config, state.data, state.filteredData)
|
|
546
507
|
|
|
547
508
|
const setsSharedFilter = state.config.dashboard.sharedFilters && state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey).length > 0
|
|
548
509
|
const setSharedFilterValue = setsSharedFilter ? state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey)[0].active : undefined
|
|
@@ -598,11 +559,10 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
598
559
|
)
|
|
599
560
|
break
|
|
600
561
|
case 'data-bite':
|
|
601
|
-
visualizationConfig = { ...visualizationConfig, newViz: true }
|
|
602
562
|
body = (
|
|
603
563
|
<>
|
|
604
564
|
<Header visualizationKey={visualizationKey} subEditor='Data Bite' />
|
|
605
|
-
<CdcDataBite key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} />
|
|
565
|
+
<CdcDataBite key={visualizationKey} config={{ ...visualizationConfig, newViz: true }} isEditor={true} setConfig={_updateConfig} isDashboard={true} />
|
|
606
566
|
</>
|
|
607
567
|
)
|
|
608
568
|
break
|
|
@@ -635,12 +595,20 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
635
595
|
body = !hideFilter ? (
|
|
636
596
|
<>
|
|
637
597
|
<Header visualizationKey={visualizationKey} subEditor='Filter Dropdowns' />
|
|
638
|
-
<Filters hide={visualizationConfig.hide}
|
|
598
|
+
<Filters hide={visualizationConfig.hide} filters={state.config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
|
|
599
|
+
<GoButton autoLoad={visualizationConfig.autoLoad} />
|
|
639
600
|
</>
|
|
640
601
|
) : (
|
|
641
602
|
<></>
|
|
642
603
|
)
|
|
643
604
|
break
|
|
605
|
+
case 'table':
|
|
606
|
+
body = (
|
|
607
|
+
<EditorWrapper component={DataTableStandAlone} visualizationKey={visualizationKey} visualizationConfig={visualizationConfig} updateConfig={_updateConfig} type={'Table'} viewport={currentViewport}>
|
|
608
|
+
<DataTableEditorPanel key={visualizationKey} config={visualizationConfig} updateConfig={_updateConfig} />
|
|
609
|
+
</EditorWrapper>
|
|
610
|
+
)
|
|
611
|
+
break
|
|
644
612
|
default:
|
|
645
613
|
body = <></>
|
|
646
614
|
break
|
|
@@ -652,7 +620,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
652
620
|
body = (
|
|
653
621
|
<DndProvider backend={HTML5Backend}>
|
|
654
622
|
<div className='header-container'>
|
|
655
|
-
<Header
|
|
623
|
+
<Header />
|
|
656
624
|
<VisualizationsPanel loadConfig={newConfig => dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })} config={state.config} />
|
|
657
625
|
</div>
|
|
658
626
|
|
|
@@ -667,188 +635,98 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
667
635
|
const { title, description } = config.dashboard || {}
|
|
668
636
|
body = (
|
|
669
637
|
<>
|
|
670
|
-
{isEditor && <Header
|
|
671
|
-
<MultiTabs isEditor={isEditor && !
|
|
672
|
-
<
|
|
673
|
-
<
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
{
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
{visualizationConfig.type === 'data-bite' && (
|
|
754
|
-
<CdcDataBite
|
|
755
|
-
key={col.widget}
|
|
756
|
-
config={visualizationConfig}
|
|
757
|
-
isEditor={false}
|
|
758
|
-
setConfig={newConfig => {
|
|
759
|
-
updateChildConfig(col.widget, newConfig)
|
|
760
|
-
}}
|
|
761
|
-
isDashboard={true}
|
|
762
|
-
/>
|
|
763
|
-
)}
|
|
764
|
-
{visualizationConfig.type === 'waffle-chart' && (
|
|
765
|
-
<CdcWaffleChart
|
|
766
|
-
key={col.widget}
|
|
767
|
-
config={visualizationConfig}
|
|
768
|
-
isEditor={false}
|
|
769
|
-
setConfig={newConfig => {
|
|
770
|
-
updateChildConfig(col.widget, newConfig)
|
|
771
|
-
}}
|
|
772
|
-
isDashboard={true}
|
|
773
|
-
configUrl={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
|
|
774
|
-
/>
|
|
775
|
-
)}
|
|
776
|
-
{visualizationConfig.type === 'markup-include' && (
|
|
777
|
-
<CdcMarkupInclude
|
|
778
|
-
key={col.widget}
|
|
779
|
-
config={visualizationConfig}
|
|
780
|
-
isEditor={false}
|
|
781
|
-
setConfig={newConfig => {
|
|
782
|
-
updateChildConfig(col.widget, newConfig)
|
|
783
|
-
}}
|
|
784
|
-
isDashboard={true}
|
|
785
|
-
configUrl={undefined}
|
|
786
|
-
/>
|
|
787
|
-
)}
|
|
788
|
-
{visualizationConfig.type === 'filtered-text' && (
|
|
789
|
-
<CdcFilteredText
|
|
790
|
-
key={col.widget}
|
|
791
|
-
config={visualizationConfig}
|
|
792
|
-
isEditor={false}
|
|
793
|
-
setConfig={newConfig => {
|
|
794
|
-
updateChildConfig(col.widget, newConfig)
|
|
795
|
-
}}
|
|
796
|
-
isDashboard={true}
|
|
797
|
-
configUrl={undefined}
|
|
798
|
-
/>
|
|
799
|
-
)}
|
|
800
|
-
{visualizationConfig.type === 'filter-dropdowns' && !hideFilter && <Filters hide={visualizationConfig.hide} autoLoad={visualizationConfig.autoLoad} />}
|
|
801
|
-
</div>
|
|
802
|
-
</React.Fragment>
|
|
803
|
-
)
|
|
804
|
-
}
|
|
805
|
-
return <React.Fragment key={`vis__${index}__${colIndex}`}></React.Fragment>
|
|
806
|
-
})}
|
|
807
|
-
</div>
|
|
808
|
-
)
|
|
809
|
-
})}
|
|
638
|
+
{isEditor && <Header />}
|
|
639
|
+
<MultiTabs isEditor={isEditor && !isPreview} />
|
|
640
|
+
<Layout.Responsive isEditor={isEditor}>
|
|
641
|
+
<div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''}`}>
|
|
642
|
+
<Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
|
|
643
|
+
{/* Description */}
|
|
644
|
+
{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
|
+
{/* Visualizations */}
|
|
655
|
+
{config.rows &&
|
|
656
|
+
config.rows
|
|
657
|
+
.filter(row => row.columns.filter(col => col.widget).length !== 0)
|
|
658
|
+
.map((row, index) => {
|
|
659
|
+
if (row.multiVizColumn && (isPreview || !isEditor)) {
|
|
660
|
+
const filteredData = getFilteredData(state)
|
|
661
|
+
const data = filteredData[index] ?? row.formattedData
|
|
662
|
+
const dataGroups = {}
|
|
663
|
+
data.forEach(d => {
|
|
664
|
+
const groupKey = d[row.multiVizColumn]
|
|
665
|
+
if (!dataGroups[groupKey]) dataGroups[groupKey] = []
|
|
666
|
+
dataGroups[groupKey].push(d)
|
|
667
|
+
})
|
|
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
|
+
return (
|
|
689
|
+
<VisualizationRow key={`row__${index}`} row={row} rowIndex={index} setSharedFilter={setSharedFilter} updateChildConfig={updateChildConfig} applyFilters={applyFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} currentViewport={currentViewport} />
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
})}
|
|
693
|
+
|
|
694
|
+
{/* Image or PDF Inserts */}
|
|
695
|
+
<section className='download-buttons'>
|
|
696
|
+
{config.table?.downloadImageButton && <MediaControls.Button title='Download Dashboard as Image' type='image' state={config} text='Download Dashboard Image' elementToCapture={imageId} />}
|
|
697
|
+
{config.table?.downloadPdfButton && <MediaControls.Button title='Download Dashboard as PDF' type='pdf' state={config} text='Download Dashboard PDF' elementToCapture={imageId} />}
|
|
698
|
+
</section>
|
|
699
|
+
|
|
700
|
+
{/* Data Table */}
|
|
701
|
+
{config.table?.show && config.data && (
|
|
702
|
+
<DataTable
|
|
703
|
+
config={config}
|
|
704
|
+
rawData={config.data?.[0]?.tableData ? config.data?.[0]?.tableData : config.data}
|
|
705
|
+
runtimeData={config.data?.[0]?.tableData ? config.data?.[0]?.tableData : config.data || []}
|
|
706
|
+
expandDataTable={config.table.expanded}
|
|
707
|
+
showDownloadButton={config.table.download}
|
|
708
|
+
tableTitle={config.dashboard.title || ''}
|
|
709
|
+
viewport={currentViewport}
|
|
710
|
+
tabbingId={config.dashboard.title || ''}
|
|
711
|
+
outerContainerRef={outerContainerRef}
|
|
712
|
+
imageRef={imageId}
|
|
713
|
+
isDebug={isDebug}
|
|
714
|
+
isEditor={isEditor}
|
|
715
|
+
/>
|
|
716
|
+
)}
|
|
717
|
+
{config.table?.show &&
|
|
718
|
+
config.datasets &&
|
|
719
|
+
Object.keys(config.datasets).map(datasetKey => {
|
|
720
|
+
//For each dataset, find any shared filters that apply to all visualizations using the dataset
|
|
810
721
|
|
|
811
|
-
{/* Image or PDF Inserts */}
|
|
812
|
-
<section className='download-buttons'>
|
|
813
|
-
{config.table?.downloadImageButton && <MediaControls.Button title='Download Dashboard as Image' type='image' state={config} text='Download Dashboard Image' elementToCapture={imageId} />}
|
|
814
|
-
{config.table?.downloadPdfButton && <MediaControls.Button title='Download Dashboard as PDF' type='pdf' state={config} text='Download Dashboard PDF' elementToCapture={imageId} />}
|
|
815
|
-
</section>
|
|
816
|
-
|
|
817
|
-
{/* Data Table */}
|
|
818
|
-
{config.table?.show && config.data && (
|
|
819
|
-
<DataTable
|
|
820
|
-
config={config}
|
|
821
|
-
rawData={config.data}
|
|
822
|
-
runtimeData={config.data || []}
|
|
823
|
-
expandDataTable={config.table.expanded}
|
|
824
|
-
showDownloadButton={config.table.download}
|
|
825
|
-
tableTitle={config.dashboard.title || ''}
|
|
826
|
-
viewport={currentViewport}
|
|
827
|
-
tabbingId={config.dashboard.title || ''}
|
|
828
|
-
outerContainerRef={outerContainerRef}
|
|
829
|
-
imageRef={imageId}
|
|
830
|
-
isDebug={isDebug}
|
|
831
|
-
isEditor={isEditor}
|
|
832
|
-
/>
|
|
833
|
-
)}
|
|
834
|
-
{config.table?.show &&
|
|
835
|
-
config.datasets &&
|
|
836
|
-
Object.keys(config.datasets).map(datasetKey => {
|
|
837
|
-
//For each dataset, find any shared filters that apply to all visualizations using the dataset
|
|
838
|
-
//Apply these filters to the table
|
|
839
|
-
let filteredTableData
|
|
840
|
-
if (config.dashboard.sharedFilters && config.dashboard.sharedFilters.length > 0) {
|
|
841
722
|
//Gets list of visuailzations using the dataset
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
if (config.visualizations[visualizationKey].dataKey === datasetKey) {
|
|
845
|
-
vizKeysUsingDataset.push(visualizationKey)
|
|
846
|
-
}
|
|
723
|
+
const vizKeysUsingDataset: string[] = getVizKeys(config).filter(visualizationKey => {
|
|
724
|
+
return config.visualizations[visualizationKey].dataKey === datasetKey
|
|
847
725
|
})
|
|
848
726
|
|
|
849
727
|
//Checks shared filters against list to see if all visualizations are represented
|
|
850
728
|
let applicableFilters: SharedFilter[] = []
|
|
851
|
-
config.dashboard.sharedFilters
|
|
729
|
+
config.dashboard.sharedFilters?.forEach(sharedFilter => {
|
|
852
730
|
let allMatch = true
|
|
853
731
|
vizKeysUsingDataset.forEach(visualizationKey => {
|
|
854
732
|
if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) === -1) {
|
|
@@ -860,28 +738,25 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
|
|
|
860
738
|
}
|
|
861
739
|
})
|
|
862
740
|
|
|
863
|
-
//Applys any applicable filters
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
)
|
|
883
|
-
})}
|
|
884
|
-
</div>
|
|
741
|
+
//Applys any applicable filters to the Table
|
|
742
|
+
const filteredTableData = applicableFilters.length > 0 ? filterData(applicableFilters, config.datasets[datasetKey].data) : undefined
|
|
743
|
+
return (
|
|
744
|
+
<div className='multi-table-container' id={`data-table-${datasetKey}`} key={`data-table-${datasetKey}`}>
|
|
745
|
+
<DataTable
|
|
746
|
+
config={config as TableConfig}
|
|
747
|
+
dataConfig={config.datasets[datasetKey]}
|
|
748
|
+
rawData={config.datasets[datasetKey].data?.[0]?.tableData || config.datasets[datasetKey].data}
|
|
749
|
+
runtimeData={config.datasets[datasetKey].data?.[0]?.tableData || filteredTableData || config.datasets[datasetKey].data || []}
|
|
750
|
+
expandDataTable={config.table.expanded}
|
|
751
|
+
tableTitle={datasetKey}
|
|
752
|
+
viewport={currentViewport}
|
|
753
|
+
tabbingId={datasetKey}
|
|
754
|
+
/>
|
|
755
|
+
</div>
|
|
756
|
+
)
|
|
757
|
+
})}
|
|
758
|
+
</div>
|
|
759
|
+
</Layout.Responsive>
|
|
885
760
|
</>
|
|
886
761
|
)
|
|
887
762
|
}
|