@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.
Files changed (52) hide show
  1. package/dist/cdcdashboard.js +128512 -99417
  2. package/examples/chart-data.json +5409 -0
  3. package/examples/full-dash-test.json +14643 -0
  4. package/examples/full-dashboard.json +10036 -0
  5. package/examples/sankey.json +5218 -0
  6. package/index.html +4 -3
  7. package/package.json +11 -10
  8. package/src/CdcDashboard.tsx +129 -124
  9. package/src/CdcDashboardComponent.tsx +316 -441
  10. package/src/DashboardContext.tsx +4 -1
  11. package/src/_stories/Dashboard.stories.tsx +79 -36
  12. package/src/_stories/_mock/api-filter-chart.json +11 -35
  13. package/src/_stories/_mock/api-filter-map.json +17 -31
  14. package/src/_stories/_mock/dashboard-gallery.json +523 -534
  15. package/src/_stories/_mock/multi-viz.json +378 -0
  16. package/src/_stories/_mock/pivot-filter.json +161 -0
  17. package/src/_stories/_mock/standalone-table.json +122 -0
  18. package/src/_stories/_mock/toggle-example.json +4035 -0
  19. package/src/components/DataDesignerModal.tsx +145 -0
  20. package/src/components/EditorWrapper/EditorWrapper.tsx +52 -0
  21. package/src/components/EditorWrapper/editor-wrapper.style.css +13 -0
  22. package/src/components/Filters.tsx +88 -0
  23. package/src/components/Grid.tsx +3 -1
  24. package/src/components/Header/FilterModal.tsx +506 -0
  25. package/src/components/Header/Header.tsx +25 -465
  26. package/src/components/Row.tsx +65 -29
  27. package/src/components/Toggle/Toggle.tsx +36 -0
  28. package/src/components/Toggle/index.tsx +1 -0
  29. package/src/components/Toggle/toggle-style.css +34 -0
  30. package/src/components/VisualizationRow.tsx +174 -0
  31. package/src/components/VisualizationsPanel.tsx +13 -3
  32. package/src/components/Widget.tsx +28 -126
  33. package/src/helpers/filterData.ts +75 -50
  34. package/src/helpers/generateValuesForFilter.ts +2 -12
  35. package/src/helpers/getApiFilterKey.ts +5 -0
  36. package/src/helpers/getFilteredData.ts +39 -0
  37. package/src/helpers/getUpdateConfig.ts +39 -22
  38. package/src/helpers/getVizConfig.ts +31 -0
  39. package/src/helpers/getVizRowColumnLocator.ts +9 -0
  40. package/src/helpers/iconHash.tsx +34 -0
  41. package/src/helpers/tests/filterData.test.ts +149 -0
  42. package/src/images/icon-toggle.svg +1 -0
  43. package/src/scss/grid.scss +10 -3
  44. package/src/scss/main.scss +11 -0
  45. package/src/store/dashboard.actions.ts +35 -3
  46. package/src/store/dashboard.reducer.ts +33 -2
  47. package/src/types/APIFilter.ts +4 -5
  48. package/src/types/ConfigRow.ts +13 -2
  49. package/src/types/DataSet.ts +11 -8
  50. package/src/types/InitialState.ts +2 -1
  51. package/src/types/SharedFilter.ts +6 -3
  52. 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
- type DropdownOptions = Record<'value' | 'text', string>[]
58
-
59
- type APIFilterDropdowns = {
60
- // null means still loading
61
- [filtername: string]: null | DropdownOptions
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 getApiFilterKey = ({ apiEndpoint, heirarchyLookup }: APIFilter) => {
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 sharedFilter = state.config.dashboard.sharedFilters[sharedFilterIndex]
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 = state.config.dashboard.sharedFilters.filter(f => sharedFilter.parents?.includes(f.key))
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
- let defaultValue = defaultFilterDropdown?.value || filterDropdowns[0].value
118
- changeFilterActive(sharedFilterIndex, defaultValue)
121
+ const defaultValue = defaultFilterDropdown?.value || filterDropdowns[0].value
122
+ sharedFilter.active = defaultValue
119
123
  }
120
124
  }
121
125
 
