@cdc/dashboard 4.24.2 → 4.24.3

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 (38) hide show
  1. package/dist/cdcdashboard.js +98192 -85200
  2. package/examples/sankey.json +5218 -0
  3. package/index.html +3 -2
  4. package/package.json +11 -10
  5. package/src/CdcDashboard.tsx +124 -124
  6. package/src/CdcDashboardComponent.tsx +173 -186
  7. package/src/DashboardContext.tsx +4 -1
  8. package/src/_stories/Dashboard.stories.tsx +27 -5
  9. package/src/_stories/_mock/pivot-filter.json +163 -0
  10. package/src/_stories/_mock/standalone-table.json +122 -0
  11. package/src/_stories/_mock/toggle-example.json +4035 -0
  12. package/src/components/EditorWrapper/EditorWrapper.tsx +52 -0
  13. package/src/components/EditorWrapper/editor-wrapper.style.css +13 -0
  14. package/src/components/Filters.tsx +88 -0
  15. package/src/components/Header/FilterModal.tsx +480 -0
  16. package/src/components/Header/Header.tsx +25 -465
  17. package/src/components/Row.tsx +28 -17
  18. package/src/components/Toggle/Toggle.tsx +37 -0
  19. package/src/components/Toggle/index.tsx +1 -0
  20. package/src/components/Toggle/toggle-style.css +34 -0
  21. package/src/components/VisualizationsPanel.tsx +13 -3
  22. package/src/components/Widget.tsx +14 -30
  23. package/src/helpers/filterData.ts +72 -49
  24. package/src/helpers/generateValuesForFilter.ts +2 -12
  25. package/src/helpers/getApiFilterKey.ts +5 -0
  26. package/src/helpers/getUpdateConfig.ts +24 -22
  27. package/src/helpers/iconHash.tsx +34 -0
  28. package/src/helpers/tests/filterData.test.ts +149 -0
  29. package/src/images/icon-toggle.svg +1 -0
  30. package/src/scss/grid.scss +1 -1
  31. package/src/scss/main.scss +6 -0
  32. package/src/store/dashboard.actions.ts +19 -2
  33. package/src/store/dashboard.reducer.ts +9 -1
  34. package/src/types/ConfigRow.ts +2 -0
  35. package/src/types/DataSet.ts +7 -7
  36. package/src/types/InitialState.ts +2 -1
  37. package/src/types/SharedFilter.ts +5 -2
  38. 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,14 @@ 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 Toggle from './components/Toggle'
64
+ import { Dashboard } from './types/Dashboard'
63
65
 
64
66
  type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
65
67
  initialState: InitialState
@@ -67,12 +69,12 @@ type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
67
69
 
