@cdc/dashboard 4.24.4 → 4.24.7
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 +179228 -141419
- 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/index.html +12 -3
- package/package.json +12 -11
- package/src/CdcDashboard.tsx +5 -1
- package/src/CdcDashboardComponent.tsx +156 -334
- package/src/DashboardContext.tsx +9 -1
- package/src/_stories/Dashboard.stories.tsx +31 -3
- package/src/_stories/_mock/dashboard-gallery.json +534 -523
- package/src/_stories/_mock/markup-include.json +78 -0
- package/src/_stories/_mock/multi-dashboards.json +914 -0
- package/src/_stories/_mock/multi-viz.json +2 -3
- package/src/_stories/_mock/pivot-filter.json +15 -11
- package/src/_stories/_mock/standalone-table.json +2 -0
- package/src/components/CollapsibleVisualizationRow.tsx +44 -0
- package/src/components/Column.tsx +1 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +80 -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 +367 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +143 -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 -97
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +4 -4
- package/src/components/MultiConfigTabs/MultiTabs.tsx +3 -2
- package/src/components/Row.tsx +52 -19
- package/src/components/Toggle/Toggle.tsx +2 -4
- package/src/components/VisualizationRow.tsx +96 -29
- 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 +26 -90
- package/src/helpers/apiFilterHelpers.ts +51 -0
- package/src/helpers/changeFilterActive.ts +30 -0
- package/src/helpers/filterData.ts +16 -56
- package/src/helpers/generateValuesForFilter.ts +1 -1
- package/src/helpers/getAutoLoadVisualization.ts +11 -0
- package/src/helpers/getFilteredData.ts +4 -2
- 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 +3 -3
- package/src/helpers/mapDataToConfig.ts +29 -0
- package/src/helpers/processData.ts +2 -3
- package/src/helpers/reloadURLHelpers.ts +68 -0
- package/src/helpers/tests/filterData.test.ts +1 -93
- 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 +9 -10
- package/src/store/dashboard.reducer.ts +41 -13
- package/src/types/APIFilter.ts +1 -4
- 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 +2 -5
- package/src/types/Tab.ts +1 -1
- package/LICENSE +0 -201
- package/src/components/EditorWrapper/EditorWrapper.tsx +0 -52
- package/src/components/EditorWrapper/editor-wrapper.style.css +0 -13
- package/src/components/Filters.tsx +0 -88
- package/src/components/Header/FilterModal.tsx +0 -506
- package/src/components/VisualizationsPanel.tsx +0 -72
- package/src/helpers/getApiFilterKey.ts +0 -5
|
@@ -0,0 +1,367 @@
|
|
|
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 || []).filter(({ key, type }) => key !== filter.key && type !== 'datafilter').map(({ key }) => key)
|
|
27
|
+
|
|
28
|
+
const vizRowColumnLocator = getVizRowColumnLocator(config.rows)
|
|
29
|
+
|
|
30
|
+
const [usedByNameLookup, usedByOptions] = useMemo(() => {
|
|
31
|
+
const nameLookup = {}
|
|
32
|
+
const vizOptions = Object.keys(config.visualizations)
|
|
33
|
+
.filter(vizKey => {
|
|
34
|
+
const vizLookup = vizRowColumnLocator[vizKey]
|
|
35
|
+
if (!vizLookup) return false
|
|
36
|
+
const viz = config.visualizations[vizKey]
|
|
37
|
+
if (viz.type === 'dashboardFilters') return false
|
|
38
|
+
const notAdded = !filter.usedBy || filter.usedBy.indexOf(vizKey) === -1
|
|
39
|
+
const usesSharedFilter = viz.usesSharedFilter
|
|
40
|
+
const rowIndex = vizLookup.row
|
|
41
|
+
const dataConfiguredOnRow = config.rows[rowIndex].dataKey
|
|
42
|
+
return filter.setBy !== vizKey && notAdded && !usesSharedFilter && !dataConfiguredOnRow
|
|
43
|
+
})
|
|
44
|
+
.map(vizKey => {
|
|
45
|
+
const viz = config.visualizations[vizKey] as Visualization
|
|
46
|
+
const vizName = viz.general?.title || viz.title || vizKey
|
|
47
|
+
nameLookup[vizKey] = vizName
|
|
48
|
+
return vizKey
|
|
49
|
+
})
|
|
50
|
+
const rowOptions: number[] = []
|
|
51
|
+
|
|
52
|
+
config.rows.forEach((row, rowIndex) => {
|
|
53
|
+
if (!!row.dataKey) {
|
|
54
|
+
nameLookup[rowIndex] = `Row ${rowIndex + 1}`
|
|
55
|
+
rowOptions.push(rowIndex)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const rowsNotSelected = rowOptions.filter(row => !filter.usedBy || filter.usedBy.indexOf(row.toString()) === -1)
|
|
60
|
+
return [nameLookup, [...vizOptions, ...rowsNotSelected]]
|
|
61
|
+
}, [config.visualizations, filter.usedBy, filter.setBy, vizRowColumnLocator])
|
|
62
|
+
|
|
63
|
+
const loadColumnData = async () => {
|
|
64
|
+
const columns = {}
|
|
65
|
+
const dataKeys = Object.keys(config.datasets)
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < dataKeys.length; i++) {
|
|
68
|
+
const dataKey = dataKeys[i]
|
|
69
|
+
let _dataSet = config.datasets[dataKey]
|
|
70
|
+
if (!_dataSet.data && _dataSet.dataUrl) {
|
|
71
|
+
_dataSet = await fetchRemoteData(_dataSet.dataUrl)
|
|
72
|
+
if (_dataSet.dataDescription) {
|
|
73
|
+
try {
|
|
74
|
+
_dataSet = transform.autoStandardize(_dataSet.data)
|
|
75
|
+
_dataSet = transform.developerStandardize(_dataSet.data, _dataSet.dataDescription)
|
|
76
|
+
} catch (e) {
|
|
77
|
+
//Data not able to be standardized, leave as is
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (_dataSet.data) {
|
|
83
|
+
_dataSet.data.forEach(row => {
|
|
84
|
+
Object.keys(row).forEach(columnName => {
|
|
85
|
+
columns[columnName] = true
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setColumns(Object.keys(columns))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
loadColumnData()
|
|
96
|
+
}, [config.datasets])
|
|
97
|
+
|
|
98
|
+
const addFilterUsedBy = (filter, value) => {
|
|
99
|
+
if (value === '') return
|
|
100
|
+
if (!filter.usedBy) filter.usedBy = []
|
|
101
|
+
filter.usedBy.push(value)
|
|
102
|
+
updateFilterProp('usedBy', filter.usedBy)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const removeFilterUsedBy = (filter, value) => {
|
|
106
|
+
let usedByIndex = filter.usedBy.indexOf(value)
|
|
107
|
+
if (usedByIndex !== -1) {
|
|
108
|
+
filter.usedBy.splice(usedByIndex, 1)
|
|
109
|
+
updateFilterProp('usedBy', filter.usedBy)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const updateAPIFilter = (key: keyof APIFilter, value: string | boolean) => {
|
|
114
|
+
const filterClone = _.cloneDeep(filter)
|
|
115
|
+
const _filter = filterClone.apiFilter || { apiEndpoint: '', valueSelector: '', textSelector: '' }
|
|
116
|
+
const newAPIFilter: APIFilter = { ..._filter, [key]: value }
|
|
117
|
+
updateFilterProp('apiFilter', newAPIFilter)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
<label>
|
|
123
|
+
<span className='edit-label column-heading'>Filter Type: </span>
|
|
124
|
+
<select defaultValue={filter.type || ''} onChange={e => updateFilterProp('type', e.target.value)} disabled={!!filter.type}>
|
|
125
|
+
<option value=''>- Select Option -</option>
|
|
126
|
+
<option value='urlfilter'>URL</option>
|
|
127
|
+
<option value='datafilter'>Data</option>
|
|
128
|
+
</select>
|
|
129
|
+
</label>
|
|
130
|
+
{filter.type === 'urlfilter' && (
|
|
131
|
+
<>
|
|
132
|
+
<TextField label='Label' value={filter.key} updateField={(_section, _subSection, _key, value) => updateFilterProp('key', value)} />
|
|
133
|
+
{!hasDashboardApplyBehavior(config.visualizations) && (
|
|
134
|
+
<>
|
|
135
|
+
<label>
|
|
136
|
+
<span className='edit-label column-heading'>URL to Filter: </span>
|
|
137
|
+
<select defaultValue={filter.datasetKey || ''} onChange={e => updateFilterProp('datasetKey', e.target.value)}>
|
|
138
|
+
<option value=''>- Select Option -</option>
|
|
139
|
+
{Object.keys(config.datasets).map(datasetKey => {
|
|
140
|
+
if (config.datasets[datasetKey].dataUrl) {
|
|
141
|
+
return (
|
|
142
|
+
<option key={datasetKey} value={datasetKey}>
|
|
143
|
+
{config.datasets[datasetKey].dataUrl}
|
|
144
|
+
</option>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return null
|
|
148
|
+
})}
|
|
149
|
+
</select>
|
|
150
|
+
</label>
|
|
151
|
+
<label>
|
|
152
|
+
<span className='edit-label column-heading'>Filter By: </span>
|
|
153
|
+
<select defaultValue={filter.filterBy || ''} onChange={e => updateFilterProp('filterBy', e.target.value)}>
|
|
154
|
+
<option value=''>- Select Option -</option>
|
|
155
|
+
<option key={'query-string'} value={'Query String'}>
|
|
156
|
+
Query String
|
|
157
|
+
</option>
|
|
158
|
+
<option key={'file-name'} value={'File Name'}>
|
|
159
|
+
File Name
|
|
160
|
+
</option>
|
|
161
|
+
</select>
|
|
162
|
+
</label>
|
|
163
|
+
{filter.filterBy === 'File Name' && (
|
|
164
|
+
<>
|
|
165
|
+
<TextField
|
|
166
|
+
label='File Name: '
|
|
167
|
+
value={filter.fileName || ''}
|
|
168
|
+
updateField={(_section, _subSection, _key, value) => updateFilterProp('fileName', value)}
|
|
169
|
+
tooltip={
|
|
170
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
171
|
+
<Tooltip.Target>
|
|
172
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
173
|
+
</Tooltip.Target>
|
|
174
|
+
<Tooltip.Content>
|
|
175
|
+
<p>{`Add \${query}\ to replace the filename with the active dropdown value.`}</p>
|
|
176
|
+
</Tooltip.Content>
|
|
177
|
+
</Tooltip>
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<label>
|
|
182
|
+
<span className='edit-label column-heading'>
|
|
183
|
+
White Space Replacments
|
|
184
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
185
|
+
<Tooltip.Target>
|
|
186
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
187
|
+
</Tooltip.Target>
|
|
188
|
+
<Tooltip.Content>
|
|
189
|
+
<p>{`Set how whitespace characters will be handled in the file request`}</p>
|
|
190
|
+
</Tooltip.Content>
|
|
191
|
+
</Tooltip>
|
|
192
|
+
</span>
|
|
193
|
+
<select defaultValue={filter.whitespaceReplacement || 'Keep Spaces'} onChange={e => updateFilterProp('whitespaceReplacement', e.target.value)}>
|
|
194
|
+
<option key={'remove-spaces'} value={'Remove Spaces'}>
|
|
195
|
+
Remove Spaces
|
|
196
|
+
</option>
|
|
197
|
+
<option key={'replace-with-underscore'} value={'Replace With Underscore'}>
|
|
198
|
+
Replace With Underscore
|
|
199
|
+
</option>
|
|
200
|
+
<option key={'keep-spaces'} value={'Keep Spaces'}>
|
|
201
|
+
Keep Spaces
|
|
202
|
+
</option>
|
|
203
|
+
</select>
|
|
204
|
+
</label>
|
|
205
|
+
</>
|
|
206
|
+
)}
|
|
207
|
+
</>
|
|
208
|
+
)}
|
|
209
|
+
{filter.filterBy === 'Query String' && <TextField label='Query string parameter' value={filter.queryParameter} updateField={(_section, _subSection, _key, value) => updateFilterProp('queryParameter', value)} />}
|
|
210
|
+
<TextField label='Filter API Endpoint: ' value={filter.apiFilter?.apiEndpoint} updateField={(_section, _subSection, _key, value) => updateAPIFilter('apiEndpoint', value)} />
|
|
211
|
+
<TextField
|
|
212
|
+
label='Option Text Selector:'
|
|
213
|
+
value={filter.apiFilter?.textSelector}
|
|
214
|
+
updateField={(_section, _subSection, _key, value) => updateAPIFilter('textSelector', value)}
|
|
215
|
+
tooltip={
|
|
216
|
+
<>
|
|
217
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
218
|
+
<Tooltip.Target>
|
|
219
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
220
|
+
</Tooltip.Target>
|
|
221
|
+
<Tooltip.Content>
|
|
222
|
+
<p>Text to use in the html option element. If none is applied value selector will be used.</p>
|
|
223
|
+
</Tooltip.Content>
|
|
224
|
+
</Tooltip>
|
|
225
|
+
{` * Optional`}
|
|
226
|
+
</>
|
|
227
|
+
}
|
|
228
|
+
/>
|
|
229
|
+
<TextField
|
|
230
|
+
label='Option Value Selector:'
|
|
231
|
+
value={filter.apiFilter?.valueSelector}
|
|
232
|
+
updateField={(_section, _subSection, _key, value) => updateAPIFilter('valueSelector', value)}
|
|
233
|
+
tooltip={
|
|
234
|
+
<>
|
|
235
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
236
|
+
<Tooltip.Target>
|
|
237
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
238
|
+
</Tooltip.Target>
|
|
239
|
+
<Tooltip.Content>
|
|
240
|
+
<p>Value to use in the html option element</p>
|
|
241
|
+
</Tooltip.Content>
|
|
242
|
+
</Tooltip>
|
|
243
|
+
{` * Required`}
|
|
244
|
+
</>
|
|
245
|
+
}
|
|
246
|
+
/>
|
|
247
|
+
|
|
248
|
+
{!!parentFilters.length && (
|
|
249
|
+
<MultiSelect
|
|
250
|
+
label='Parent Filter(s): '
|
|
251
|
+
options={parentFilters.map(key => ({ value: key, label: key }))}
|
|
252
|
+
fieldName='parents'
|
|
253
|
+
selected={filter.parents}
|
|
254
|
+
updateField={(_section, _subsection, _fieldname, newItems) => {
|
|
255
|
+
updateFilterProp('parents', newItems)
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
<TextField label='Reset Label: ' value={filter.resetLabel || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)} />
|
|
261
|
+
|
|
262
|
+
<TextField label='Default Value Set By Query String Parameter: ' value={filter.setByQueryParameter || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('setByQueryParameter', value)} />
|
|
263
|
+
</>
|
|
264
|
+
)}
|
|
265
|
+
{filter.type === 'datafilter' && (
|
|
266
|
+
<>
|
|
267
|
+
<label>
|
|
268
|
+
<span className='edit-label column-heading'>Filter: </span>
|
|
269
|
+
<select
|
|
270
|
+
value={filter.columnName}
|
|
271
|
+
onChange={e => {
|
|
272
|
+
updateFilterProp('columnName', e.target.value)
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
<option value=''>- Select Option -</option>
|
|
276
|
+
{columns.map(dataKey => (
|
|
277
|
+
<option value={dataKey} key={`filter-column-select-item-${dataKey}`}>
|
|
278
|
+
{dataKey}
|
|
279
|
+
</option>
|
|
280
|
+
))}
|
|
281
|
+
</select>
|
|
282
|
+
</label>
|
|
283
|
+
|
|
284
|
+
<TextField label='Label' value={filter.key} updateField={(_section, _subSection, _key, value) => updateFilterProp('key', value)} />
|
|
285
|
+
|
|
286
|
+
<label>
|
|
287
|
+
<span className='edit-label column-heading'>Show Dropdown</span>
|
|
288
|
+
<input
|
|
289
|
+
type='checkbox'
|
|
290
|
+
defaultChecked={filter.showDropdown === true}
|
|
291
|
+
onChange={e => {
|
|
292
|
+
updateFilterProp('showDropdown', !filter.showDropdown)
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
</label>
|
|
296
|
+
|
|
297
|
+
<label>
|
|
298
|
+
<span className='edit-label column-heading'>Set By: </span>
|
|
299
|
+
<select value={filter.setBy} onChange={e => updateFilterProp('setBy', e.target.value)}>
|
|
300
|
+
<option value=''>- Select Option -</option>
|
|
301
|
+
{Object.keys(config.visualizations)
|
|
302
|
+
.filter(vizKey => config.visualizations[vizKey].type !== 'dashboardFilters')
|
|
303
|
+
.map(vizKey => {
|
|
304
|
+
const viz = config.visualizations[vizKey] as Visualization
|
|
305
|
+
return (
|
|
306
|
+
<option value={vizKey} key={`set-by-select-item-${vizKey}`}>
|
|
307
|
+
{viz.general?.title || viz.title || vizKey}
|
|
308
|
+
</option>
|
|
309
|
+
)
|
|
310
|
+
})}
|
|
311
|
+
</select>
|
|
312
|
+
</label>
|
|
313
|
+
<label>
|
|
314
|
+
<span className='edit-label column-heading'>Used By: </span>
|
|
315
|
+
<ul>
|
|
316
|
+
{filter.usedBy &&
|
|
317
|
+
filter.usedBy.map(opt => (
|
|
318
|
+
<li key={`used-by-list-item-${opt}`}>
|
|
319
|
+
<span>{usedByNameLookup[opt] || opt}</span>{' '}
|
|
320
|
+
<button
|
|
321
|
+
onClick={e => {
|
|
322
|
+
e.preventDefault()
|
|
323
|
+
removeFilterUsedBy(filter, opt)
|
|
324
|
+
}}
|
|
325
|
+
>
|
|
326
|
+
X
|
|
327
|
+
</button>
|
|
328
|
+
</li>
|
|
329
|
+
))}
|
|
330
|
+
</ul>
|
|
331
|
+
<select value='' onChange={e => addFilterUsedBy(filter, e.target.value)}>
|
|
332
|
+
<option value=''>- Select Option -</option>
|
|
333
|
+
{usedByOptions.map(opt => (
|
|
334
|
+
<option value={opt} key={`used-by-select-item-${opt}`}>
|
|
335
|
+
{usedByNameLookup[opt] || opt}
|
|
336
|
+
</option>
|
|
337
|
+
))}
|
|
338
|
+
</select>
|
|
339
|
+
</label>
|
|
340
|
+
<TextField label='Reset Label: ' value={filter.resetLabel || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)} />
|
|
341
|
+
|
|
342
|
+
<label>
|
|
343
|
+
<span className='edit-label column-heading'>Parent Filter: </span>
|
|
344
|
+
<select
|
|
345
|
+
value={filter.parents || []}
|
|
346
|
+
onChange={e => {
|
|
347
|
+
updateFilterProp('parents', e.target.value)
|
|
348
|
+
}}
|
|
349
|
+
>
|
|
350
|
+
<option value=''>Select a filter</option>
|
|
351
|
+
{config.dashboard.sharedFilters &&
|
|
352
|
+
config.dashboard.sharedFilters.map(sharedFilter => {
|
|
353
|
+
if (sharedFilter.key !== filter.key) {
|
|
354
|
+
return <option key={sharedFilter.key}>{sharedFilter.key}</option>
|
|
355
|
+
}
|
|
356
|
+
})}
|
|
357
|
+
</select>
|
|
358
|
+
</label>
|
|
359
|
+
|
|
360
|
+
<TextField label='Default Value Set By Query String Parameter: ' value={filter.setByQueryParameter || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('setByQueryParameter', value)} />
|
|
361
|
+
</>
|
|
362
|
+
)}
|
|
363
|
+
</>
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export default FilterEditor
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './DashboardFiltersEditor'
|
|
@@ -0,0 +1,143 @@
|
|
|
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 '../Header/Header'
|
|
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
|
+
|
|
15
|
+
export type DropdownOptions = Record<'value' | 'text', string>[]
|
|
16
|
+
|
|
17
|
+
export type APIFilterDropdowns = {
|
|
18
|
+
// null means still loading
|
|
19
|
+
[filtername: string]: null | DropdownOptions
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type DashboardFiltersProps = {
|
|
23
|
+
apiFilterDropdowns: APIFilterDropdowns
|
|
24
|
+
visualizationConfig: DashboardFilters
|
|
25
|
+
isEditor?: boolean
|
|
26
|
+
setConfig: (config: DashboardFilters) => void
|
|
27
|
+
currentViewport?: ViewPort
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({ apiFilterDropdowns, visualizationConfig, setConfig: updateConfig, currentViewport, isEditor = false }) => {
|
|
31
|
+
const state = useContext(DashboardContext)
|
|
32
|
+
const { config: dashboardConfig, reloadURLData, loadAPIFilters } = state
|
|
33
|
+
const dispatch = useContext(DashboardDispatchContext)
|
|
34
|
+
|
|
35
|
+
const applyFilters = () => {
|
|
36
|
+
const dashboardConfig = _.cloneDeep(state.config.dashboard)
|
|
37
|
+
const nonAutoLoadFilterIndexes = Object.values(state.config.visualizations)
|
|
38
|
+
.filter(v => v.type === 'dashboardFilters')
|
|
39
|
+
.reduce((acc, viz: DashboardFilters) => (!viz.autoLoad ? [...acc, viz.sharedFilterIndexes] : acc), [])
|
|
40
|
+
const allRequiredFiltersSelected = !dashboardConfig.sharedFilters.some((filter, filterIndex) => {
|
|
41
|
+
if (nonAutoLoadFilterIndexes.includes(filterIndex)) {
|
|
42
|
+
return !filter.active && !filter.queuedActive
|
|
43
|
+
} else {
|
|
44
|
+
// autoload filters don't need to be selected to apply filters
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
if (allRequiredFiltersSelected) {
|
|
49
|
+
if (hasDashboardApplyBehavior(state.config.visualizations)) {
|
|
50
|
+
const queryParams = getQueryParams()
|
|
51
|
+
let needsQueryUpdate = false
|
|
52
|
+
dashboardConfig.sharedFilters.forEach((sharedFilter, index) => {
|
|
53
|
+
if (sharedFilter.queuedActive) {
|
|
54
|
+
dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
|
|
55
|
+
delete dashboardConfig.sharedFilters[index].queuedActive
|
|
56
|
+
|
|
57
|
+
if (sharedFilter.setByQueryParameter && queryParams[sharedFilter.setByQueryParameter] !== sharedFilter.active) {
|
|
58
|
+
queryParams[sharedFilter.setByQueryParameter] = sharedFilter.active
|
|
59
|
+
needsQueryUpdate = true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (needsQueryUpdate) {
|
|
65
|
+
updateQueryString(queryParams)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
|
|
70
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: getFilteredData(_.cloneDeep(state)) })
|
|
71
|
+
loadAPIFilters(dashboardConfig.sharedFilters)
|
|
72
|
+
.then(newFilters => {
|
|
73
|
+
reloadURLData(newFilters)
|
|
74
|
+
})
|
|
75
|
+
.catch(e => {
|
|
76
|
+
console.error(e)
|
|
77
|
+
})
|
|
78
|
+
} else {
|
|
79
|
+
// TODO noftify of required fields
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleOnChange = (index: number, value: string | string[]) => {
|
|
84
|
+
const newConfig = _.cloneDeep(dashboardConfig)
|
|
85
|
+
let newSharedFilters = changeFilterActive(index, value, newConfig.dashboard.sharedFilters, visualizationConfig)
|
|
86
|
+
|
|
87
|
+
if (hasDashboardApplyBehavior(dashboardConfig.visualizations)) {
|
|
88
|
+
const isAutoSelectFilter = visualizationConfig.autoLoad
|
|
89
|
+
const missingFilterSelections = newConfig.dashboard.sharedFilters.some(f => !f.active)
|
|
90
|
+
if (isAutoSelectFilter && !missingFilterSelections) {
|
|
91
|
+
// a dropdown has been selected that doesn't
|
|
92
|
+
// require the Go Button
|
|
93
|
+
loadAPIFilters(newSharedFilters).then(filters => {
|
|
94
|
+
reloadURLData(filters)
|
|
95
|
+
})
|
|
96
|
+
} else {
|
|
97
|
+
if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
|
|
98
|
+
newSharedFilters[index].queuedActive = value
|
|
99
|
+
// setData to empty object because we no longer have a data state.
|
|
100
|
+
dispatch({ type: 'SET_DATA', payload: {} })
|
|
101
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
|
|
102
|
+
loadAPIFilters(newSharedFilters)
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
if (newSharedFilters[index].apiFilter) {
|
|
106
|
+
reloadURLData(newSharedFilters)
|
|
107
|
+
} else {
|
|
108
|
+
const clonedState = _.cloneDeep(state)
|
|
109
|
+
clonedState.config.dashboard.sharedFilters = newSharedFilters
|
|
110
|
+
const newFilteredData = getFilteredData(clonedState)
|
|
111
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
112
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const [displayPanel, setDisplayPanel] = useState(true)
|
|
117
|
+
const onBackClick = () => {
|
|
118
|
+
setDisplayPanel(!displayPanel)
|
|
119
|
+
updateConfig({
|
|
120
|
+
...visualizationConfig,
|
|
121
|
+
showEditorPanel: !displayPanel
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Layout.VisualizationWrapper config={visualizationConfig} isEditor={isEditor} currentViewport={currentViewport}>
|
|
127
|
+
{isEditor && (
|
|
128
|
+
<Layout.Sidebar displayPanel={displayPanel} isDashboard={true} title={'Configure Dashboard Filters'} onBackClick={onBackClick}>
|
|
129
|
+
<DashboardFiltersEditor updateConfig={updateConfig} vizConfig={visualizationConfig} />
|
|
130
|
+
</Layout.Sidebar>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
<Layout.Responsive isEditor={isEditor}>
|
|
134
|
+
<div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''} cove-component__content col-12`}>
|
|
135
|
+
<Filters show={visualizationConfig?.sharedFilterIndexes?.map(Number)} filters={dashboardConfig.dashboard.sharedFilters || []} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
|
|
136
|
+
{visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad && <button onClick={applyFilters}>GO!</button>}
|
|
137
|
+
</div>
|
|
138
|
+
</Layout.Responsive>
|
|
139
|
+
</Layout.VisualizationWrapper>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default DashboardFiltersWrapper
|
|
@@ -71,6 +71,11 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
const setExpandCollapseAllButtons = (selection: boolean) => {
|
|
75
|
+
dispatch({ type: 'UPDATE_ROW', payload: { rowIndex, rowData: { expandCollapseAllButtons: selection } } })
|
|
76
|
+
setCanContinue(true)
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
return (
|
|
75
80
|
<Modal>
|
|
76
81
|
<Modal.Content>
|
|
@@ -121,15 +126,10 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
|
|
|
121
126
|
}}
|
|
122
127
|
/>
|
|
123
128
|
) : (
|
|
124
|
-
|
|
125
|
-
options={Object.keys(config.datasets[configureData.dataKey]?.data[0] || {})}
|
|
126
|
-
value={config.rows[rowIndex].
|
|
127
|
-
|
|
128
|
-
initial='--Select--'
|
|
129
|
-
fieldName=''
|
|
130
|
-
updateField={(section, subsection, fieldName, value) => setMultiVizColumn(value)}
|
|
131
|
-
required
|
|
132
|
-
/>
|
|
129
|
+
<>
|
|
130
|
+
<InputSelect options={Object.keys(config.datasets[configureData.dataKey]?.data[0] || {})} value={config.rows[rowIndex].multiVizColumn} label='Multi-Visualization Column' initial='--Select--' updateField={(section, subsection, fieldName, value) => setMultiVizColumn(value)} required />
|
|
131
|
+
<CheckBox value={config.rows[rowIndex].expandCollapseAllButtons} label=' Add Expand/Collapse All buttons' fieldName='' updateField={(section, subsection, fieldName, value) => setExpandCollapseAllButtons(value)} />
|
|
132
|
+
</>
|
|
133
133
|
)
|
|
134
134
|
) : (
|
|
135
135
|
<></>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type ExpandCollapseButtonsProps = {
|
|
2
|
+
setAllExpanded: Function
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const ExpandCollapseButtons: React.FC<ExpandCollapseButtonsProps> = ({ setAllExpanded }) => {
|
|
6
|
+
return (
|
|
7
|
+
<div className='d-block '>
|
|
8
|
+
<div className='d-flex flex-row-reverse mb-2'>
|
|
9
|
+
<button className='btn expand-collapse-buttons' onClick={() => setAllExpanded(false)}>
|
|
10
|
+
- Collapse All
|
|
11
|
+
</button>
|
|
12
|
+
<button className='btn expand-collapse-buttons mr-2' onClick={() => setAllExpanded(true)}>
|
|
13
|
+
+ Expand All
|
|
14
|
+
</button>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default ExpandCollapseButtons
|