@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.
- package/dist/cdcdashboard.js +144406 -127510
- package/examples/custom/css/respiratory.css +236 -0
- package/examples/custom/js/respiratory.js +242 -0
- package/examples/default-multi-dataset-shared-filter.json +1729 -0
- package/examples/ed-visits-county-file.json +618 -0
- package/examples/filtered-dash.json +6 -21
- package/examples/single-state-dashboard-filters.json +421 -0
- package/examples/state-level.json +90136 -0
- package/examples/state-points.json +10474 -0
- package/examples/test-file.json +147 -0
- package/examples/testing.json +94456 -0
- package/index.html +25 -4
- package/package.json +12 -11
- package/src/CdcDashboard.tsx +5 -1
- package/src/CdcDashboardComponent.tsx +250 -327
- package/src/DashboardContext.tsx +15 -1
- package/src/_stories/Dashboard.stories.tsx +158 -40
- package/src/_stories/_mock/api-filter-chart.json +11 -35
- package/src/_stories/_mock/api-filter-map.json +17 -31
- package/src/_stories/_mock/bump-chart.json +3554 -0
- package/src/_stories/_mock/methodology.json +412 -0
- package/src/_stories/_mock/methodologyAPI.ts +90 -0
- package/src/_stories/_mock/multi-viz.json +3 -4
- package/src/_stories/_mock/pivot-filter.json +14 -12
- package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
- package/src/components/CollapsibleVisualizationRow.tsx +44 -0
- package/src/components/Column.tsx +1 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +102 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +477 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +191 -0
- package/src/components/DashboardFilters/index.ts +3 -0
- package/src/components/DataDesignerModal.tsx +9 -9
- package/src/components/ExpandCollapseButtons.tsx +20 -0
- package/src/components/Header/Header.tsx +1 -102
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +24 -12
- package/src/components/Row.tsx +52 -19
- package/src/components/Toggle/Toggle.tsx +2 -4
- package/src/components/VisualizationRow.tsx +169 -30
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
- package/src/components/VisualizationsPanel/index.ts +1 -0
- package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
- package/src/components/Widget.tsx +27 -90
- package/src/helpers/FilterBehavior.ts +4 -0
- package/src/helpers/addValuesToDashboardFilters.ts +49 -0
- package/src/helpers/apiFilterHelpers.ts +102 -0
- package/src/helpers/changeFilterActive.ts +39 -0
- package/src/helpers/filterData.ts +10 -48
- package/src/helpers/generateValuesForFilter.ts +1 -1
- package/src/helpers/getAutoLoadVisualization.ts +11 -0
- package/src/helpers/getFilteredData.ts +7 -5
- package/src/helpers/getVizConfig.ts +23 -2
- package/src/helpers/getVizRowColumnLocator.ts +2 -1
- package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
- package/src/helpers/iconHash.tsx +5 -3
- package/src/helpers/loadAPIFilters.ts +74 -0
- package/src/helpers/mapDataToConfig.ts +29 -0
- package/src/helpers/processData.ts +2 -3
- package/src/helpers/reloadURLHelpers.ts +78 -0
- package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
- package/src/helpers/tests/apiFilterHelpers.test.ts +156 -0
- package/src/helpers/tests/filterData.test.ts +1 -93
- package/src/helpers/tests/getFilteredData.test.ts +86 -0
- package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +176 -0
- package/src/helpers/tests/reloadURLHelpers.test.ts +195 -0
- package/src/scss/editor-panel.scss +1 -1
- package/src/scss/grid.scss +34 -27
- package/src/scss/main.scss +41 -3
- package/src/scss/variables.scss +4 -0
- package/src/store/dashboard.actions.ts +12 -4
- package/src/store/dashboard.reducer.ts +30 -4
- package/src/types/APIFilter.ts +1 -5
- package/src/types/ConfigRow.ts +2 -0
- package/src/types/Dashboard.ts +1 -1
- package/src/types/DashboardConfig.ts +2 -4
- package/src/types/DashboardFilters.ts +7 -0
- package/src/types/InitialState.ts +1 -1
- package/src/types/MultiDashboard.ts +2 -2
- package/src/types/SharedFilter.ts +4 -6
- package/src/types/Tab.ts +1 -1
- package/LICENSE +0 -201
- package/src/components/Filters.tsx +0 -88
- package/src/components/Header/FilterModal.tsx +0 -510
- package/src/components/VisualizationsPanel.tsx +0 -95
- 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
|