@cdc/dashboard 4.23.11 → 4.24.2
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 +109007 -98738
- package/examples/DEV-6574.json +2224 -0
- package/examples/filters/Alabama.json +72 -0
- package/examples/filters/Alaska.json +1737 -0
- package/examples/filters/Arkansas.json +4713 -0
- package/examples/filters/California.json +212 -0
- package/examples/filters/Colorado.json +1500 -0
- package/examples/filters/Connecticut.json +559 -0
- package/examples/filters/Delaware.json +63 -0
- package/examples/filters/DistrictofColumbia.json +63 -0
- package/examples/filters/Florida.json +4217 -0
- package/examples/filters/States.json +146 -0
- package/examples/test.json +752 -0
- package/examples/zika.json +2274 -0
- package/index.html +5 -3
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +124 -963
- package/src/CdcDashboardComponent.tsx +903 -0
- package/src/_stories/Dashboard.stories.tsx +2 -2
- package/src/components/Column.tsx +15 -12
- package/src/components/Header/Header.tsx +694 -0
- package/src/components/Header/index.tsx +1 -676
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +106 -0
- package/src/components/MultiConfigTabs/MultiTabs.tsx +30 -0
- package/src/components/MultiConfigTabs/index.tsx +8 -0
- package/src/components/MultiConfigTabs/multiconfigtabs.styles.css +32 -0
- package/src/components/Widget.tsx +25 -9
- package/src/helpers/filterData.ts +73 -73
- package/src/helpers/generateValuesForFilter.ts +25 -29
- package/src/helpers/getUpdateConfig.ts +6 -2
- package/src/helpers/processData.ts +13 -0
- package/src/helpers/processDataLegacy.ts +14 -0
- package/src/{index.jsx → index.tsx} +2 -2
- package/src/scss/editor-panel.scss +14 -11
- package/src/scss/grid.scss +4 -6
- package/src/scss/main.scss +2 -8
- package/src/store/dashboard.actions.ts +10 -4
- package/src/store/dashboard.reducer.ts +74 -3
- package/src/types/ConfigRow.ts +6 -0
- package/src/types/Dashboard.ts +11 -0
- package/src/types/DashboardConfig.ts +23 -0
- package/src/types/InitialState.ts +10 -0
- package/src/types/MultiDashboard.ts +11 -0
- package/src/types/SharedFilter.ts +31 -20
- package/src/types/Config.ts +0 -27
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import React, { useState, useEffect, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
|
|
4
|
+
|
|
5
|
+
// types
|
|
6
|
+
import { type APIFilter } from '../../types/APIFilter'
|
|
7
|
+
import { type SharedFilter } from '../../types/SharedFilter'
|
|
8
|
+
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
|
+
import { useGlobalContext } from '@cdc/core/components/GlobalContext'
|
|
13
|
+
import Modal from '@cdc/core/components/ui/Modal'
|
|
14
|
+
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
15
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
16
|
+
import Select from '@cdc/core/components/ui/Select'
|
|
17
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
18
|
+
|
|
19
|
+
import './index.scss'
|
|
20
|
+
import MultiConfigTabs from '../MultiConfigTabs'
|
|
21
|
+
|
|
22
|
+
type HeaderProps = {
|
|
23
|
+
setPreview?: any
|
|
24
|
+
back?: any
|
|
25
|
+
subEditor?: any
|
|
26
|
+
visualizationKey?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const FilterBehavior = {
|
|
30
|
+
Apply: 'Apply Button',
|
|
31
|
+
OnChange: 'Filter Change'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const Header = (props: HeaderProps) => {
|
|
35
|
+
const { setPreview, visualizationKey, subEditor } = props
|
|
36
|
+
const { config, setParentConfig, tabSelected } = useContext(DashboardContext)
|
|
37
|
+
if (!config) return null
|
|
38
|
+
const dispatch = useContext(DashboardDispatchContext)
|
|
39
|
+
const setTabSelected = (payload: number) => dispatch({ type: 'SET_TAB_SELECTED', payload })
|
|
40
|
+
const back = () => {
|
|
41
|
+
if (!visualizationKey) return
|
|
42
|
+
const newConfig: Config = { ...config } as Config
|
|
43
|
+
newConfig.visualizations[visualizationKey].editing = false
|
|
44
|
+
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { overlay } = useGlobalContext()
|
|
48
|
+
|
|
49
|
+
const [columns, setColumns] = useState<string[]>([])
|
|
50
|
+
|
|
51
|
+
const transform = new DataTransform()
|
|
52
|
+
|
|
53
|
+
const changeConfigValue = (parentObj, key, value) => {
|
|
54
|
+
let newConfig = { ...config }
|
|
55
|
+
if (!newConfig[parentObj]) newConfig[parentObj] = {}
|
|
56
|
+
newConfig[parentObj][key] = value
|
|
57
|
+
dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const setTab = index => {
|
|
61
|
+
setTabSelected(index)
|
|
62
|
+
if (index === 3) {
|
|
63
|
+
setPreview(true)
|
|
64
|
+
} else {
|
|
65
|
+
setPreview(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const addNewFilter = () => {
|
|
70
|
+
let dashboardConfig = { ...config.dashboard }
|
|
71
|
+
|
|
72
|
+
dashboardConfig.sharedFilters = dashboardConfig.sharedFilters || []
|
|
73
|
+
const newFilter: SharedFilter = { key: 'Dashboard Filter ' + (dashboardConfig.sharedFilters.length + 1) }
|
|
74
|
+
dashboardConfig.sharedFilters.push(newFilter)
|
|
75
|
+
|
|
76
|
+
dispatch({ type: 'UPDATE_CONFIG', payload: [{ ...config, dashboard: dashboardConfig }] })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const removeFilter = index => {
|
|
80
|
+
let dashboardConfig = { ...config.dashboard }
|
|
81
|
+
let visualizations = { ...config.visualizations }
|
|
82
|
+
|
|
83
|
+
dashboardConfig.sharedFilters?.splice(index, 1)
|
|
84
|
+
|
|
85
|
+
Object.keys(visualizations).forEach(vizKey => {
|
|
86
|
+
if (visualizations[vizKey].visualizationType === 'filter-dropdowns' && visualizations[vizKey].hide && visualizations[vizKey].hide.length > 0) {
|
|
87
|
+
if (visualizations[vizKey].hide.indexOf(index) !== -1) {
|
|
88
|
+
visualizations[vizKey].hide.splice(visualizations[vizKey].hide.indexOf(index), 1)
|
|
89
|
+
}
|
|
90
|
+
visualizations[vizKey].hide.forEach((hideIndex, i) => {
|
|
91
|
+
if (hideIndex > index) {
|
|
92
|
+
visualizations[vizKey].hide[i] = hideIndex - 1
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Ensures URL filters refresh after filter removal
|
|
99
|
+
if (dashboardConfig.datasets) {
|
|
100
|
+
Object.keys(dashboardConfig.datasets).forEach(datasetKey => {
|
|
101
|
+
dashboardConfig.datasets![datasetKey].runtimeDataUrl = ''
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const newConfig = { ...config, visualizations, dashboard: dashboardConfig }
|
|
106
|
+
dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
|
|
107
|
+
|
|
108
|
+
overlay?.actions.toggleOverlay()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const convertStateToConfig = (type = 'JSON') => {
|
|
112
|
+
let strippedState = JSON.parse(JSON.stringify(config))
|
|
113
|
+
delete strippedState.newViz
|
|
114
|
+
delete strippedState.runtime
|
|
115
|
+
|
|
116
|
+
if (type === 'JSON') {
|
|
117
|
+
return JSON.stringify(strippedState)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return strippedState
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const parsedData = convertStateToConfig()
|
|
125
|
+
|
|
126
|
+
// Emit the data in a regular JS event so it can be consumed by anything.
|
|
127
|
+
const event = new CustomEvent('updateVizConfig', { detail: parsedData })
|
|
128
|
+
|
|
129
|
+
window.dispatchEvent(event)
|
|
130
|
+
|
|
131
|
+
// Pass up to Editor if needed
|
|
132
|
+
if (setParentConfig) {
|
|
133
|
+
const newConfig = convertStateToConfig('object')
|
|
134
|
+
setParentConfig(newConfig)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
|
+
}, [config])
|
|
139
|
+
|
|
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
|
+
const handleCheck = e => {
|
|
546
|
+
const { checked } = e.currentTarget
|
|
547
|
+
if (checked) {
|
|
548
|
+
dispatch({ type: 'INITIALIZE_MULTIDASHBOARDS' })
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const multiInitialized = !!config.multiDashboards
|
|
553
|
+
|
|
554
|
+
return (
|
|
555
|
+
<div aria-level={2} role='heading' className={`editor-heading${subEditor ? ' sub-dashboard-viz' : ''}`}>
|
|
556
|
+
{subEditor ? (
|
|
557
|
+
<div className='heading-1 back-to' onClick={back} style={{ cursor: 'pointer' }}>
|
|
558
|
+
<span>←</span> Back to Dashboard
|
|
559
|
+
</div>
|
|
560
|
+
) : (
|
|
561
|
+
<div className='heading-1'>
|
|
562
|
+
Dashboard Editor{' '}
|
|
563
|
+
<span className='small'>
|
|
564
|
+
<input type='checkbox' onChange={handleCheck} checked={multiInitialized} disabled={multiInitialized} /> make multidashboard
|
|
565
|
+
</span>
|
|
566
|
+
<br />
|
|
567
|
+
{<input type='text' placeholder='Enter Dashboard Name Here' defaultValue={config.dashboard?.title} onChange={e => changeConfigValue('dashboard', 'title', e.target.value)} />}
|
|
568
|
+
</div>
|
|
569
|
+
)}
|
|
570
|
+
{!subEditor && (
|
|
571
|
+
<div className='toggle-bar__wrapper'>
|
|
572
|
+
<MultiConfigTabs isEditor />
|
|
573
|
+
<ul className='toggle-bar'>
|
|
574
|
+
<li
|
|
575
|
+
className={tabSelected === 0 ? 'active' : 'inactive'}
|
|
576
|
+
onClick={() => {
|
|
577
|
+
setTab(0)
|
|
578
|
+
}}
|
|
579
|
+
>
|
|
580
|
+
Dashboard Description
|
|
581
|
+
</li>
|
|
582
|
+
<li
|
|
583
|
+
className={tabSelected === 1 ? 'active' : 'inactive'}
|
|
584
|
+
onClick={() => {
|
|
585
|
+
setTab(1)
|
|
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>
|
|
606
|
+
</ul>
|
|
607
|
+
<div className='heading-body'>
|
|
608
|
+
{tabSelected === 0 && <input type='text' className='description-input' placeholder='Type a dashboard description here.' defaultValue={config.dashboard?.description} onChange={e => changeConfigValue('dashboard', 'description', e.target.value)} />}
|
|
609
|
+
{tabSelected === 1 && (
|
|
610
|
+
<>
|
|
611
|
+
{config.dashboard.sharedFilters &&
|
|
612
|
+
config.dashboard.sharedFilters.map((sharedFilter, index) => (
|
|
613
|
+
<span className='shared-filter-button' key={`shared-filter-${sharedFilter.key}`}>
|
|
614
|
+
<a
|
|
615
|
+
href='#'
|
|
616
|
+
onClick={e => {
|
|
617
|
+
e.preventDefault()
|
|
618
|
+
overlay?.actions.openOverlay(filterModal(sharedFilter, index))
|
|
619
|
+
}}
|
|
620
|
+
>
|
|
621
|
+
{sharedFilter.key}
|
|
622
|
+
</a>
|
|
623
|
+
<button onClick={() => removeFilter(index)}>X</button>
|
|
624
|
+
</span>
|
|
625
|
+
))}
|
|
626
|
+
<button onClick={addNewFilter}>Add New Filter</button>
|
|
627
|
+
|
|
628
|
+
<Select
|
|
629
|
+
value={config.filterBehavior}
|
|
630
|
+
fieldName='filterBehavior'
|
|
631
|
+
label='Filter Behavior'
|
|
632
|
+
initial='- Select Option -'
|
|
633
|
+
onchange={e => {
|
|
634
|
+
const newConfig = { ...config, filterBehavior: e.target.value }
|
|
635
|
+
dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
|
|
636
|
+
}}
|
|
637
|
+
options={Object.values(FilterBehavior)}
|
|
638
|
+
tooltip={
|
|
639
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
640
|
+
<Tooltip.Target>
|
|
641
|
+
<Icon display='question' color='' style={{ marginLeft: '0.5rem' }} />
|
|
642
|
+
</Tooltip.Target>
|
|
643
|
+
<Tooltip.Content>
|
|
644
|
+
<p>The Apply Button option changes the visualization when the user clicks "apply". The Filter Change option immediately changes the visualization when the selection is changed.</p>
|
|
645
|
+
</Tooltip.Content>
|
|
646
|
+
</Tooltip>
|
|
647
|
+
}
|
|
648
|
+
/>
|
|
649
|
+
</>
|
|
650
|
+
)}
|
|
651
|
+
{tabSelected === 2 && (
|
|
652
|
+
<>
|
|
653
|
+
<div className='wrap'>
|
|
654
|
+
<label>
|
|
655
|
+
<input type='checkbox' defaultChecked={config.table.show} onChange={e => changeConfigValue('table', 'show', e.target.checked)} />
|
|
656
|
+
Show Data Table(s)
|
|
657
|
+
</label>
|
|
658
|
+
<br />
|
|
659
|
+
|
|
660
|
+
<label>
|
|
661
|
+
<input type='checkbox' defaultChecked={config.table.expanded} onChange={e => changeConfigValue('table', 'expanded', e.target.checked)} />
|
|
662
|
+
Expanded by Default
|
|
663
|
+
</label>
|
|
664
|
+
<br />
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<div className='wrap'>
|
|
668
|
+
<label>
|
|
669
|
+
<input type='checkbox' defaultChecked={config.table.limitHeight} onChange={e => changeConfigValue('table', 'limitHeight', e.target.checked)} />
|
|
670
|
+
Limit Table Height
|
|
671
|
+
</label>
|
|
672
|
+
{config.table.limitHeight && <input className='table-height-input' type='text' placeholder='Height (px)' defaultValue={config.table.height} onChange={e => changeConfigValue('table', 'height', e.target.value)} />}
|
|
673
|
+
</div>
|
|
674
|
+
|
|
675
|
+
<div className='wrap'>
|
|
676
|
+
<label>
|
|
677
|
+
<input type='checkbox' defaultChecked={config.table.download} onChange={e => changeConfigValue('table', 'download', e.target.checked)} />
|
|
678
|
+
Show Download CSV Link
|
|
679
|
+
</label>
|
|
680
|
+
<label>
|
|
681
|
+
<input type='checkbox' defaultChecked={config.table.showDownloadUrl} onChange={e => changeConfigValue('table', 'showDownloadUrl', e.target.checked)} />
|
|
682
|
+
Show URL to Automatically Updated Data
|
|
683
|
+
</label>
|
|
684
|
+
</div>
|
|
685
|
+
</>
|
|
686
|
+
)}
|
|
687
|
+
</div>
|
|
688
|
+
</div>
|
|
689
|
+
)}
|
|
690
|
+
</div>
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export default Header
|