@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.
- package/dist/cdcdashboard.js +98192 -85200
- package/examples/sankey.json +5218 -0
- package/index.html +3 -2
- package/package.json +11 -10
- package/src/CdcDashboard.tsx +124 -124
- package/src/CdcDashboardComponent.tsx +173 -186
- package/src/DashboardContext.tsx +4 -1
- package/src/_stories/Dashboard.stories.tsx +27 -5
- package/src/_stories/_mock/pivot-filter.json +163 -0
- package/src/_stories/_mock/standalone-table.json +122 -0
- package/src/_stories/_mock/toggle-example.json +4035 -0
- package/src/components/EditorWrapper/EditorWrapper.tsx +52 -0
- package/src/components/EditorWrapper/editor-wrapper.style.css +13 -0
- package/src/components/Filters.tsx +88 -0
- package/src/components/Header/FilterModal.tsx +480 -0
- package/src/components/Header/Header.tsx +25 -465
- package/src/components/Row.tsx +28 -17
- package/src/components/Toggle/Toggle.tsx +37 -0
- package/src/components/Toggle/index.tsx +1 -0
- package/src/components/Toggle/toggle-style.css +34 -0
- package/src/components/VisualizationsPanel.tsx +13 -3
- package/src/components/Widget.tsx +14 -30
- package/src/helpers/filterData.ts +72 -49
- package/src/helpers/generateValuesForFilter.ts +2 -12
- package/src/helpers/getApiFilterKey.ts +5 -0
- package/src/helpers/getUpdateConfig.ts +24 -22
- package/src/helpers/iconHash.tsx +34 -0
- package/src/helpers/tests/filterData.test.ts +149 -0
- package/src/images/icon-toggle.svg +1 -0
- package/src/scss/grid.scss +1 -1
- package/src/scss/main.scss +6 -0
- package/src/store/dashboard.actions.ts +19 -2
- package/src/store/dashboard.reducer.ts +9 -1
- package/src/types/ConfigRow.ts +2 -0
- package/src/types/DataSet.ts +7 -7
- package/src/types/InitialState.ts +2 -1
- package/src/types/SharedFilter.ts +5 -2
- package/src/types/Tab.ts +1 -0
|
@@ -1,26 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect, useContext } from 'react'
|
|
2
2
|
|
|
3
3
|
import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
|
|
4
4
|
|
|
5
5
|
// types
|
|
6
|
-
import { type APIFilter } from '../../types/APIFilter'
|
|
7
6
|
import { type SharedFilter } from '../../types/SharedFilter'
|
|
8
7
|
import { type DashboardConfig as Config } from '../../types/DashboardConfig'
|
|
9
|
-
|
|
10
|
-
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
11
|
-
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
12
8
|
import { useGlobalContext } from '@cdc/core/components/GlobalContext'
|
|
13
|
-
|
|
9
|
+
|
|
14
10
|
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
15
11
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
16
12
|
import Select from '@cdc/core/components/ui/Select'
|
|
17
|
-
import Button from '@cdc/core/components/elements/Button'
|
|
18
13
|
|
|
19
14
|
import './index.scss'
|
|
20
15
|
import MultiConfigTabs from '../MultiConfigTabs'
|
|
16
|
+
import { Tab } from '../../types/Tab'
|
|
17
|
+
import FilterModal from './FilterModal'
|
|
18
|
+
import _ from 'lodash'
|
|
21
19
|
|
|
22
20
|
type HeaderProps = {
|
|
23
|
-
setPreview?: any
|
|
24
21
|
back?: any
|
|
25
22
|
subEditor?: any
|
|
26
23
|
visualizationKey?: string
|
|
@@ -32,24 +29,20 @@ export const FilterBehavior = {
|
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
const Header = (props: HeaderProps) => {
|
|
35
|
-
const
|
|
32
|
+
const tabs: Tab[] = ['Dashboard Description', 'Dashboard Filters', 'Data Table Settings', 'Dashboard Preview']
|
|
33
|
+
const { visualizationKey, subEditor } = props
|
|
36
34
|
const { config, setParentConfig, tabSelected } = useContext(DashboardContext)
|
|
37
35
|
if (!config) return null
|
|
38
36
|
const dispatch = useContext(DashboardDispatchContext)
|
|
39
|
-
const setTabSelected = (payload: number) => dispatch({ type: 'SET_TAB_SELECTED', payload })
|
|
40
37
|
const back = () => {
|
|
41
38
|
if (!visualizationKey) return
|
|
42
|
-
const newConfig
|
|
39
|
+
const newConfig = _.cloneDeep(config)
|
|
43
40
|
newConfig.visualizations[visualizationKey].editing = false
|
|
44
41
|
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
const { overlay } = useGlobalContext()
|
|
48
45
|
|
|
49
|
-
const [columns, setColumns] = useState<string[]>([])
|
|
50
|
-
|
|
51
|
-
const transform = new DataTransform()
|
|
52
|
-
|
|
53
46
|
const changeConfigValue = (parentObj, key, value) => {
|
|
54
47
|
let newConfig = { ...config }
|
|
55
48
|
if (!newConfig[parentObj]) newConfig[parentObj] = {}
|
|
@@ -57,15 +50,6 @@ const Header = (props: HeaderProps) => {
|
|
|
57
50
|
dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
|
|
58
51
|
}
|
|
59
52
|
|
|
60
|
-
const setTab = index => {
|
|
61
|
-
setTabSelected(index)
|
|
62
|
-
if (index === 3) {
|
|
63
|
-
setPreview(true)
|
|
64
|
-
} else {
|
|
65
|
-
setPreview(false)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
53
|
const addNewFilter = () => {
|
|
70
54
|
let dashboardConfig = { ...config.dashboard }
|
|
71
55
|
|
|
@@ -137,411 +121,6 @@ const Header = (props: HeaderProps) => {
|
|
|
137
121
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
122
|
}, [config])
|
|
139
123
|
|
|
140
|
-
useEffect(() => {
|
|
141
|
-
const runSetColumns = async () => {
|
|
142
|
-
if (!config) return
|
|
143
|
-
if (config.filterBehavior === FilterBehavior.Apply) return
|
|
144
|
-
let columns = {}
|
|
145
|
-
let dataKeys = Object.keys(config.datasets)
|
|
146
|
-
|
|
147
|
-
for (let i = 0; i < dataKeys.length; i++) {
|
|
148
|
-
let _dataSet = config.datasets[dataKeys[i]]
|
|
149
|
-
if (!_dataSet.data && _dataSet.dataUrl) {
|
|
150
|
-
config.datasets[dataKeys[i]].data = await fetchRemoteData(config.datasets[dataKeys[i]].dataUrl)
|
|
151
|
-
_dataSet = config.datasets[dataKeys[i]]
|
|
152
|
-
if (_dataSet.dataDescription) {
|
|
153
|
-
try {
|
|
154
|
-
config.datasets[dataKeys[i]].data = transform.autoStandardize(_dataSet.data)
|
|
155
|
-
_dataSet = config.datasets[dataKeys[i]]
|
|
156
|
-
config.datasets[dataKeys[i]].data = transform.developerStandardize(_dataSet.data, _dataSet.dataDescription)
|
|
157
|
-
_dataSet = config.datasets[dataKeys[i]]
|
|
158
|
-
} catch (e) {
|
|
159
|
-
//Data not able to be standardized, leave as is
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (_dataSet.data) {
|
|
165
|
-
config.datasets[dataKeys[i]].data.forEach(row => {
|
|
166
|
-
Object.keys(row).forEach(columnName => (columns[columnName] = true))
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
setColumns(Object.keys(columns))
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
runSetColumns()
|
|
175
|
-
}, [config.datasets])
|
|
176
|
-
|
|
177
|
-
const filterModal = (filter: SharedFilter, index) => {
|
|
178
|
-
const saveChanges = () => {
|
|
179
|
-
let tempConfig = { ...config.dashboard }
|
|
180
|
-
tempConfig.sharedFilters[index] = filter
|
|
181
|
-
|
|
182
|
-
dispatch({ type: 'UPDATE_CONFIG', payload: [{ ...config, dashboard: tempConfig }] })
|
|
183
|
-
overlay?.actions.toggleOverlay()
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const updateFilterProp = (name, index, value) => {
|
|
187
|
-
// @TODO this should be refactored into a reducer function.
|
|
188
|
-
// it's unsafe to directly set objects w/o guardrails
|
|
189
|
-
let newFilter = { ...filter }
|
|
190
|
-
|
|
191
|
-
newFilter[name] = value
|
|
192
|
-
|
|
193
|
-
console.log('newFilter', newFilter)
|
|
194
|
-
|
|
195
|
-
overlay?.actions.openOverlay(filterModal(newFilter, index))
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const addFilterUsedBy = (filter, index, value) => {
|
|
199
|
-
if (!filter.usedBy) filter.usedBy = []
|
|
200
|
-
filter.usedBy.push(value)
|
|
201
|
-
updateFilterProp('usedBy', index, filter.usedBy)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const removeFilterUsedBy = (filter, index, value) => {
|
|
205
|
-
let usedByIndex = filter.usedBy.indexOf(value)
|
|
206
|
-
if (usedByIndex !== -1) {
|
|
207
|
-
filter.usedBy.splice(usedByIndex, 1)
|
|
208
|
-
updateFilterProp('usedBy', index, filter.usedBy)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const updateAPIFilter = (key: keyof APIFilter, value: string | boolean) => {
|
|
213
|
-
const _filter = filter.apiFilter || { apiEndpoint: '', valueSelector: '', textSelector: '' }
|
|
214
|
-
const newAPIFilter: APIFilter = { ..._filter, [key]: value }
|
|
215
|
-
overlay?.actions.openOverlay(filterModal({ ...filter, apiFilter: newAPIFilter }, index))
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return (
|
|
219
|
-
<Modal>
|
|
220
|
-
<Modal.Content>
|
|
221
|
-
<h2 className='shared-filter-modal__title'>Dashboard Filter Settings</h2>
|
|
222
|
-
<fieldset className='shared-filter-modal shared-filter-modal__fieldset' key={filter.columnName + index}>
|
|
223
|
-
<label>
|
|
224
|
-
<span className='edit-label column-heading'>Filter Type: </span>
|
|
225
|
-
<select defaultValue={filter.type || ''} onChange={e => updateFilterProp('type', index, e.target.value)}>
|
|
226
|
-
<option value=''>- Select Option -</option>
|
|
227
|
-
<option value='urlfilter'>URL</option>
|
|
228
|
-
<option value='datafilter'>Data</option>
|
|
229
|
-
</select>
|
|
230
|
-
</label>
|
|
231
|
-
{filter.type === 'urlfilter' && (
|
|
232
|
-
<>
|
|
233
|
-
<label>
|
|
234
|
-
<span className='edit-label column-heading'>Label: </span>
|
|
235
|
-
<input
|
|
236
|
-
type='text'
|
|
237
|
-
value={filter.key}
|
|
238
|
-
onChange={e => {
|
|
239
|
-
updateFilterProp('key', index, e.target.value)
|
|
240
|
-
}}
|
|
241
|
-
/>
|
|
242
|
-
</label>
|
|
243
|
-
{config.filterBehavior !== FilterBehavior.Apply && (
|
|
244
|
-
<>
|
|
245
|
-
<label>
|
|
246
|
-
<span className='edit-label column-heading'>URL to Filter: </span>
|
|
247
|
-
<select defaultValue={filter.datasetKey || ''} onChange={e => updateFilterProp('datasetKey', index, e.target.value)}>
|
|
248
|
-
<option value=''>- Select Option -</option>
|
|
249
|
-
{Object.keys(config.datasets).map(datasetKey => {
|
|
250
|
-
if (config.datasets[datasetKey].dataUrl) {
|
|
251
|
-
return (
|
|
252
|
-
<option key={datasetKey} value={datasetKey}>
|
|
253
|
-
{config.datasets[datasetKey].dataUrl}
|
|
254
|
-
</option>
|
|
255
|
-
)
|
|
256
|
-
}
|
|
257
|
-
return null
|
|
258
|
-
})}
|
|
259
|
-
</select>
|
|
260
|
-
</label>
|
|
261
|
-
<label>
|
|
262
|
-
<span className='edit-label column-heading'>Filter By: </span>
|
|
263
|
-
<select defaultValue={filter.filterBy || ''} onChange={e => updateFilterProp('filterBy', index, e.target.value)}>
|
|
264
|
-
<option value=''>- Select Option -</option>
|
|
265
|
-
<option key={'query-string'} value={'Query String'}>
|
|
266
|
-
Query String
|
|
267
|
-
</option>
|
|
268
|
-
<option key={'file-name'} value={'File Name'}>
|
|
269
|
-
File Name
|
|
270
|
-
</option>
|
|
271
|
-
</select>
|
|
272
|
-
</label>
|
|
273
|
-
{filter.filterBy === 'File Name' && (
|
|
274
|
-
<>
|
|
275
|
-
<label>
|
|
276
|
-
<span className='edit-label column-heading'>
|
|
277
|
-
File Name:
|
|
278
|
-
<Tooltip style={{ textTransform: 'none' }}>
|
|
279
|
-
<Tooltip.Target>
|
|
280
|
-
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
281
|
-
</Tooltip.Target>
|
|
282
|
-
<Tooltip.Content>
|
|
283
|
-
<p>{`Add \${query}\ to replace the filename with the active dropdown value.`}</p>
|
|
284
|
-
</Tooltip.Content>
|
|
285
|
-
</Tooltip>
|
|
286
|
-
</span>
|
|
287
|
-
|
|
288
|
-
<input type='text' defaultValue={filter.fileName || ''} onChange={e => updateFilterProp('fileName', index, e.target.value)} />
|
|
289
|
-
</label>
|
|
290
|
-
|
|
291
|
-
<label>
|
|
292
|
-
<span className='edit-label column-heading'>
|
|
293
|
-
White Space Replacments
|
|
294
|
-
<Tooltip style={{ textTransform: 'none' }}>
|
|
295
|
-
<Tooltip.Target>
|
|
296
|
-
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
297
|
-
</Tooltip.Target>
|
|
298
|
-
<Tooltip.Content>
|
|
299
|
-
<p>{`Set how whitespace characters will be handled in the file request`}</p>
|
|
300
|
-
</Tooltip.Content>
|
|
301
|
-
</Tooltip>
|
|
302
|
-
</span>
|
|
303
|
-
<select defaultValue={filter.whitespaceReplacement || 'Keep Spaces'} onChange={e => updateFilterProp('whitespaceReplacement', index, e.target.value)}>
|
|
304
|
-
<option key={'remove-spaces'} value={'Remove Spaces'}>
|
|
305
|
-
Remove Spaces
|
|
306
|
-
</option>
|
|
307
|
-
<option key={'replace-with-underscore'} value={'Replace With Underscore'}>
|
|
308
|
-
Replace With Underscore
|
|
309
|
-
</option>
|
|
310
|
-
<option key={'keep-spaces'} value={'Keep Spaces'}>
|
|
311
|
-
Keep Spaces
|
|
312
|
-
</option>
|
|
313
|
-
</select>
|
|
314
|
-
</label>
|
|
315
|
-
</>
|
|
316
|
-
)}
|
|
317
|
-
</>
|
|
318
|
-
)}
|
|
319
|
-
{filter.filterBy === 'Query String' && (
|
|
320
|
-
<label>
|
|
321
|
-
<span className='edit-label column-heading'>Query string parameter</span> <input type='text' defaultValue={filter.queryParameter} onChange={e => updateFilterProp('queryParameter', index, e.target.value)} />
|
|
322
|
-
</label>
|
|
323
|
-
)}
|
|
324
|
-
<label>
|
|
325
|
-
<span className='edit-label column-heading'>Filter API Endpoint: </span>
|
|
326
|
-
<input
|
|
327
|
-
type='text'
|
|
328
|
-
value={filter.apiFilter?.apiEndpoint}
|
|
329
|
-
onChange={e => {
|
|
330
|
-
updateAPIFilter('apiEndpoint', e.target.value)
|
|
331
|
-
}}
|
|
332
|
-
/>
|
|
333
|
-
</label>
|
|
334
|
-
<label>
|
|
335
|
-
<span className='edit-label column-heading'>
|
|
336
|
-
Option Text Selector:
|
|
337
|
-
<Tooltip style={{ textTransform: 'none' }}>
|
|
338
|
-
<Tooltip.Target>
|
|
339
|
-
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
340
|
-
</Tooltip.Target>
|
|
341
|
-
<Tooltip.Content>
|
|
342
|
-
<p>Text to use in the html option element</p>
|
|
343
|
-
</Tooltip.Content>
|
|
344
|
-
</Tooltip>
|
|
345
|
-
</span>
|
|
346
|
-
<input
|
|
347
|
-
type='text'
|
|
348
|
-
value={filter.apiFilter?.textSelector}
|
|
349
|
-
onChange={e => {
|
|
350
|
-
updateAPIFilter('textSelector', e.target.value)
|
|
351
|
-
}}
|
|
352
|
-
/>
|
|
353
|
-
</label>
|
|
354
|
-
<label>
|
|
355
|
-
<span className='edit-label column-heading'>
|
|
356
|
-
Option Value Selector:
|
|
357
|
-
<Tooltip style={{ textTransform: 'none' }}>
|
|
358
|
-
<Tooltip.Target>
|
|
359
|
-
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
360
|
-
</Tooltip.Target>
|
|
361
|
-
<Tooltip.Content>
|
|
362
|
-
<p>Value to use in the html option element</p>
|
|
363
|
-
</Tooltip.Content>
|
|
364
|
-
</Tooltip>
|
|
365
|
-
</span>
|
|
366
|
-
<input
|
|
367
|
-
type='text'
|
|
368
|
-
value={filter.apiFilter?.valueSelector}
|
|
369
|
-
onChange={e => {
|
|
370
|
-
updateAPIFilter('valueSelector', e.target.value)
|
|
371
|
-
}}
|
|
372
|
-
/>
|
|
373
|
-
</label>
|
|
374
|
-
<label>
|
|
375
|
-
<span className='edit-label column-heading'>Parent Filter: </span>
|
|
376
|
-
<select
|
|
377
|
-
value={filter.parents || []}
|
|
378
|
-
onChange={e => {
|
|
379
|
-
updateFilterProp('parents', index, e.target.value)
|
|
380
|
-
}}
|
|
381
|
-
>
|
|
382
|
-
<option value=''>Select a filter</option>
|
|
383
|
-
{config.dashboard.sharedFilters &&
|
|
384
|
-
config.dashboard.sharedFilters.map(sharedFilter => {
|
|
385
|
-
if (sharedFilter.key !== filter.key && sharedFilter.type !== 'datafilter') {
|
|
386
|
-
return <option value={sharedFilter.key}>{sharedFilter.key}</option>
|
|
387
|
-
}
|
|
388
|
-
})}
|
|
389
|
-
</select>
|
|
390
|
-
</label>
|
|
391
|
-
<label>
|
|
392
|
-
<span className='edit-label column-heading'>Auto Load: </span>
|
|
393
|
-
<input
|
|
394
|
-
type='checkbox'
|
|
395
|
-
checked={filter.apiFilter?.autoLoad}
|
|
396
|
-
onChange={e => {
|
|
397
|
-
updateAPIFilter('autoLoad', !filter.apiFilter?.autoLoad)
|
|
398
|
-
}}
|
|
399
|
-
/>
|
|
400
|
-
</label>
|
|
401
|
-
<label>
|
|
402
|
-
<span className='edit-label column-heading'>Default Value: </span>
|
|
403
|
-
<input
|
|
404
|
-
type='text'
|
|
405
|
-
value={filter.apiFilter?.defaultValue}
|
|
406
|
-
onChange={e => {
|
|
407
|
-
updateAPIFilter('defaultValue', e.target.value)
|
|
408
|
-
}}
|
|
409
|
-
/>
|
|
410
|
-
</label>
|
|
411
|
-
</>
|
|
412
|
-
)}
|
|
413
|
-
{filter.type === 'datafilter' && (
|
|
414
|
-
<>
|
|
415
|
-
<label>
|
|
416
|
-
<span className='edit-label column-heading'>Filter: </span>
|
|
417
|
-
<select
|
|
418
|
-
value={filter.columnName}
|
|
419
|
-
onChange={e => {
|
|
420
|
-
updateFilterProp('columnName', index, e.target.value)
|
|
421
|
-
}}
|
|
422
|
-
>
|
|
423
|
-
<option value=''>- Select Option -</option>
|
|
424
|
-
{columns.map(dataKey => (
|
|
425
|
-
<option value={dataKey} key={`filter-column-select-item-${dataKey}`}>
|
|
426
|
-
{dataKey}
|
|
427
|
-
</option>
|
|
428
|
-
))}
|
|
429
|
-
</select>
|
|
430
|
-
</label>
|
|
431
|
-
<label>
|
|
432
|
-
<span className='edit-label column-heading'>Label: </span>
|
|
433
|
-
<input
|
|
434
|
-
type='text'
|
|
435
|
-
value={filter.key}
|
|
436
|
-
onChange={e => {
|
|
437
|
-
updateFilterProp('key', index, e.target.value)
|
|
438
|
-
}}
|
|
439
|
-
/>
|
|
440
|
-
</label>
|
|
441
|
-
<label>
|
|
442
|
-
<span className='edit-label column-heading'>Show Dropdown</span>
|
|
443
|
-
<input
|
|
444
|
-
type='checkbox'
|
|
445
|
-
defaultChecked={filter.showDropdown === true}
|
|
446
|
-
onChange={e => {
|
|
447
|
-
updateFilterProp('showDropdown', index, !filter.showDropdown)
|
|
448
|
-
}}
|
|
449
|
-
/>
|
|
450
|
-
</label>
|
|
451
|
-
<label>
|
|
452
|
-
<span className='edit-label column-heading'>Set By: </span>
|
|
453
|
-
<select value={filter.setBy} onChange={e => updateFilterProp('setBy', index, e.target.value)}>
|
|
454
|
-
<option value=''>- Select Option -</option>
|
|
455
|
-
{Object.keys(config.visualizations).map(vizKey => (
|
|
456
|
-
<option value={vizKey} key={`set-by-select-item-${vizKey}`}>
|
|
457
|
-
{config.visualizations[vizKey].general && config.visualizations[vizKey].general.title ? config.visualizations[vizKey].general.title : config.visualizations[vizKey].title || vizKey}
|
|
458
|
-
</option>
|
|
459
|
-
))}
|
|
460
|
-
</select>
|
|
461
|
-
</label>
|
|
462
|
-
<label>
|
|
463
|
-
<span className='edit-label column-heading'>Used By: </span>
|
|
464
|
-
<ul>
|
|
465
|
-
{filter.usedBy &&
|
|
466
|
-
filter.usedBy.map(vizKey => (
|
|
467
|
-
<li key={`used-by-list-item-${vizKey}`}>
|
|
468
|
-
<span>{config.visualizations[vizKey].general && config.visualizations[vizKey].general.title ? config.visualizations[vizKey].general.title : config.visualizations[vizKey].title || vizKey}</span>{' '}
|
|
469
|
-
<button
|
|
470
|
-
onClick={e => {
|
|
471
|
-
e.preventDefault()
|
|
472
|
-
removeFilterUsedBy(filter, index, vizKey)
|
|
473
|
-
}}
|
|
474
|
-
>
|
|
475
|
-
X
|
|
476
|
-
</button>
|
|
477
|
-
</li>
|
|
478
|
-
))}
|
|
479
|
-
</ul>
|
|
480
|
-
<select onChange={e => addFilterUsedBy(filter, index, e.target.value)}>
|
|
481
|
-
<option value=''>- Select Option -</option>
|
|
482
|
-
{Object.keys(config.visualizations)
|
|
483
|
-
.filter(vizKey => filter.setBy !== vizKey && (!filter.usedBy || filter.usedBy.indexOf(vizKey) === -1) && !config.visualizations[vizKey].usesSharedFilter)
|
|
484
|
-
.map(vizKey => (
|
|
485
|
-
<option value={vizKey} key={`used-by-select-item-${vizKey}`}>
|
|
486
|
-
{config.visualizations[vizKey].general && config.visualizations[vizKey].general.title ? config.visualizations[vizKey].general.title : config.visualizations[vizKey].title || vizKey}
|
|
487
|
-
</option>
|
|
488
|
-
))}
|
|
489
|
-
</select>
|
|
490
|
-
</label>
|
|
491
|
-
<label>
|
|
492
|
-
<span className='edit-label column-heading'>Reset Label: </span>
|
|
493
|
-
<input
|
|
494
|
-
type='text'
|
|
495
|
-
value={filter.resetLabel || ''}
|
|
496
|
-
onChange={e => {
|
|
497
|
-
updateFilterProp('resetLabel', index, e.target.value)
|
|
498
|
-
}}
|
|
499
|
-
/>
|
|
500
|
-
</label>
|
|
501
|
-
<label>
|
|
502
|
-
<span className='edit-label column-heading'>Parent Filter: </span>
|
|
503
|
-
<select
|
|
504
|
-
value={filter.parents || []}
|
|
505
|
-
onChange={e => {
|
|
506
|
-
updateFilterProp('parents', index, e.target.value)
|
|
507
|
-
}}
|
|
508
|
-
>
|
|
509
|
-
<option value=''>Select a filter</option>
|
|
510
|
-
{config.dashboard.sharedFilters &&
|
|
511
|
-
config.dashboard.sharedFilters.map(sharedFilter => {
|
|
512
|
-
if (sharedFilter.key !== filter.key) {
|
|
513
|
-
return <option>{sharedFilter.key}</option>
|
|
514
|
-
}
|
|
515
|
-
})}
|
|
516
|
-
</select>
|
|
517
|
-
</label>
|
|
518
|
-
</>
|
|
519
|
-
)}
|
|
520
|
-
</fieldset>
|
|
521
|
-
|
|
522
|
-
<Button
|
|
523
|
-
className='btn--remove warn'
|
|
524
|
-
onClick={() => {
|
|
525
|
-
removeFilter(index)
|
|
526
|
-
}}
|
|
527
|
-
>
|
|
528
|
-
Remove Filter
|
|
529
|
-
</Button>
|
|
530
|
-
|
|
531
|
-
<div className='shared-filter-modal__right-buttons'>
|
|
532
|
-
<Button className='btn--cancel muted' style={{ display: 'inline-block', marginRight: '1em' }} onClick={overlay?.actions.toggleOverlay}>
|
|
533
|
-
Cancel
|
|
534
|
-
</Button>
|
|
535
|
-
|
|
536
|
-
<Button type='button' className='btn--submit success' style={{ display: 'inline-block' }} onClick={saveChanges}>
|
|
537
|
-
Save
|
|
538
|
-
</Button>
|
|
539
|
-
</div>
|
|
540
|
-
</Modal.Content>
|
|
541
|
-
</Modal>
|
|
542
|
-
)
|
|
543
|
-
}
|
|
544
|
-
|
|
545
124
|
const handleCheck = e => {
|
|
546
125
|
const { checked } = e.currentTarget
|
|
547
126
|
if (checked) {
|
|
@@ -571,42 +150,23 @@ const Header = (props: HeaderProps) => {
|
|
|
571
150
|
<div className='toggle-bar__wrapper'>
|
|
572
151
|
<MultiConfigTabs isEditor />
|
|
573
152
|
<ul className='toggle-bar'>
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
>
|
|
588
|
-
Dashboard Filters
|
|
589
|
-
</li>
|
|
590
|
-
<li
|
|
591
|
-
className={tabSelected === 2 ? 'active' : 'inactive'}
|
|
592
|
-
onClick={() => {
|
|
593
|
-
setTab(2)
|
|
594
|
-
}}
|
|
595
|
-
>
|
|
596
|
-
Data Table Settings
|
|
597
|
-
</li>
|
|
598
|
-
<li
|
|
599
|
-
className={tabSelected === 3 ? 'active' : 'inactive'}
|
|
600
|
-
onClick={() => {
|
|
601
|
-
setTab(3)
|
|
602
|
-
}}
|
|
603
|
-
>
|
|
604
|
-
Dashboard Preview
|
|
605
|
-
</li>
|
|
153
|
+
{tabs.map(tab => {
|
|
154
|
+
return (
|
|
155
|
+
<li
|
|
156
|
+
key={tab}
|
|
157
|
+
className={tabSelected === tab ? 'active' : 'inactive'}
|
|
158
|
+
onClick={() => {
|
|
159
|
+
dispatch({ type: 'SET_TAB_SELECTED', payload: tab })
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{tab}
|
|
163
|
+
</li>
|
|
164
|
+
)
|
|
165
|
+
})}
|
|
606
166
|
</ul>
|
|
607
167
|
<div className='heading-body'>
|
|
608
|
-
{tabSelected ===
|
|
609
|
-
{tabSelected ===
|
|
168
|
+
{tabSelected === 'Dashboard Description' && <input type='text' className='description-input' placeholder='Type a dashboard description here.' defaultValue={config.dashboard?.description} onChange={e => changeConfigValue('dashboard', 'description', e.target.value)} />}
|
|
169
|
+
{tabSelected === 'Dashboard Filters' && (
|
|
610
170
|
<>
|
|
611
171
|
{config.dashboard.sharedFilters &&
|
|
612
172
|
config.dashboard.sharedFilters.map((sharedFilter, index) => (
|
|
@@ -615,7 +175,7 @@ const Header = (props: HeaderProps) => {
|
|
|
615
175
|
href='#'
|
|
616
176
|
onClick={e => {
|
|
617
177
|
e.preventDefault()
|
|
618
|
-
overlay?.actions.openOverlay(
|
|
178
|
+
overlay?.actions.openOverlay(<FilterModal index={index} config={config} filterState={sharedFilter} removeFilter={removeFilter} />)
|
|
619
179
|
}}
|
|
620
180
|
>
|
|
621
181
|
{sharedFilter.key}
|
|
@@ -648,7 +208,7 @@ const Header = (props: HeaderProps) => {
|
|
|
648
208
|
/>
|
|
649
209
|
</>
|
|
650
210
|
)}
|
|
651
|
-
{tabSelected ===
|
|
211
|
+
{tabSelected === 'Data Table Settings' && (
|
|
652
212
|
<>
|
|
653
213
|
<div className='wrap'>
|
|
654
214
|
<label>
|
package/src/components/Row.tsx
CHANGED
|
@@ -11,35 +11,43 @@ import TwoColIcon from '../images/icon-col-6.svg'
|
|
|
11
11
|
import ThreeColIcon from '../images/icon-col-4.svg'
|
|
12
12
|
import FourEightColIcon from '../images/icon-col-4-8.svg'
|
|
13
13
|
import EightFourColIcon from '../images/icon-col-8-4.svg'
|
|
14
|
+
import ToggleIcon from '../images/icon-toggle.svg'
|
|
15
|
+
import { ConfigRow } from '../types/ConfigRow'
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
type RowMenuProps = {
|
|
18
|
+
rowIdx: number
|
|
19
|
+
row: ConfigRow
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const RowMenu: React.FC<RowMenuProps> = ({ rowIdx, row }) => {
|
|
16
23
|
const { config } = useContext(DashboardContext)
|
|
17
|
-
if (!config) return null
|
|
18
|
-
const { rows } = config
|
|
19
24
|
const dispatch = useContext(DashboardDispatchContext)
|
|
25
|
+
const { rows } = config
|
|
26
|
+
|
|
20
27
|
const updateConfig = config => dispatch({ type: 'UPDATE_CONFIG', payload: [config] })
|
|
21
28
|
const getCurr = () => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
if (row[0].toggle) return 'toggle'
|
|
30
|
+
return row.reduce((acc, curr) => {
|
|
31
|
+
if (curr.width) {
|
|
32
|
+
acc += curr.width
|
|
33
|
+
}
|
|
34
|
+
return acc
|
|
35
|
+
}, '')
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
const [curr, setCurr] = useState(getCurr())
|
|
32
39
|
|
|
33
|
-
const setRowLayout = layout => {
|
|
40
|
+
const setRowLayout = (layout: number[], toggle = undefined) => {
|
|
34
41
|
const newRows = [...rows]
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
for (let i = 0; i < r.length; i++) {
|
|
38
|
-
r[i].width = layout[i] ?? null
|
|
39
|
-
}
|
|
42
|
+
const row = newRows[rowIdx]
|
|
40
43
|
|
|
44
|
+
row.forEach((col, i) => {
|
|
45
|
+
col.width = layout[i] ?? null
|
|
46
|
+
col.toggle = toggle
|
|
47
|
+
if (!toggle) col.hide = undefined
|
|
48
|
+
})
|
|
41
49
|
updateConfig({ ...config, rows: newRows })
|
|
42
|
-
setCurr(layout.join(''))
|
|
50
|
+
setCurr(toggle ? 'toggle' : layout.join(''))
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
const moveRow = (dir = 'down') => {
|
|
@@ -104,6 +112,9 @@ const RowMenu = ({ rowIdx, row }) => {
|
|
|
104
112
|
</li>,
|
|
105
113
|
<li className={curr === '84' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([8, 4])} key='84' title='2 Columns'>
|
|
106
114
|
<EightFourColIcon />
|
|
115
|
+
</li>,
|
|
116
|
+
<li className={curr === 'toggle' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([12, 12, 12], true)} key='toggle' title='Toggle between up to three visualizations'>
|
|
117
|
+
<ToggleIcon />
|
|
107
118
|
</li>
|
|
108
119
|
]
|
|
109
120
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { DashboardDispatchContext } from '../../DashboardContext'
|
|
3
|
+
import { ConfigRow } from '../../types/ConfigRow'
|
|
4
|
+
import { Visualization } from '@cdc/core/types/Visualization'
|
|
5
|
+
import { getIcon } from '../../helpers/iconHash'
|
|
6
|
+
import './toggle-style.css'
|
|
7
|
+
import _ from 'lodash'
|
|
8
|
+
|
|
9
|
+
type ToggleProps = {
|
|
10
|
+
row: ConfigRow
|
|
11
|
+
rowIndex: number
|
|
12
|
+
visualizations: Record<string, Visualization>
|
|
13
|
+
}
|
|
14
|
+
const Toggle: React.FC<ToggleProps> = ({ row, rowIndex, visualizations }) => {
|
|
15
|
+
const dispatch = useContext(DashboardDispatchContext)
|
|
16
|
+
|
|
17
|
+
const selectItem = (colIndex, e = null) => {
|
|
18
|
+
if (e?.key && e.key !== 'Enter') return
|
|
19
|
+
dispatch({ type: 'TOGGLE_ROW', payload: { rowIndex, colIndex } })
|
|
20
|
+
}
|
|
21
|
+
return (
|
|
22
|
+
<div className='toggle-component'>
|
|
23
|
+
{row.map((col, colIndex) => {
|
|
24
|
+
if (!col.widget) return null
|
|
25
|
+
const type = visualizations[col.widget].type
|
|
26
|
+
const selected = col.hide !== undefined ? col.hide : colIndex === 0
|
|
27
|
+
return (
|
|
28
|
+
<div role='radio' className={selected ? 'selected' : ''} key={colIndex} onClick={() => selectItem(colIndex)} onKeyUp={e => selectItem(colIndex, e)} aria-checked={selected} tabIndex={0} aria-label={`Toggle ${type}`}>
|
|
29
|
+
{getIcon(visualizations[col.widget])} <span>{_.capitalize(type)}</span>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
})}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default Toggle
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Toggle'
|