@cdc/dashboard 4.24.5 → 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 +122872 -112065
- 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 +10 -1
- package/package.json +12 -11
- package/src/CdcDashboard.tsx +5 -1
- package/src/CdcDashboardComponent.tsx +165 -306
- package/src/DashboardContext.tsx +9 -1
- package/src/_stories/Dashboard.stories.tsx +38 -34
- package/src/_stories/_mock/api-filter-chart.json +11 -35
- package/src/_stories/_mock/api-filter-map.json +17 -31
- package/src/_stories/_mock/multi-viz.json +2 -3
- package/src/_stories/_mock/pivot-filter.json +14 -12
- 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/Row.tsx +52 -19
- package/src/components/Toggle/Toggle.tsx +2 -4
- package/src/components/VisualizationRow.tsx +82 -24
- 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 +10 -48
- 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 +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 +2 -5
- 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,218 @@
|
|
|
1
|
+
import { Accordion, AccordionItem, AccordionItemButton, AccordionItemHeading, AccordionItemPanel } from 'react-accessible-accordion'
|
|
2
|
+
import { useContext, useMemo, useState } from 'react'
|
|
3
|
+
import { CheckBox, Select } from '@cdc/core/components/EditorPanel/Inputs'
|
|
4
|
+
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
5
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
6
|
+
import FieldSetWrapper from '@cdc/core/components/EditorPanel/FieldSetWrapper'
|
|
7
|
+
import FilterEditor from './components/FilterEditor'
|
|
8
|
+
import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
9
|
+
import { DashboardContext, DashboardDispatchContext } from '../../../DashboardContext'
|
|
10
|
+
import _ from 'lodash'
|
|
11
|
+
import { DashboardFilters } from '../../../types/DashboardFilters'
|
|
12
|
+
import { SharedFilter } from '../../../types/SharedFilter'
|
|
13
|
+
import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
|
|
14
|
+
import { useGlobalContext } from '@cdc/core/components/GlobalContext'
|
|
15
|
+
import DeleteFilterModal from './components/DeleteFilterModal'
|
|
16
|
+
|
|
17
|
+
type DashboardFitlersEditorProps = {
|
|
18
|
+
vizConfig: DashboardFilters
|
|
19
|
+
updateConfig: Function
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConfig, updateConfig }) => {
|
|
23
|
+
const { config, loadAPIFilters, data } = useContext(DashboardContext)
|
|
24
|
+
const { overlay } = useGlobalContext()
|
|
25
|
+
const {
|
|
26
|
+
dashboard: { sharedFilters },
|
|
27
|
+
visualizations
|
|
28
|
+
} = config
|
|
29
|
+
const dispatch = useContext(DashboardDispatchContext)
|
|
30
|
+
|
|
31
|
+
const existingOptions = useMemo(() => {
|
|
32
|
+
const sharedFilterIndexes = (config.visualizations[vizConfig.uid] as DashboardFilters).sharedFilterIndexes.map(Number)
|
|
33
|
+
return config.dashboard.sharedFilters
|
|
34
|
+
.map<[number, string]>(({ key }, i) => [i, key])
|
|
35
|
+
.filter(([filterIndex]) => !sharedFilterIndexes.includes(filterIndex)) // filter out already added filters
|
|
36
|
+
.map(([filterIndex, filterName]) => <option key={filterIndex} value={filterIndex}>{`${filterIndex} - ${filterName}`}</option>)
|
|
37
|
+
}, [config.visualizations, vizConfig.uid])
|
|
38
|
+
|
|
39
|
+
const openControls = useState({})
|
|
40
|
+
const [canAddExisting, setCanAddExisting] = useState(false)
|
|
41
|
+
|
|
42
|
+
const updateFilterProp = (prop: string, index: number, value) => {
|
|
43
|
+
const newSharedFilters = _.cloneDeep(sharedFilters)
|
|
44
|
+
const oldEndpoint = sharedFilters[index].apiFilter?.apiEndpoint
|
|
45
|
+
const oldAPIValueSelector = sharedFilters[index].apiFilter?.valueSelector
|
|
46
|
+
const apiFilterChanged = value.apiEndpoint !== oldEndpoint || value.valueSelector !== oldAPIValueSelector
|
|
47
|
+
newSharedFilters[index][prop] = value
|
|
48
|
+
if (prop === 'columnName') {
|
|
49
|
+
// changing a data column and want to load the data into the preview options
|
|
50
|
+
const sharedFiltersWithValues = addValuesToFilters<SharedFilter>(newSharedFilters, data)
|
|
51
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFiltersWithValues })
|
|
52
|
+
} else if (prop === 'apiFilter' && value.apiEndpoint && value.valueSelector && apiFilterChanged) {
|
|
53
|
+
// changing a api filter and want to load the api data into the preview.
|
|
54
|
+
// automatically dispatches SET_SHARED_FILTERS
|
|
55
|
+
loadAPIFilters(newSharedFilters, {})
|
|
56
|
+
} else {
|
|
57
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const removeFilter = index => {
|
|
62
|
+
const newSharedFilters = _.cloneDeep(sharedFilters)
|
|
63
|
+
|
|
64
|
+
newSharedFilters.splice(index, 1)
|
|
65
|
+
const shiftDownIndexes = Object.keys(sharedFilters).slice(index + 1)
|
|
66
|
+
const anyViz: Record<string, AnyVisualization> = visualizations
|
|
67
|
+
Object.keys(anyViz).forEach(vizKey => {
|
|
68
|
+
const viz = anyViz[vizKey]
|
|
69
|
+
if (viz.type === 'dashboardFilters') {
|
|
70
|
+
// shift the indexes down
|
|
71
|
+
const sharedFilterIndexes = viz.sharedFilterIndexes
|
|
72
|
+
.filter(filterIndex => filterIndex != index)
|
|
73
|
+
.map(filterIndex => {
|
|
74
|
+
if (shiftDownIndexes.includes(filterIndex.toString())) {
|
|
75
|
+
return filterIndex - 1
|
|
76
|
+
}
|
|
77
|
+
return filterIndex
|
|
78
|
+
})
|
|
79
|
+
dispatch({ type: 'UPDATE_VISUALIZATION', payload: { vizKey, configureData: { sharedFilterIndexes } } })
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const addNewFilter = () => {
|
|
86
|
+
const _sharedFilters = _.cloneDeep(sharedFilters) || []
|
|
87
|
+
const columnName = 'New Dashboard Filter ' + (_sharedFilters.length + 1)
|
|
88
|
+
const newFilter = { key: columnName, showDropdown: true, values: [] } as SharedFilter
|
|
89
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: [..._sharedFilters, newFilter] })
|
|
90
|
+
updateConfig({ ...vizConfig, sharedFilterIndexes: [...vizConfig.sharedFilterIndexes, _sharedFilters.length] })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Accordion allowZeroExpanded={true}>
|
|
95
|
+
<AccordionItem>
|
|
96
|
+
<AccordionItemHeading>
|
|
97
|
+
<AccordionItemButton>General</AccordionItemButton>
|
|
98
|
+
</AccordionItemHeading>
|
|
99
|
+
<AccordionItemPanel>
|
|
100
|
+
<Select
|
|
101
|
+
value={vizConfig.filterBehavior}
|
|
102
|
+
label='Filter Behavior'
|
|
103
|
+
updateField={(_section, _subsection, _key, value) => {
|
|
104
|
+
updateConfig({ ...vizConfig, filterBehavior: value })
|
|
105
|
+
}}
|
|
106
|
+
options={['Apply Button', 'Filter Change']}
|
|
107
|
+
tooltip={
|
|
108
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
109
|
+
<Tooltip.Target>
|
|
110
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
111
|
+
</Tooltip.Target>
|
|
112
|
+
<Tooltip.Content>
|
|
113
|
+
<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>
|
|
114
|
+
</Tooltip.Content>
|
|
115
|
+
</Tooltip>
|
|
116
|
+
}
|
|
117
|
+
/>
|
|
118
|
+
{vizConfig.filterBehavior === 'Filter Change' && (
|
|
119
|
+
<CheckBox
|
|
120
|
+
label='Auto Load'
|
|
121
|
+
value={vizConfig.autoLoad}
|
|
122
|
+
updateField={(_section, _subsection, _key, value) => {
|
|
123
|
+
updateConfig({ ...vizConfig, autoLoad: value })
|
|
124
|
+
}}
|
|
125
|
+
tooltip={
|
|
126
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
127
|
+
<Tooltip.Target>
|
|
128
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
129
|
+
</Tooltip.Target>
|
|
130
|
+
<Tooltip.Content>
|
|
131
|
+
<p>Check if you would like for all URL filters to automatically select a value when a parent filter is changed.</p>
|
|
132
|
+
</Tooltip.Content>
|
|
133
|
+
</Tooltip>
|
|
134
|
+
}
|
|
135
|
+
/>
|
|
136
|
+
)}
|
|
137
|
+
</AccordionItemPanel>
|
|
138
|
+
</AccordionItem>
|
|
139
|
+
|
|
140
|
+
<AccordionItem>
|
|
141
|
+
<AccordionItemHeading>
|
|
142
|
+
<AccordionItemButton>Filters</AccordionItemButton>
|
|
143
|
+
</AccordionItemHeading>
|
|
144
|
+
<AccordionItemPanel>
|
|
145
|
+
{vizConfig.sharedFilterIndexes.map(index => {
|
|
146
|
+
const filter = sharedFilters[index]
|
|
147
|
+
return (
|
|
148
|
+
<FieldSetWrapper
|
|
149
|
+
key={filter.key + index}
|
|
150
|
+
fieldName={filter.key}
|
|
151
|
+
fieldKey={index}
|
|
152
|
+
fieldType='Dashboard Filter'
|
|
153
|
+
controls={openControls}
|
|
154
|
+
deleteField={() => {
|
|
155
|
+
overlay?.actions.openOverlay(
|
|
156
|
+
<DeleteFilterModal
|
|
157
|
+
removeFilterCompletely={removeFilter}
|
|
158
|
+
removeFilterFromViz={index => {
|
|
159
|
+
updateConfig({ ...vizConfig, sharedFilterIndexes: vizConfig.sharedFilterIndexes.filter(i => i !== index) })
|
|
160
|
+
}}
|
|
161
|
+
filterIndex={index}
|
|
162
|
+
/>
|
|
163
|
+
)
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<FilterEditor
|
|
167
|
+
filter={filter}
|
|
168
|
+
updateFilterProp={(name, value) => {
|
|
169
|
+
updateFilterProp(name, index, value)
|
|
170
|
+
}}
|
|
171
|
+
config={config}
|
|
172
|
+
/>
|
|
173
|
+
</FieldSetWrapper>
|
|
174
|
+
)
|
|
175
|
+
})}
|
|
176
|
+
<button onClick={addNewFilter} className='btn btn-primary full-width'>
|
|
177
|
+
Add Filter
|
|
178
|
+
</button>
|
|
179
|
+
{canAddExisting ? (
|
|
180
|
+
<label>
|
|
181
|
+
<span className='edit-label column-heading'>
|
|
182
|
+
Select Existing Dashboard Filter
|
|
183
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
184
|
+
<Tooltip.Target>
|
|
185
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
186
|
+
</Tooltip.Target>
|
|
187
|
+
<Tooltip.Content>
|
|
188
|
+
<p>This feature is indentended to support legacy functionality. Be advised that any change to the filter in this editor will reflect on the whole dashboard. </p>
|
|
189
|
+
</Tooltip.Content>
|
|
190
|
+
</Tooltip>
|
|
191
|
+
</span>
|
|
192
|
+
<select
|
|
193
|
+
value={''}
|
|
194
|
+
onChange={e => {
|
|
195
|
+
updateConfig({ ...vizConfig, sharedFilterIndexes: [...vizConfig.sharedFilterIndexes, e.target.value] })
|
|
196
|
+
setCanAddExisting(false)
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
{[
|
|
200
|
+
<option key='select' value=''>
|
|
201
|
+
Select
|
|
202
|
+
</option>,
|
|
203
|
+
...existingOptions
|
|
204
|
+
]}
|
|
205
|
+
</select>
|
|
206
|
+
</label>
|
|
207
|
+
) : (
|
|
208
|
+
<button onClick={() => setCanAddExisting(true)} className='btn btn-primary full-width'>
|
|
209
|
+
Add Existing Dashboard Filter
|
|
210
|
+
</button>
|
|
211
|
+
)}
|
|
212
|
+
</AccordionItemPanel>
|
|
213
|
+
</AccordionItem>
|
|
214
|
+
</Accordion>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export default DashboardFiltersEditor
|
package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useGlobalContext } from '@cdc/core/components/GlobalContext'
|
|
2
|
+
import Modal from '@cdc/core/components/ui/Modal'
|
|
3
|
+
import { DashboardContext } from '../../../../DashboardContext'
|
|
4
|
+
import { useContext } from 'react'
|
|
5
|
+
import { DashboardFilters } from '../../../../types/DashboardFilters'
|
|
6
|
+
|
|
7
|
+
type DeleteFilterProps = {
|
|
8
|
+
removeFilterCompletely: (number) => void
|
|
9
|
+
removeFilterFromViz: (number) => void
|
|
10
|
+
filterIndex: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DeleteFilterModal: React.FC<DeleteFilterProps> = ({ removeFilterCompletely, removeFilterFromViz, filterIndex }) => {
|
|
14
|
+
const { overlay } = useGlobalContext()
|
|
15
|
+
const { config } = useContext(DashboardContext)
|
|
16
|
+
const filterUsedByMany = Object.values(config.visualizations).filter(viz => (viz as DashboardFilters).sharedFilterIndexes?.map(Number).includes(Number(filterIndex))).length > 1
|
|
17
|
+
|
|
18
|
+
const message = filterUsedByMany ? 'This filter is used by multiple visualizations. You can either delete the filter from this visualization only or you can delete the filter completely, which will also remove it from other visualizations.' : 'Are you sure you want to delete this filter?'
|
|
19
|
+
return (
|
|
20
|
+
<Modal showClose={true}>
|
|
21
|
+
<Modal.Content>
|
|
22
|
+
<p>{message}</p>
|
|
23
|
+
{filterUsedByMany && (
|
|
24
|
+
<button
|
|
25
|
+
className='btn btn-warning'
|
|
26
|
+
onClick={() => {
|
|
27
|
+
removeFilterFromViz(filterIndex)
|
|
28
|
+
overlay?.actions.toggleOverlay()
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
Delete from Visualization
|
|
32
|
+
</button>
|
|
33
|
+
)}
|
|
34
|
+
<button
|
|
35
|
+
className='btn btn-danger'
|
|
36
|
+
onClick={() => {
|
|
37
|
+
removeFilterCompletely(filterIndex)
|
|
38
|
+
overlay?.actions.toggleOverlay()
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
Delete{filterUsedByMany ? ' Completely' : ''}
|
|
42
|
+
</button>
|
|
43
|
+
</Modal.Content>
|
|
44
|
+
</Modal>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default DeleteFilterModal
|
|
@@ -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'
|