68
70
  export default function CdcDashboard({ initialState, isEditor = false, isDebug = false }: DashboardProps) {
69
71
  const [state, dispatch] = useReducer(dashboardReducer, initialState)
70
- console.log('state', state)
71
72
  const editorContext = useContext(EditorContext)
72
73
  const [apiFilterDropdowns, setAPIFilterDropdowns] = useState<APIFilterDropdowns>({})
73
- const [currentViewport, setCurrentViewport] = useState('lg')
74
+ const [currentViewport, setCurrentViewport] = useState<ViewPort>('lg')
74
75
  const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
75
76
 
77
+ const isPreview = state.tabSelected === 'Dashboard Preview'
76
78
  const replacements = {
77
79
  'Remove Spaces': '',
78
80
  'Keep Spaces': ' ',
@@ -96,10 +98,6 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
96
98
 
97
99
  const transform = new DataTransform()
98
100
 
99
- const getApiFilterKey = ({ apiEndpoint, heirarchyLookup }: APIFilter) => {
100
- return apiEndpoint + (heirarchyLookup || '')
101
- }
102
-
103
101
  const setAutoLoadDefaultValue = (sharedFilterIndex: number, filterDropdowns: DropdownOptions) => {
104
102
  const autoLoadViz = getAutoLoadVisualization()
105
103
  if (!autoLoadViz) return // no autoLoading happening
@@ -135,15 +133,15 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
135
133
  if (!_parents.length) return null
136
134
  return _parents.map(({ queryParameter, queuedActive }) => ({ key: queryParameter || '', value: queuedActive || '' }))
137
135
  }
138
- const getFilterValues = (filterData: Object | Array<Object>, apiFilter: APIFilter): DropdownOptions => {
136
+ const getFilterValues = (data: Object | Array<Object>, apiFilter: APIFilter): DropdownOptions => {
139
137
  const { textSelector, valueSelector, heirarchyLookup } = apiFilter
140
138
  if (heirarchyLookup) {
141
139
  const heirarchy = heirarchyLookup!.split('.')
142
140
  const selector = heirarchy.shift() // pop first element
143
- return getFilterValues(selector ? filterData[selector] : filterData, { ...apiFilter, heirarchyLookup: heirarchy.join('.') })
141
+ return getFilterValues(selector ? data[selector] : data, { ...apiFilter, heirarchyLookup: heirarchy.join('.') })
144
142
  }
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
+ if (!Array.isArray(data)) 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')
144
+ return data.map(v => ({ text: v[textSelector], value: v[valueSelector] }))
147
145
  }
148
146
  state.config.dashboard.sharedFilters.forEach(async (filter, index) => {
149
147
  if (!filter.apiFilter) return
@@ -167,55 +165,68 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
167
165
  }
168
166
  }
169
167
 
168
+ const getApplicableFilters = (dashboard: Dashboard, key: string): false | SharedFilter[] => {
169
+ const c = dashboard.sharedFilters?.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(key) !== -1)
170
+ return c?.length > 0 ? c : false
171
+ }
172
+
170
173
  const reloadURLData = async () => {
171
174
  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 => {
175
+ if (!config.datasets) return
176
+ let newData = { ...state.data }
177
+ let newDatasets = { ...config.datasets }
178
+ let datasetsNeedsUpdate = false
179
+ let datasetKeys = Object.keys(config.datasets)
180
+ let newFileName = ''
181
+
182
+ for (let i = 0; i < datasetKeys.length; i++) {
183
+ const datasetKey = datasetKeys[i]
184
+ const dataset = config.datasets[datasetKey]
185
+ const filters = config.dashboard?.sharedFilters
186
+ if (dataset.dataUrl && filters) {
187
+ const dataUrl = new URL(dataset.runtimeDataUrl || dataset.dataUrl, window.location.origin)
188
+ let currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
189
+ let updatedQSParams = {}
190
+ let isUpdateNeeded = false
191
+
192
+ filters.forEach(filter => {
193
+ // filter.active is always a string when filter.type is 'urlfilter'
194
+ if (filter.type === 'urlfilter' && !Array.isArray(filter.active)) {
189
195
  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}')) {
196
+ isUpdateNeeded = true
197
+ if (filter.datasetKey === datasetKey) {
198
+ if (filter.fileName) {
199
+ // if a file name is found, ie, state_${query}, use that, ie. state_activeFilter.json
200
+ newFileName = capitalizeSplitAndJoin.call(String(filter.fileName), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces'])
201
+ } else {
202
+ // if no file name is entered use the default active filter. ie. /activeFilter.json
203
+ newFileName = filter.active
204
+ }
205
+ }
206
+
207
+ if (newFileName?.includes('${query}')) {
195
208
  newFileName = newFileName.replace('${query}', capitalizeSplitAndJoin.call(String(filter.active), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces']))
196
209
  }
197
210
  }
198
211
 
199
- if (filter.type === 'urlfilter' && !!filter.queryParameter) {
212
+ if (!!filter.queryParameter) {
200
213
  if (updatedQSParams[filter.queryParameter]) {
201
214
  updatedQSParams[filter.queryParameter] = updatedQSParams[filter.queryParameter] + filter.active
202
215
  } else {
203
216
  updatedQSParams[filter.queryParameter] = filter.active
204
217
  }
205
218
  }
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
- })
219
+ }
220
+ })
216
221
 
217
- if (!isUpdateNeeded) return
222
+ Object.keys(updatedQSParams).forEach(updatedParam => {
223
+ if (decodeURIComponent(updatedQSParams[updatedParam]) !== currentQSParams[updatedParam]) {
224
+ isUpdateNeeded = true
225
+ }
226
+ })
218
227
 
228
+ if (isUpdateNeeded) {
229
+ datasetsNeedsUpdate = true
219
230
  Object.keys(currentQSParams).forEach(currentParam => {
220
231
  if (!updatedQSParams[currentParam]) {
221
232
  updatedQSParams[currentParam] = currentQSParams[currentParam]
@@ -243,32 +254,30 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
243
254
  newDatasets[datasetKey].data = newDataset
244
255
  newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
245
256
  newData[datasetKey] = newDataset
246
- datasetsNeedsUpdate = true
247
257
  }
248
258
  }
259
+ }
249
260
 
250
- if (datasetsNeedsUpdate) {
251
- dispatch({ type: 'SET_DATA', payload: newData })
261
+ if (datasetsNeedsUpdate) {
262
+ dispatch({ type: 'SET_DATA', payload: newData })
252
263
 
253
- let newFilteredData = {}
254
- let newConfig = { ...config }
255
- getVizKeys(config).forEach(key => {
256
- let dataKey = config.visualizations[key].dataKey
264
+ let newFilteredData = {}
265
+ let visualizations = { ...config.visualizations }
266
+ getVizKeys(config).forEach(key => {
267
+ let dataKey = config.visualizations[key].dataKey
257
268
 
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
- }
269
+ const applicableFilters = getApplicableFilters(config.dashboard, key)
270
+ if (applicableFilters) {
271
+ newFilteredData[key] = filterData(applicableFilters, newData[dataKey])
272
+ }
262
273
 
263
- if (newData[dataKey]) {
264
- newConfig.visualizations[key].formattedData = newData[dataKey]
265
- }
266
- })
274
+ if (newData[dataKey]) {
275
+ visualizations[key].formattedData = newData[dataKey]
276
+ }
277
+ })
267
278
 
268
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
269
- newConfig.datasets = newDatasets
270
- dispatch({ type: 'SET_CONFIG', payload: newConfig })
271
- }
279
+ dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
280
+ dispatch({ type: 'SET_CONFIG', payload: { ...config, datasets: newDatasets, visualizations } })
272
281
  }
273
282
  }
274
283
 
@@ -297,14 +306,13 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
297
306
  }
298
307
 
299
308
  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) {
309
+ const applicableFilters = getApplicableFilters(newConfig.dashboard, visualizationKey)
310
+ if (applicableFilters) {
303
311
  const visualization = newConfig.visualizations[visualizationKey]
304
312
 
305
313
  const formattedData = visualization.dataDescription ? getFormattedData(state.data[visualization.dataKey] || visualization.data, visualization.dataDescription) : undefined
306
314
 
307
- newFilteredData[visualizationKey] = filterData(applicableFilters, formattedData || state.data[visualization.dataKey], state.config.filterBehavior)
315
+ newFilteredData[visualizationKey] = filterData(applicableFilters, formattedData || state.data[visualization.dataKey])
308
316
  }
309
317
  })
310
318
 
@@ -346,12 +354,23 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
346
354
  const allFiltersSelected = !state.config.dashboard.sharedFilters.some(filter => !filter.active && !filter.queuedActive)
347
355
  if (allFiltersSelected) {
348
356
  if (state.config.filterBehavior === FilterBehavior.Apply) {
357
+ const queryParams = getQueryParams()
358
+ let needsQueryUpdate = false
349
359
  state.config.dashboard.sharedFilters.forEach((sharedFilter, index) => {
350
360
  if (sharedFilter.queuedActive) {
351
361
  dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
352
362
  delete dashboardConfig.sharedFilters[index].queuedActive
363
+
364
+ if (sharedFilter.setByQueryParameter && queryParams[sharedFilter.setByQueryParameter] !== sharedFilter.active) {
365
+ queryParams[sharedFilter.setByQueryParameter] = sharedFilter.active
366
+ needsQueryUpdate = true
367
+ }
353
368
  }
354
369
  })
370
+
371
+ if (needsQueryUpdate) {
372
+ updateQueryString(queryParams)
373
+ }
355
374
  }
356
375
 
357
376
  dispatch({ type: 'SET_CONFIG', payload: { ...state.config, dashboard: dashboardConfig } })
@@ -362,13 +381,21 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
362
381
  }
363
382
  }
364
383
 
365
- const changeFilterActive = (index: number, value: string) => {
384
+ const changeFilterActive = (index: number, value: string | string[]) => {
366
385
  const { config } = state
367
386
  let dashboardConfig = { ...config.dashboard }
387
+ let filterActive = dashboardConfig.sharedFilters[index]
368
388
 
369
389
  if (config.filterBehavior !== FilterBehavior.Apply) {
370
390
  dashboardConfig.sharedFilters[index].active = value
391
+
392
+ const queryParams = getQueryParams()
393
+ if (filterActive.setByQueryParameter && queryParams[filterActive.setByQueryParameter] !== filterActive.active) {
394
+ queryParams[filterActive.setByQueryParameter] = filterActive.active
395
+ updateQueryString(queryParams)
396
+ }
371
397
  } else {
398
+ if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
372
399
  dashboardConfig.sharedFilters[index].queuedActive = value
373
400
  }
374
401
 
@@ -381,24 +408,24 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
381
408
 
382
409
  const updateDataFilters = () => {
383
410
  const { config } = state
384
- let dashboardConfig = { ...config.dashboard }
411
+ const dashboardConfig = { ...config.dashboard }
385
412
 
386
- let newFilteredData = {}
413
+ const newFilteredData = {}
387
414
  getVizKeys(config).forEach(key => {
388
- let applicableFilters = dashboardConfig.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(key) !== -1)
389
- if (applicableFilters.length > 0) {
415
+ const applicableFilters = getApplicableFilters(dashboardConfig, key)
416
+ if (applicableFilters) {
390
417
  const visualization = config.visualizations[key]
391
418
  const _data = state.data[visualization.dataKey] || visualization.data
392
419
  const formattedData = visualization.dataDescription ? getFormattedData(_data, visualization.dataDescription) : _data
393
420
 
394
- newFilteredData[key] = filterData(applicableFilters, formattedData, config.filterBehavior)
421
+ newFilteredData[key] = filterData(applicableFilters, formattedData)
395
422
  }
396
423
  })
397
424
 
398
425
  dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
399
426
  }
400
427
 
401
- const handleOnChange = (index: number, value: string) => {
428
+ const handleOnChange = (index: number, value: string | string[]) => {
402
429
  const { config } = state
403
430
  changeFilterActive(index, value)
404
431
  if (config.filterBehavior === FilterBehavior.Apply) {
@@ -423,6 +450,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
423
450
  if (config.filterBehavior !== FilterBehavior.Apply) {
424
451
  dashboardConfig.sharedFilters[index].active = value
425
452
  } else {
453
+ if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
426
454
  dashboardConfig.sharedFilters[index].queuedActive = value
427
455
  }
428
456
  const newSharedFilters = config.dashboard.sharedFilters.map((filter, _index) => {
@@ -439,68 +467,6 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
439
467
  }
440
468
  }
441
469
 
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
470
  const resizeObserver = new ResizeObserver(entries => {
505
471
  for (let entry of entries) {
506
472
  let newViewport = getViewport(entry.contentRect.width)
@@ -520,13 +486,20 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
520
486
  // Prevent render if loading
521
487
  if (state.loading) return <Loading />
522
488
 
489
+ const GoButton = ({ autoLoad }: { autoLoad?: boolean }) => {
490
+ if (state.config.filterBehavior === FilterBehavior.Apply && !autoLoad) {
491
+ return <button onClick={applyFilters}>GO!</button>
492
+ }
493
+ return null
494
+ }
495
+
523
496
  let body: JSX.Element | null = null
524
497
  // Editor mode
525
- if (isEditor && !state.preview) {
498
+ if (isEditor && !isPreview) {
526
499
  let subVisualizationEditing = false
527
500
 
528
501
  getVizKeys(state.config).forEach(visualizationKey => {
529
- let visualizationConfig = { ...state.config.visualizations[visualizationKey] }
502
+ const visualizationConfig = _.cloneDeep(state.config.visualizations[visualizationKey])
530
503
 
531
504
  const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
532
505
 
@@ -598,11 +571,10 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
598
571
  )
599
572
  break
600
573
  case 'data-bite':
601
- visualizationConfig = { ...visualizationConfig, newViz: true }
602
574
  body = (
603
575
  <>
604
576
  <Header visualizationKey={visualizationKey} subEditor='Data Bite' />
605
- <CdcDataBite key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} />
577
+ <CdcDataBite key={visualizationKey} config={{ ...visualizationConfig, newViz: true }} isEditor={true} setConfig={_updateConfig} isDashboard={true} />
606
578
  </>
607
579
  )
608
580
  break
@@ -635,12 +607,20 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
635
607
  body = !hideFilter ? (
636
608
  <>
637
609
  <Header visualizationKey={visualizationKey} subEditor='Filter Dropdowns' />
638
- <Filters hide={visualizationConfig.hide} autoLoad={visualizationConfig.autoLoad} />
610
+ <Filters hide={visualizationConfig.hide} filters={state.config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
611
+ <GoButton autoLoad={visualizationConfig.autoLoad} />
639
612
  </>
640
613
  ) : (
641
614
  <></>
642
615
  )
643
616
  break
617
+ case 'table':
618
+ body = (
619
+ <EditorWrapper component={DataTableStandAlone} visualizationKey={visualizationKey} visualizationConfig={visualizationConfig} updateConfig={_updateConfig} type={'Table'} viewport={currentViewport}>
620
+ <DataTableEditorPanel key={visualizationKey} config={visualizationConfig} updateConfig={_updateConfig} />
621
+ </EditorWrapper>
622
+ )
623
+ break
644
624
  default:
645
625
  body = <></>
646
626
  break
@@ -652,7 +632,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
652
632
  body = (
653
633
  <DndProvider backend={HTML5Backend}>
654
634
  <div className='header-container'>
655
- <Header setPreview={setPreview} />
635
+ <Header />
656
636
  <VisualizationsPanel loadConfig={newConfig => dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })} config={state.config} />
657
637
  </div>
658
638
 
@@ -667,28 +647,35 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
667
647
  const { title, description } = config.dashboard || {}
668
648
  body = (
669
649
  <>
670
- {isEditor && <Header setPreview={setPreview} />}
671
- <MultiTabs isEditor={isEditor && !state.preview} />
650
+ {isEditor && <Header />}
651
+ <MultiTabs isEditor={isEditor && !isPreview} />
672
652
  <div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''}`}>
673
653
  <Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
674
654
  {/* Description */}
675
655
  {description && <div className='subtext'>{parse(description)}</div>}
676
656
 
677
657
  {/* Filters */}
678
- {config.dashboard.sharedFilters && Object.values(config.visualizations || {}).filter(viz => viz.visualizationType === 'filter-dropdowns').length === 0 && <Filters hide={undefined} autoLoad={undefined} />}
658
+ {config.dashboard.sharedFilters && Object.values(config.visualizations || {}).filter(viz => viz.visualizationType === 'filter-dropdowns').length === 0 && (
659
+ <>
660
+ <Filters filters={state.config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
661
+ <GoButton />
662
+ </>
663
+ )}
679
664
 
680
665
  {/* Visualizations */}
681
666
  {config.rows &&
682
667
  config.rows
683
668
  .filter(row => row.filter(col => col.widget).length !== 0)
684
669
  .map((row, index) => {
670
+ const isToggleRow = row[0].toggle
685
671
  return (
686
- <div className={`dashboard-row ${row.equalHeight ? 'equal-height' : ''}`} key={`row__${index}`}>
672
+ <div className={`dashboard-row ${row.equalHeight ? 'equal-height' : ''} ${isToggleRow ? 'toggle' : ''}`} key={`row__${index}`}>
673
+ {isToggleRow && <Toggle row={row} rowIndex={index} visualizations={config.visualizations} />}
687
674
  {row.map((col, colIndex) => {
688
675
  if (col.width) {
689
676
  if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`dashboard-col dashboard-col-${col.width}`}></div>
690
677
 
691
- let visualizationConfig = { ...config.visualizations[col.widget] }
678
+ const visualizationConfig = _.cloneDeep(config.visualizations[col.widget])
692
679
 
693
680
  const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
694
681
 
@@ -714,9 +701,12 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
714
701
  </a>
715
702
  )
716
703
  const hideFilter = visualizationConfig.autoLoad && inNoDataState
704
+
705
+ const hiddenToggle = col.hide !== undefined ? col.hide : colIndex !== 0
706
+ const hidden = col.toggle ? hiddenToggle : false
717
707
  return (
718
708
  <React.Fragment key={`vis__${index}__${colIndex}`}>
719
- <div className={`dashboard-col dashboard-col-${col.width}`}>
709
+ <div className={`dashboard-col dashboard-col-${col.width} ${hidden ? 'hidden-toggle' : ''}`}>
720
710
  {visualizationConfig.type === 'chart' && (
721
711
  <CdcChart
722
712
  key={col.widget}
@@ -797,7 +787,13 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
797
787
  configUrl={undefined}
798
788
  />
799
789
  )}
800
- {visualizationConfig.type === 'filter-dropdowns' && !hideFilter && <Filters hide={visualizationConfig.hide} autoLoad={visualizationConfig.autoLoad} />}
790
+ {visualizationConfig.type === 'filter-dropdowns' && !hideFilter && (
791
+ <>
792
+ <Filters hide={visualizationConfig.hide} filters={state.config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
793
+ <GoButton autoLoad={visualizationConfig.autoLoad} />
794
+ </>
795
+ )}
796
+ {visualizationConfig.type === 'table' && <DataTableStandAlone key={col.widget} visualizationKey={col.widget} config={visualizationConfig} viewport={currentViewport} />}
801
797
  </div>
802
798
  </React.Fragment>
803
799
  )
@@ -818,8 +814,8 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
818
814
  {config.table?.show && config.data && (
819
815
  <DataTable
820
816
  config={config}
821
- rawData={config.data}
822
- runtimeData={config.data || []}
817
+ rawData={config.data?.[0]?.tableData ? config.data?.[0]?.tableData : config.data}
818
+ runtimeData={config.data?.[0]?.tableData ? config.data?.[0]?.tableData : config.data || []}
823
819
  expandDataTable={config.table.expanded}
824
820
  showDownloadButton={config.table.download}
825
821
  tableTitle={config.dashboard.title || ''}
@@ -835,44 +831,35 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
835
831
  config.datasets &&
836
832
  Object.keys(config.datasets).map(datasetKey => {
837
833
  //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
- //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
- }
847
- })
848
834
 
849
- //Checks shared filters against list to see if all visualizations are represented
850
- let applicableFilters: SharedFilter[] = []
851
- config.dashboard.sharedFilters.forEach(sharedFilter => {
852
- let allMatch = true
853
- vizKeysUsingDataset.forEach(visualizationKey => {
854
- if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) === -1) {
855
- allMatch = false
856
- }
857
- })
858
- if (allMatch) {
859
- applicableFilters.push(sharedFilter)
835
+ //Gets list of visuailzations using the dataset
836
+ const vizKeysUsingDataset: string[] = getVizKeys(config).filter(visualizationKey => {
837
+ return config.visualizations[visualizationKey].dataKey === datasetKey
838
+ })
839
+
840
+ //Checks shared filters against list to see if all visualizations are represented
841
+ let applicableFilters: SharedFilter[] = []
842
+ config.dashboard.sharedFilters.forEach(sharedFilter => {
843
+ let allMatch = true
844
+ vizKeysUsingDataset.forEach(visualizationKey => {
845
+ if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) === -1) {
846
+ allMatch = false
860
847
  }
861
848
  })
862
-
863
- //Applys any applicable filters
864
- if (applicableFilters.length > 0) {
865
- filteredTableData = filterData(applicableFilters, config.datasets[datasetKey].data, state.config.filterBehavior)
849
+ if (allMatch) {
850
+ applicableFilters.push(sharedFilter)
866
851
  }
867
- }
852
+ })
868
853
 
854
+ //Applys any applicable filters to the Table
855
+ const filteredTableData = applicableFilters.length > 0 ? filterData(applicableFilters, config.datasets[datasetKey].data) : undefined
869
856
  return (
870
857
  <div className='multi-table-container' id={`data-table-${datasetKey}`} key={`data-table-${datasetKey}`}>
871
858
  <DataTable
872
859
  config={config as TableConfig}
873
860
  dataConfig={config.datasets[datasetKey]}
874
- rawData={config.datasets[datasetKey].data}
875
- runtimeData={filteredTableData || config.datasets[datasetKey].data || []}
861
+ rawData={config.datasets[datasetKey].data?.[0]?.tableData || config.datasets[datasetKey].data}
862
+ runtimeData={config.datasets[datasetKey].data?.[0]?.tableData || filteredTableData || config.datasets[datasetKey].data || []}
876
863
  expandDataTable={config.table.expanded}
877
864
  tableTitle={datasetKey}
878
865
  viewport={currentViewport}
@@ -1,6 +1,7 @@
1
1
  import { Dispatch, createContext } from 'react'
2
2
  import { DashboardState } from './store/dashboard.reducer'
3
3
  import DashboardActions from './store/dashboard.actions'
4
+ import { Tab } from './types/Tab'
4
5
 
5
6
  type ConfigCTX = DashboardState & {
6
7
  outerContainerRef: (node: any) => void
@@ -8,12 +9,14 @@ type ConfigCTX = DashboardState & {
8
9
  isDebug: boolean
9
10
  }
10
11
 
12
+ const firstTab: Tab = 'Dashboard Description'
13
+
11
14
  export const initialState = {
12
15
  data: {},
13
16
  loading: false,
14
17
  filteredData: {},
15
18
  preview: false,
16
- tabSelected: 0
19
+ tabSelected: firstTab
17
20
  }
18
21
 
19
22
  const initialContext: ConfigCTX = {