122
- const loadAPIFilters = async () => {
123
- if (state.config.dashboard.sharedFilters) {
124
- const sharedAPIFilters = state.config.dashboard.sharedFilters.filter(f => f.apiFilter)
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 = (filterData: Object | Array<Object>, apiFilter: APIFilter): DropdownOptions => {
139
- const { textSelector, valueSelector, heirarchyLookup } = apiFilter
140
- if (heirarchyLookup) {
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
- state.config.dashboard.sharedFilters.forEach(async (filter, index) => {
149
- if (!filter.apiFilter) return
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 && filter.filterBy === 'Query String') return // don't reload filter unless it's a child
156
- const endpoint = baseEndpoint + (params ? gatherQueryParams(params) : '')
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
- fetch(endpoint)
159
- .then(resp => resp.json())
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 { config } = state
172
- if (config.datasets) {
173
- let newData = { ...state.data }
174
- let newDatasets = { ...config.datasets }
175
- let datasetsNeedsUpdate = false
176
- let datasetKeys = Object.keys(config.datasets)
177
- let newFileName = ''
178
-
179
- for (let i = 0; i < datasetKeys.length; i++) {
180
- const datasetKey = datasetKeys[i]
181
- const dataset = config.datasets[datasetKey]
182
- if (dataset.dataUrl && config.dashboard && config.dashboard.sharedFilters) {
183
- const dataUrl = new URL(dataset.runtimeDataUrl || dataset.dataUrl, window.location.origin)
184
- let currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
185
- let updatedQSParams = {}
186
- let isUpdateNeeded = false
187
-
188
- config.dashboard.sharedFilters.forEach(filter => {
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
- // if no file name is entered use the default active filter. ie. /activeFilter.json
191
- if (!filter.fileName && filter.datasetKey === datasetKey) newFileName = filter.active
192
- // if a file name is found, ie, state_${query}, use that, ie. state_activeFilter.json
193
- if (filter.datasetKey === datasetKey && filter.fileName) newFileName = capitalizeSplitAndJoin.call(String(filter.fileName), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces'])
194
- if (newFileName && newFileName.includes('${query}')) {
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 (filter.type === 'urlfilter' && !!filter.queryParameter) {
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
- if (filter.filterBy === 'File Name') {
207
- isUpdateNeeded = true
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
- if (!isUpdateNeeded) return
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
- if (datasetsNeedsUpdate) {
251
- dispatch({ type: 'SET_DATA', payload: newData })
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
- let applicableFilters = config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(key) !== -1)
259
- if (applicableFilters.length > 0) {
260
- newFilteredData[key] = filterData(applicableFilters, newData[dataKey], state.config.filterBehavior)
261
- }
262
-
263
- if (newData[dataKey]) {
264
- newConfig.visualizations[key].formattedData = newData[dataKey]
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
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
269
- newConfig.datasets = newDatasets
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
- let newConfig = { ...config }
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
- getVizKeys(newConfig).forEach(visualizationKey => {
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: 'SET_CONFIG', payload: newConfig })
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
- }, [JSON.stringify(state.config.dashboard ? state.config.dashboard.sharedFilters : undefined)])
327
+ }, [])
322
328
 
323
329
  const updateChildConfig = (visualizationKey, newConfig) => {
324
- const { config } = state
325
- let updatedConfig = { ...config }
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(updatedConfig, ['dashboard', 'visualizations', 'rows'])
333
- multiDashboards[activeDashboard] = { ...toSave, label }
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
- let dashboardConfig = state.config.dashboard
346
- const allFiltersSelected = !state.config.dashboard.sharedFilters.some(filter => !filter.active && !filter.queuedActive)
347
- if (allFiltersSelected) {
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
- state.config.dashboard.sharedFilters.forEach((sharedFilter, index) => {
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: 'SET_CONFIG', payload: { ...state.config, dashboard: dashboardConfig } })
383
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
358
384
  updateDataFilters()
359
- reloadURLData()
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 { config } = state
367
- let dashboardConfig = { ...config.dashboard }
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
- dashboardConfig.sharedFilters[index].active = value
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
- dashboardConfig.sharedFilters[index].queuedActive = value
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: 'SET_CONFIG', payload: { ...config, dashboard: dashboardConfig } })
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 { config } = state
384
- let dashboardConfig = { ...config.dashboard }
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 { config } = state
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
- if (!autoLoadViz) return // nothing left to do for regular filter behavior.
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 = { ...config, dashboard: { ...config.dashboard, sharedFilters: newSharedFilters } }
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 && !state.preview) {
501
+ if (isEditor && !isPreview) {
526
502
  let subVisualizationEditing = false
527
503
 
528
504
  getVizKeys(state.config).forEach(visualizationKey => {
529
- let visualizationConfig = { ...state.config.visualizations[visualizationKey] }
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} autoLoad={visualizationConfig.autoLoad} />
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 setPreview={setPreview} />
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 setPreview={setPreview} />}
671
- <MultiTabs isEditor={isEditor && !state.preview} />
672
- <div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''}`}>
673
- <Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
674
- {/* Description */}
675
- {description && <div className='subtext'>{parse(description)}</div>}
676
-
677
- {/* Filters */}
678
- {config.dashboard.sharedFilters && Object.values(config.visualizations || {}).filter(viz => viz.visualizationType === 'filter-dropdowns').length === 0 && <Filters hide={undefined} autoLoad={undefined} />}
679
-
680
- {/* Visualizations */}
681
- {config.rows &&
682
- config.rows
683
- .filter(row => row.filter(col => col.widget).length !== 0)
684
- .map((row, index) => {
685
- return (
686
- <div className={`dashboard-row ${row.equalHeight ? 'equal-height' : ''}`} key={`row__${index}`}>
687
- {row.map((col, colIndex) => {
688
- if (col.width) {
689
- if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`dashboard-col dashboard-col-${col.width}`}></div>
690
-
691
- let visualizationConfig = { ...config.visualizations[col.widget] }
692
-
693
- const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
694
-
695
- if (state.filteredData && state.filteredData[col.widget]) {
696
- visualizationConfig.data = state.filteredData[col.widget]
697
- if (visualizationConfig.formattedData) {
698
- visualizationConfig.originalFormattedData = visualizationConfig.formattedData
699
- visualizationConfig.formattedData = visualizationConfig.data
700
- }
701
- } else {
702
- visualizationConfig.data = state.data[dataKey]
703
- if (visualizationConfig.formattedData) {
704
- visualizationConfig.originalFormattedData = visualizationConfig.formattedData
705
- visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
706
- }
707
- }
708
-
709
- const setsSharedFilter = config.dashboard.sharedFilters && config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget).length > 0
710
- const setSharedFilterValue = setsSharedFilter ? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget)[0].active : undefined
711
- const tableLink = (
712
- <a href={`#data-table-${visualizationConfig.dataKey}`} className='margin-left-href'>
713
- {visualizationConfig.dataKey} (Go to Table)
714
- </a>
715
- )
716
- const hideFilter = visualizationConfig.autoLoad && inNoDataState
717
- return (
718
- <React.Fragment key={`vis__${index}__${colIndex}`}>
719
- <div className={`dashboard-col dashboard-col-${col.width}`}>
720
- {visualizationConfig.type === 'chart' && (
721
- <CdcChart
722
- key={col.widget}
723
- config={visualizationConfig}
724
- dashboardConfig={config}
725
- isEditor={false}
726
- setConfig={newConfig => {
727
- updateChildConfig(col.widget, newConfig)
728
- }}
729
- setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
730
- isDashboard={true}
731
- link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
732
- configUrl={undefined}
733
- setEditing={undefined}
734
- hostname={undefined}
735
- setSharedFilterValue={undefined}
736
- />
737
- )}
738
- {visualizationConfig.type === 'map' && (
739
- <CdcMap
740
- key={col.widget}
741
- config={visualizationConfig}
742
- isEditor={false}
743
- setConfig={newConfig => {
744
- updateChildConfig(col.widget, newConfig)
745
- }}
746
- showLoader={false}
747
- setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
748
- setSharedFilterValue={setSharedFilterValue}
749
- isDashboard={true}
750
- link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
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
- let vizKeysUsingDataset: string[] = []
843
- getVizKeys(config).forEach(visualizationKey => {
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.forEach(sharedFilter => {
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
- if (applicableFilters.length > 0) {
865
- filteredTableData = filterData(applicableFilters, config.datasets[datasetKey].data, state.config.filterBehavior)
866
- }
867
- }
868
-
869
- return (
870
- <div className='multi-table-container' id={`data-table-${datasetKey}`} key={`data-table-${datasetKey}`}>
871
- <DataTable
872
- config={config as TableConfig}
873
- dataConfig={config.datasets[datasetKey]}
874
- rawData={config.datasets[datasetKey].data}
875
- runtimeData={filteredTableData || config.datasets[datasetKey].data || []}
876
- expandDataTable={config.table.expanded}
877
- tableTitle={datasetKey}
878
- viewport={currentViewport}
879
- tabbingId={datasetKey}
880
- />
881
- </div>
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
  }