@cdc/dashboard 4.24.5 → 4.24.9

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 (87) hide show
  1. package/dist/cdcdashboard.js +144406 -127510
  2. package/examples/custom/css/respiratory.css +236 -0
  3. package/examples/custom/js/respiratory.js +242 -0
  4. package/examples/default-multi-dataset-shared-filter.json +1729 -0
  5. package/examples/ed-visits-county-file.json +618 -0
  6. package/examples/filtered-dash.json +6 -21
  7. package/examples/single-state-dashboard-filters.json +421 -0
  8. package/examples/state-level.json +90136 -0
  9. package/examples/state-points.json +10474 -0
  10. package/examples/test-file.json +147 -0
  11. package/examples/testing.json +94456 -0
  12. package/index.html +25 -4
  13. package/package.json +12 -11
  14. package/src/CdcDashboard.tsx +5 -1
  15. package/src/CdcDashboardComponent.tsx +250 -327
  16. package/src/DashboardContext.tsx +15 -1
  17. package/src/_stories/Dashboard.stories.tsx +158 -40
  18. package/src/_stories/_mock/api-filter-chart.json +11 -35
  19. package/src/_stories/_mock/api-filter-map.json +17 -31
  20. package/src/_stories/_mock/bump-chart.json +3554 -0
  21. package/src/_stories/_mock/methodology.json +412 -0
  22. package/src/_stories/_mock/methodologyAPI.ts +90 -0
  23. package/src/_stories/_mock/multi-viz.json +3 -4
  24. package/src/_stories/_mock/pivot-filter.json +14 -12
  25. package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
  26. package/src/components/CollapsibleVisualizationRow.tsx +44 -0
  27. package/src/components/Column.tsx +1 -1
  28. package/src/components/DashboardFilters/DashboardFilters.tsx +102 -0
  29. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
  30. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
  31. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +477 -0
  32. package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
  33. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +191 -0
  34. package/src/components/DashboardFilters/index.ts +3 -0
  35. package/src/components/DataDesignerModal.tsx +9 -9
  36. package/src/components/ExpandCollapseButtons.tsx +20 -0
  37. package/src/components/Header/Header.tsx +1 -102
  38. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +24 -12
  39. package/src/components/Row.tsx +52 -19
  40. package/src/components/Toggle/Toggle.tsx +2 -4
  41. package/src/components/VisualizationRow.tsx +169 -30
  42. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
  43. package/src/components/VisualizationsPanel/index.ts +1 -0
  44. package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
  45. package/src/components/Widget.tsx +27 -90
  46. package/src/helpers/FilterBehavior.ts +4 -0
  47. package/src/helpers/addValuesToDashboardFilters.ts +49 -0
  48. package/src/helpers/apiFilterHelpers.ts +102 -0
  49. package/src/helpers/changeFilterActive.ts +39 -0
  50. package/src/helpers/filterData.ts +10 -48
  51. package/src/helpers/generateValuesForFilter.ts +1 -1
  52. package/src/helpers/getAutoLoadVisualization.ts +11 -0
  53. package/src/helpers/getFilteredData.ts +7 -5
  54. package/src/helpers/getVizConfig.ts +23 -2
  55. package/src/helpers/getVizRowColumnLocator.ts +2 -1
  56. package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
  57. package/src/helpers/iconHash.tsx +5 -3
  58. package/src/helpers/loadAPIFilters.ts +74 -0
  59. package/src/helpers/mapDataToConfig.ts +29 -0
  60. package/src/helpers/processData.ts +2 -3
  61. package/src/helpers/reloadURLHelpers.ts +78 -0
  62. package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
  63. package/src/helpers/tests/apiFilterHelpers.test.ts +156 -0
  64. package/src/helpers/tests/filterData.test.ts +1 -93
  65. package/src/helpers/tests/getFilteredData.test.ts +86 -0
  66. package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +176 -0
  67. package/src/helpers/tests/reloadURLHelpers.test.ts +195 -0
  68. package/src/scss/editor-panel.scss +1 -1
  69. package/src/scss/grid.scss +34 -27
  70. package/src/scss/main.scss +41 -3
  71. package/src/scss/variables.scss +4 -0
  72. package/src/store/dashboard.actions.ts +12 -4
  73. package/src/store/dashboard.reducer.ts +30 -4
  74. package/src/types/APIFilter.ts +1 -5
  75. package/src/types/ConfigRow.ts +2 -0
  76. package/src/types/Dashboard.ts +1 -1
  77. package/src/types/DashboardConfig.ts +2 -4
  78. package/src/types/DashboardFilters.ts +7 -0
  79. package/src/types/InitialState.ts +1 -1
  80. package/src/types/MultiDashboard.ts +2 -2
  81. package/src/types/SharedFilter.ts +4 -6
  82. package/src/types/Tab.ts +1 -1
  83. package/LICENSE +0 -201
  84. package/src/components/Filters.tsx +0 -88
  85. package/src/components/Header/FilterModal.tsx +0 -510
  86. package/src/components/VisualizationsPanel.tsx +0 -95
  87. package/src/helpers/getApiFilterKey.ts +0 -5
@@ -0,0 +1,477 @@
1
+ import _ from 'lodash'
2
+ import { APIFilter } from '../../../../types/APIFilter'
3
+ import { getVizRowColumnLocator } from '../../../../helpers/getVizRowColumnLocator'
4
+ import { TextField } from '@cdc/core/components/EditorPanel/Inputs'
5
+ import DataTransform from '@cdc/core/helpers/DataTransform'
6
+ import { useEffect, useMemo, useState } from 'react'
7
+ import { SharedFilter } from '../../../../types/SharedFilter'
8
+ import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
9
+ import Tooltip from '@cdc/core/components/ui/Tooltip'
10
+ import Icon from '@cdc/core/components/ui/Icon'
11
+ import MultiSelect from '@cdc/core/components/MultiSelect'
12
+ import { DashboardConfig } from '../../../../types/DashboardConfig'
13
+ import { Visualization } from '@cdc/core/types/Visualization'
14
+ import { hasDashboardApplyBehavior } from '../../../../helpers/hasDashboardApplyBehavior'
15
+
16
+ type FilterEditorProps = {
17
+ config: DashboardConfig
18
+ filter: SharedFilter
19
+ updateFilterProp: (name: keyof SharedFilter, value: any) => void
20
+ }
21
+
22
+ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilterProp }) => {
23
+ const [columns, setColumns] = useState<string[]>([])
24
+ const transform = new DataTransform()
25
+
26
+ const parentFilters: string[] = (config.dashboard.sharedFilters || [])
27
+ .filter(({ key, type }) => key !== filter.key && type !== 'datafilter')
28
+ .map(({ key }) => key)
29
+
30
+ const vizRowColumnLocator = getVizRowColumnLocator(config.rows)
31
+
32
+ const [usedByNameLookup, usedByOptions] = useMemo(() => {
33
+ const nameLookup = {}
34
+ const vizOptions = Object.keys(config.visualizations).filter(vizKey => {
35
+ const vizLookup = vizRowColumnLocator[vizKey]
36
+ if (!vizLookup) return false
37
+ const viz = config.visualizations[vizKey] as Visualization
38
+ if (viz.type === 'dashboardFilters') return false
39
+ const vizName = viz.general?.title || viz.title || vizKey
40
+ nameLookup[vizKey] = vizName
41
+ const notAdded = !filter.usedBy || filter.usedBy.indexOf(vizKey) === -1
42
+ const usesSharedFilter = viz.usesSharedFilter
43
+ const rowIndex = vizLookup.row
44
+ const dataConfiguredOnRow = config.rows[rowIndex].dataKey
45
+ return filter.setBy !== vizKey && notAdded && !usesSharedFilter && !dataConfiguredOnRow
46
+ })
47
+ const rowOptions: number[] = []
48
+
49
+ config.rows.forEach((row, rowIndex) => {
50
+ if (!!row.dataKey) {
51
+ nameLookup[rowIndex] = `Row ${rowIndex + 1}`
52
+ rowOptions.push(rowIndex)
53
+ }
54
+ })
55
+
56
+ const rowsNotSelected = rowOptions.filter(row => !filter.usedBy || filter.usedBy.indexOf(row.toString()) === -1)
57
+ return [nameLookup, [...vizOptions, ...rowsNotSelected]]
58
+ }, [config.visualizations, filter.usedBy, filter.setBy, vizRowColumnLocator])
59
+
60
+ const loadColumnData = async () => {
61
+ const columns = {}
62
+ const dataKeys = Object.keys(config.datasets)
63
+
64
+ for (let i = 0; i < dataKeys.length; i++) {
65
+ const dataKey = dataKeys[i]
66
+ let _dataSet = config.datasets[dataKey]
67
+ if (!_dataSet.data && _dataSet.dataUrl) {
68
+ _dataSet = await fetchRemoteData(_dataSet.dataUrl)
69
+ if (_dataSet.dataDescription) {
70
+ try {
71
+ _dataSet = transform.autoStandardize(_dataSet.data)
72
+ _dataSet = transform.developerStandardize(_dataSet.data, _dataSet.dataDescription)
73
+ } catch (e) {
74
+ //Data not able to be standardized, leave as is
75
+ }
76
+ }
77
+ }
78
+
79
+ if (_dataSet.data) {
80
+ _dataSet.data.forEach(row => {
81
+ Object.keys(row).forEach(columnName => {
82
+ columns[columnName] = true
83
+ })
84
+ })
85
+ }
86
+ }
87
+
88
+ setColumns(Object.keys(columns))
89
+ }
90
+
91
+ useEffect(() => {
92
+ loadColumnData()
93
+ }, [config.datasets])
94
+
95
+ const addFilterUsedBy = (filter, value) => {
96
+ if (value === '') return
97
+ if (!filter.usedBy) filter.usedBy = []
98
+ filter.usedBy.push(value)
99
+ updateFilterProp('usedBy', filter.usedBy)
100
+ }
101
+
102
+ const removeFilterUsedBy = (filter, value) => {
103
+ let usedByIndex = filter.usedBy.indexOf(value)
104
+ if (usedByIndex !== -1) {
105
+ filter.usedBy.splice(usedByIndex, 1)
106
+ updateFilterProp('usedBy', filter.usedBy)
107
+ }
108
+ }
109
+
110
+ const updateAPIFilter = (key: keyof APIFilter, value: string | boolean) => {
111
+ const filterClone = _.cloneDeep(filter)
112
+ const _filter = filterClone.apiFilter || { apiEndpoint: '', valueSelector: '', textSelector: '' }
113
+ const newAPIFilter: APIFilter = { ..._filter, [key]: value }
114
+ updateFilterProp('apiFilter', newAPIFilter)
115
+ }
116
+
117
+ return (
118
+ <>
119
+ <label>
120
+ <span className='edit-label column-heading'>Filter Type: </span>
121
+ <select
122
+ defaultValue={filter.type || ''}
123
+ onChange={e => updateFilterProp('type', e.target.value)}
124
+ disabled={!!filter.type}
125
+ >
126
+ <option value=''>- Select Option -</option>
127
+ <option value='urlfilter'>URL</option>
128
+ <option value='datafilter'>Data</option>
129
+ </select>
130
+ </label>
131
+ {filter.type === 'urlfilter' && (
132
+ <>
133
+ <TextField
134
+ label='Label'
135
+ value={filter.key}
136
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('key', value)}
137
+ />
138
+ {!hasDashboardApplyBehavior(config.visualizations) && (
139
+ <>
140
+ <label>
141
+ <span className='edit-label column-heading'>URL to Filter: </span>
142
+ <select
143
+ defaultValue={filter.datasetKey || ''}
144
+ onChange={e => updateFilterProp('datasetKey', e.target.value)}
145
+ >
146
+ <option value=''>- Select Option -</option>
147
+ {Object.keys(config.datasets).map(datasetKey => {
148
+ if (config.datasets[datasetKey].dataUrl) {
149
+ return (
150
+ <option key={datasetKey} value={datasetKey}>
151
+ {config.datasets[datasetKey].dataUrl}
152
+ </option>
153
+ )
154
+ }
155
+ return null
156
+ })}
157
+ </select>
158
+ </label>
159
+ <label>
160
+ <span className='edit-label column-heading'>Filter By: </span>
161
+ <select
162
+ defaultValue={filter.filterBy || ''}
163
+ onChange={e => updateFilterProp('filterBy', e.target.value)}
164
+ >
165
+ <option value=''>- Select Option -</option>
166
+ <option key={'query-string'} value={'Query String'}>
167
+ Query String
168
+ </option>
169
+ <option key={'file-name'} value={'File Name'}>
170
+ File Name
171
+ </option>
172
+ </select>
173
+ </label>
174
+ {filter.filterBy === 'File Name' && (
175
+ <>
176
+ <TextField
177
+ label='File Name: '
178
+ value={filter.fileName || ''}
179
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('fileName', value)}
180
+ tooltip={
181
+ <Tooltip style={{ textTransform: 'none' }}>
182
+ <Tooltip.Target>
183
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
184
+ </Tooltip.Target>
185
+ <Tooltip.Content>
186
+ <p>{`Add \${query}\ to replace the filename with the active dropdown value.`}</p>
187
+ </Tooltip.Content>
188
+ </Tooltip>
189
+ }
190
+ />
191
+
192
+ <label>
193
+ <span className='edit-label column-heading'>
194
+ White Space Replacments
195
+ <Tooltip style={{ textTransform: 'none' }}>
196
+ <Tooltip.Target>
197
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
198
+ </Tooltip.Target>
199
+ <Tooltip.Content>
200
+ <p>{`Set how whitespace characters will be handled in the file request`}</p>
201
+ </Tooltip.Content>
202
+ </Tooltip>
203
+ </span>
204
+ <select
205
+ defaultValue={filter.whitespaceReplacement || 'Keep Spaces'}
206
+ onChange={e => updateFilterProp('whitespaceReplacement', e.target.value)}
207
+ >
208
+ <option key={'remove-spaces'} value={'Remove Spaces'}>
209
+ Remove Spaces
210
+ </option>
211
+ <option key={'replace-with-underscore'} value={'Replace With Underscore'}>
212
+ Replace With Underscore
213
+ </option>
214
+ <option key={'keep-spaces'} value={'Keep Spaces'}>
215
+ Keep Spaces
216
+ </option>
217
+ </select>
218
+ </label>
219
+ </>
220
+ )}
221
+ </>
222
+ )}
223
+ {filter.filterBy === 'Query String' && (
224
+ <TextField
225
+ label='Query string parameter'
226
+ value={filter.queryParameter}
227
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('queryParameter', value)}
228
+ />
229
+ )}
230
+ <TextField
231
+ label='Filter API Endpoint: '
232
+ value={filter.apiFilter?.apiEndpoint}
233
+ updateField={(_section, _subSection, _key, value) => updateAPIFilter('apiEndpoint', value)}
234
+ />
235
+ <TextField
236
+ label='Option Text Selector:'
237
+ value={filter.apiFilter?.textSelector}
238
+ updateField={(_section, _subSection, _key, value) => updateAPIFilter('textSelector', value)}
239
+ tooltip={
240
+ <>
241
+ <Tooltip style={{ textTransform: 'none' }}>
242
+ <Tooltip.Target>
243
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
244
+ </Tooltip.Target>
245
+ <Tooltip.Content>
246
+ <p>Text to use in the html option element. If none is applied value selector will be used.</p>
247
+ </Tooltip.Content>
248
+ </Tooltip>
249
+ {` * Optional`}
250
+ </>
251
+ }
252
+ />
253
+ <TextField
254
+ label='Option Value Selector:'
255
+ value={filter.apiFilter?.valueSelector}
256
+ updateField={(_section, _subSection, _key, value) => updateAPIFilter('valueSelector', value)}
257
+ tooltip={
258
+ <>
259
+ <Tooltip style={{ textTransform: 'none' }}>
260
+ <Tooltip.Target>
261
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
262
+ </Tooltip.Target>
263
+ <Tooltip.Content>
264
+ <p>Value to use in the html option element</p>
265
+ </Tooltip.Content>
266
+ </Tooltip>
267
+ {` * Required`}
268
+ </>
269
+ }
270
+ />
271
+
272
+ {!!parentFilters.length && (
273
+ <MultiSelect
274
+ label='Parent Filter(s): '
275
+ options={parentFilters.map(key => ({ value: key, label: key }))}
276
+ fieldName='parents'
277
+ selected={filter.parents}
278
+ updateField={(_section, _subsection, _fieldname, newItems) => {
279
+ updateFilterProp('parents', newItems)
280
+ }}
281
+ />
282
+ )}
283
+
284
+ <MultiSelect
285
+ label='Used By: (optional)'
286
+ tooltip={
287
+ <Tooltip style={{ textTransform: 'none' }}>
288
+ <Tooltip.Target>
289
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
290
+ </Tooltip.Target>
291
+ <Tooltip.Content>
292
+ <p>
293
+ Select if you would like specific visualizations or rows to use this filter. Otherwise the filter
294
+ will be added to all api requests.
295
+ </p>
296
+ </Tooltip.Content>
297
+ </Tooltip>
298
+ }
299
+ options={[...usedByOptions, ...(filter.usedBy || [])].map(opt => ({
300
+ value: opt,
301
+ label: usedByNameLookup[opt]
302
+ }))}
303
+ fieldName='usedBy'
304
+ selected={filter.usedBy}
305
+ updateField={(_section, _subsection, _fieldname, newItems) => {
306
+ updateFilterProp('usedBy', newItems)
307
+ }}
308
+ />
309
+
310
+ <TextField
311
+ label='Reset Label: '
312
+ value={filter.resetLabel || ''}
313
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)}
314
+ />
315
+
316
+ <TextField
317
+ label='Default Value Set By Query String Parameter: '
318
+ value={filter.setByQueryParameter || ''}
319
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('setByQueryParameter', value)}
320
+ />
321
+ </>
322
+ )}
323
+ {filter.type === 'datafilter' && (
324
+ <>
325
+ <label>
326
+ <span className='edit-label column-heading'>Filter: </span>
327
+ <select
328
+ value={filter.columnName}
329
+ onChange={e => {
330
+ updateFilterProp('columnName', e.target.value)
331
+ }}
332
+ >
333
+ <option value=''>- Select Option -</option>
334
+ {columns.map(dataKey => (
335
+ <option value={dataKey} key={`filter-column-select-item-${dataKey}`}>
336
+ {dataKey}
337
+ </option>
338
+ ))}
339
+ </select>
340
+ </label>
341
+
342
+ <TextField
343
+ label='Label'
344
+ value={filter.key}
345
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('key', value)}
346
+ />
347
+
348
+ <label>
349
+ <span className='edit-label column-heading'>Show Dropdown</span>
350
+ <input
351
+ type='checkbox'
352
+ defaultChecked={filter.showDropdown === true}
353
+ onChange={e => {
354
+ updateFilterProp('showDropdown', !filter.showDropdown)
355
+ }}
356
+ />
357
+ </label>
358
+
359
+ <label>
360
+ <span className='edit-label column-heading'>Set By: </span>
361
+ <select value={filter.setBy} onChange={e => updateFilterProp('setBy', e.target.value)}>
362
+ <option value=''>- Select Option -</option>
363
+ {Object.keys(config.visualizations)
364
+ .filter(vizKey => config.visualizations[vizKey].type !== 'dashboardFilters')
365
+ .map(vizKey => {
366
+ const viz = config.visualizations[vizKey] as Visualization
367
+ return (
368
+ <option value={vizKey} key={`set-by-select-item-${vizKey}`}>
369
+ {viz.general?.title || viz.title || vizKey}
370
+ </option>
371
+ )
372
+ })}
373
+ </select>
374
+ </label>
375
+ <label>
376
+ <span className='edit-label column-heading'>Used By: </span>
377
+ <ul>
378
+ {filter.usedBy &&
379
+ filter.usedBy.map(opt => (
380
+ <li key={`used-by-list-item-${opt}`}>
381
+ <span>{usedByNameLookup[opt] || opt}</span>{' '}
382
+ <button
383
+ onClick={e => {
384
+ e.preventDefault()
385
+ removeFilterUsedBy(filter, opt)
386
+ }}
387
+ >
388
+ X
389
+ </button>
390
+ </li>
391
+ ))}
392
+ </ul>
393
+ <select value='' onChange={e => addFilterUsedBy(filter, e.target.value)}>
394
+ <option value=''>- Select Option -</option>
395
+ {usedByOptions.map(opt => (
396
+ <option value={opt} key={`used-by-select-item-${opt}`}>
397
+ {usedByNameLookup[opt] || opt}
398
+ </option>
399
+ ))}
400
+ </select>
401
+ </label>
402
+ <TextField
403
+ label='Reset Label: '
404
+ value={filter.resetLabel || ''}
405
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)}
406
+ />
407
+
408
+ <label>
409
+ <span className='edit-label column-heading'>Parent Filter: </span>
410
+ <select
411
+ value={filter.parents || []}
412
+ onChange={e => {
413
+ updateFilterProp('parents', e.target.value)
414
+ }}
415
+ >
416
+ <option value=''>Select a filter</option>
417
+ {config.dashboard.sharedFilters &&
418
+ config.dashboard.sharedFilters.map(sharedFilter => {
419
+ if (sharedFilter.key !== filter.key) {
420
+ return <option key={sharedFilter.key}>{sharedFilter.key}</option>
421
+ }
422
+ })}
423
+ </select>
424
+ </label>
425
+
426
+ <TextField
427
+ label='Default Value Set By Query String Parameter: '
428
+ value={filter.setByQueryParameter || ''}
429
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('setByQueryParameter', value)}
430
+ />
431
+ </>
432
+ )}
433
+ <label>
434
+ <span className='mr-1'>Multi Select</span>
435
+ <input
436
+ type='checkbox'
437
+ checked={filter.multiSelect}
438
+ onChange={e => {
439
+ updateFilterProp('multiSelect', !filter.multiSelect)
440
+ }}
441
+ />
442
+ </label>
443
+
444
+ {filter.multiSelect && (
445
+ <TextField
446
+ label='Select Limit'
447
+ value={filter.selectLimit}
448
+ updateField={(_section, _subSection, _field, value) => updateFilterProp('selectLimit', value)}
449
+ type='number'
450
+ tooltip={
451
+ <Tooltip style={{ textTransform: 'none' }}>
452
+ <Tooltip.Target>
453
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
454
+ </Tooltip.Target>
455
+ <Tooltip.Content>
456
+ <p>The maximum number of items that can be selected.</p>
457
+ </Tooltip.Content>
458
+ </Tooltip>
459
+ }
460
+ />
461
+ )}
462
+
463
+ <label>
464
+ <span className='mr-1'>Show Dropdown</span>
465
+ <input
466
+ type='checkbox'
467
+ checked={filter.showDropdown}
468
+ onChange={e => {
469
+ updateFilterProp('showDropdown', !filter.showDropdown)
470
+ }}
471
+ />
472
+ </label>
473
+ </>
474
+ )
475
+ }
476
+
477
+ export default FilterEditor
@@ -0,0 +1 @@
1
+ export { default } from './DashboardFiltersEditor'
@@ -0,0 +1,191 @@
1
+ import { useContext, useState } from 'react'
2
+ import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
3
+ import Filters from './DashboardFilters'
4
+ import { changeFilterActive } from '../../helpers/changeFilterActive'
5
+ import _ from 'lodash'
6
+ import { FilterBehavior } from '../../helpers/FilterBehavior'
7
+ import { getFilteredData } from '../../helpers/getFilteredData'
8
+ import { DashboardFilters } from '../../types/DashboardFilters'
9
+ import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
10
+ import Layout from '@cdc/core/components/Layout'
11
+ import DashboardFiltersEditor from './DashboardFiltersEditor'
12
+ import { ViewPort } from '@cdc/core/types/ViewPort'
13
+ import { hasDashboardApplyBehavior } from '../../helpers/hasDashboardApplyBehavior'
14
+ import * as apiFilterHelpers from '../../helpers/apiFilterHelpers'
15
+
16
+ export type DropdownOptions = Record<'value' | 'text', string>[]
17
+
18
+ /** the cached dropdown options for each filter */
19
+ export type APIFilterDropdowns = {
20
+ // null means still loading
21
+ [dropdownsKey: string]: null | DropdownOptions
22
+ }
23
+
24
+ type DashboardFiltersProps = {
25
+ apiFilterDropdowns: APIFilterDropdowns
26
+ visualizationConfig: DashboardFilters
27
+ isEditor?: boolean
28
+ setConfig: (config: DashboardFilters) => void
29
+ currentViewport?: ViewPort
30
+ }
31
+
32
+ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
33
+ apiFilterDropdowns,
34
+ visualizationConfig,
35
+ setConfig: updateConfig,
36
+ currentViewport,
37
+ isEditor = false
38
+ }) => {
39
+ const state = useContext(DashboardContext)
40
+ const { config: dashboardConfig, reloadURLData, loadAPIFilters, setAPIFilterDropdowns } = state
41
+ const dispatch = useContext(DashboardDispatchContext)
42
+
43
+ const applyFilters = () => {
44
+ const dashboardConfig = _.cloneDeep(state.config.dashboard)
45
+ const nonAutoLoadFilterIndexes = Object.values(state.config.visualizations)
46
+ .filter(v => v.type === 'dashboardFilters')
47
+ .reduce((acc, viz: DashboardFilters) => (!viz.autoLoad ? [...acc, viz.sharedFilterIndexes] : acc), [])
48
+ const allRequiredFiltersSelected = !dashboardConfig.sharedFilters.some((filter, filterIndex) => {
49
+ if (nonAutoLoadFilterIndexes.includes(filterIndex)) {
50
+ return !filter.active && !filter.queuedActive
51
+ } else {
52
+ // autoload filters don't need to be selected to apply filters
53
+ return false
54
+ }
55
+ })
56
+ if (allRequiredFiltersSelected) {
57
+ if (hasDashboardApplyBehavior(state.config.visualizations)) {
58
+ const queryParams = getQueryParams()
59
+ let needsQueryUpdate = false
60
+ dashboardConfig.sharedFilters.forEach((sharedFilter, index) => {
61
+ if (sharedFilter.queuedActive) {
62
+ dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
63
+ delete dashboardConfig.sharedFilters[index].queuedActive
64
+
65
+ if (
66
+ sharedFilter.setByQueryParameter &&
67
+ queryParams[sharedFilter.setByQueryParameter] !== sharedFilter.active
68
+ ) {
69
+ queryParams[sharedFilter.setByQueryParameter] = Array.isArray(sharedFilter.active)
70
+ ? sharedFilter.active.join(',')
71
+ : sharedFilter.active
72
+ needsQueryUpdate = true
73
+ }
74
+ }
75
+ })
76
+
77
+ if (needsQueryUpdate) {
78
+ updateQueryString(queryParams)
79
+ }
80
+ }
81
+
82
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
83
+ dispatch({ type: 'SET_FILTERED_DATA', payload: getFilteredData(_.cloneDeep(state)) })
84
+ loadAPIFilters(dashboardConfig.sharedFilters, apiFilterDropdowns)
85
+ .then(newFilters => {
86
+ reloadURLData(newFilters)
87
+ })
88
+ .catch(e => {
89
+ console.error(e)
90
+ })
91
+ } else {
92
+ // TODO noftify of required fields
93
+ }
94
+ }
95
+
96
+ const handleOnChange = (index: number, value: string | string[]) => {
97
+ const newConfig = _.cloneDeep(dashboardConfig)
98
+ let [newSharedFilters, changedFilterIndexes] = changeFilterActive(
99
+ index,
100
+ value,
101
+ newConfig.dashboard.sharedFilters,
102
+ visualizationConfig
103
+ )
104
+
105
+ if (hasDashboardApplyBehavior(dashboardConfig.visualizations)) {
106
+ const isAutoSelectFilter = visualizationConfig.autoLoad
107
+ const missingFilterSelections = newConfig.dashboard.sharedFilters.some(f => !f.active)
108
+ const apiEndpoints = newSharedFilters.filter(f => f.apiFilter).map(f => f.apiFilter.apiEndpoint)
109
+ const loadingFilterMemo = apiFilterHelpers.getLoadingFilterMemo(
110
+ apiEndpoints,
111
+ apiFilterDropdowns,
112
+ changedFilterIndexes
113
+ )
114
+ if (isAutoSelectFilter && !missingFilterSelections) {
115
+ // a dropdown has been selected that doesn't
116
+ // require the Go Button
117
+ setAPIFilterDropdowns(loadingFilterMemo)
118
+ loadAPIFilters(newSharedFilters, loadingFilterMemo).then(filters => {
119
+ reloadURLData(filters)
120
+ })
121
+ } else {
122
+ newSharedFilters[index].queuedActive = value
123
+ // setData to empty object because we no longer have a data state.
124
+ dispatch({ type: 'SET_DATA', payload: {} })
125
+ dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
126
+ setAPIFilterDropdowns(loadingFilterMemo)
127
+ loadAPIFilters(newSharedFilters, loadingFilterMemo)
128
+ }
129
+ } else {
130
+ if (newSharedFilters[index].apiFilter) {
131
+ reloadURLData(newSharedFilters)
132
+ } else {
133
+ const clonedState = _.cloneDeep(state)
134
+ clonedState.config.dashboard.sharedFilters = newSharedFilters
135
+ const newFilteredData = getFilteredData(clonedState)
136
+ dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
137
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
138
+ }
139
+ }
140
+ }
141
+ const [displayPanel, setDisplayPanel] = useState(true)
142
+ const onBackClick = () => {
143
+ setDisplayPanel(!displayPanel)
144
+ updateConfig({
145
+ ...visualizationConfig,
146
+ showEditorPanel: !displayPanel
147
+ })
148
+ }
149
+
150
+ // if all of the filters are hidden filters don't display the VisualizationWrapper
151
+ const filters = visualizationConfig?.sharedFilterIndexes
152
+ ?.map(Number)
153
+ .map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
154
+
155
+ const displayNone = filters.length ? filters.every(filter => filter.showDropdown === false) : false
156
+ if (displayNone && !isEditor) return <></>
157
+ return (
158
+ <Layout.VisualizationWrapper config={visualizationConfig} isEditor={isEditor} currentViewport={currentViewport}>
159
+ {isEditor && (
160
+ <Layout.Sidebar
161
+ displayPanel={displayPanel}
162
+ isDashboard={true}
163
+ title={'Configure Dashboard Filters'}
164
+ onBackClick={onBackClick}
165
+ >
166
+ <DashboardFiltersEditor updateConfig={updateConfig} vizConfig={visualizationConfig} />
167
+ </Layout.Sidebar>
168
+ )}
169
+
170
+ {!displayNone && (
171
+ <Layout.Responsive isEditor={isEditor}>
172
+ <div
173
+ className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''} cove-component__content col-12`}
174
+ >
175
+ <Filters
176
+ show={visualizationConfig?.sharedFilterIndexes?.map(Number)}
177
+ filters={dashboardConfig.dashboard.sharedFilters || []}
178
+ apiFilterDropdowns={apiFilterDropdowns}
179
+ handleOnChange={handleOnChange}
180
+ />
181
+ {visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad && (
182
+ <button onClick={applyFilters}>GO!</button>
183
+ )}
184
+ </div>
185
+ </Layout.Responsive>
186
+ )}
187
+ </Layout.VisualizationWrapper>
188
+ )
189
+ }
190
+
191
+ export default DashboardFiltersWrapper
@@ -0,0 +1,3 @@
1
+ export { default } from './DashboardFiltersWrapper'
2
+
3
+ export type { APIFilterDropdowns, DropdownOptions } from './DashboardFiltersWrapper'