@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,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
|
|
@@ -2,19 +2,9 @@ import { useEffect, useContext } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
|
|
4
4
|
|
|
5
|
-
// types
|
|
6
|
-
import { type SharedFilter } from '../../types/SharedFilter'
|
|
7
|
-
import { type DashboardConfig as Config } from '../../types/DashboardConfig'
|
|
8
|
-
import { useGlobalContext } from '@cdc/core/components/GlobalContext'
|
|
9
|
-
|
|
10
|
-
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
11
|
-
import Icon from '@cdc/core/components/ui/Icon'
|
|
12
|
-
import Select from '@cdc/core/components/ui/Select'
|
|
13
|
-
|
|
14
5
|
import './index.scss'
|
|
15
6
|
import MultiConfigTabs from '../MultiConfigTabs'
|
|
16
7
|
import { Tab } from '../../types/Tab'
|
|
17
|
-
import FilterModal from './FilterModal'
|
|
18
8
|
import _ from 'lodash'
|
|
19
9
|
|
|
20
10
|
type HeaderProps = {
|
|
@@ -29,7 +19,7 @@ export const FilterBehavior = {
|
|
|
29
19
|
}
|
|
30
20
|
|
|
31
21
|
const Header = (props: HeaderProps) => {
|
|
32
|
-
const tabs: Tab[] = ['Dashboard Description', '
|
|
22
|
+
const tabs: Tab[] = ['Dashboard Description', 'Data Table Settings', 'Dashboard Preview']
|
|
33
23
|
const { visualizationKey, subEditor } = props
|
|
34
24
|
const { config, setParentConfig, tabSelected } = useContext(DashboardContext)
|
|
35
25
|
if (!config) return null
|
|
@@ -41,8 +31,6 @@ const Header = (props: HeaderProps) => {
|
|
|
41
31
|
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
42
32
|
}
|
|
43
33
|
|
|
44
|
-
const { overlay } = useGlobalContext()
|
|
45
|
-
|
|
46
34
|
const changeConfigValue = (parentObj, key, value) => {
|
|
47
35
|
let newConfig = { ...config }
|
|
48
36
|
if (!newConfig[parentObj]) newConfig[parentObj] = {}
|
|
@@ -50,48 +38,6 @@ const Header = (props: HeaderProps) => {
|
|
|
50
38
|
dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
|
|
51
39
|
}
|
|
52
40
|
|
|
53
|
-
const addNewFilter = () => {
|
|
54
|
-
let dashboardConfig = { ...config.dashboard }
|
|
55
|
-
|
|
56
|
-
dashboardConfig.sharedFilters = dashboardConfig.sharedFilters || []
|
|
57
|
-
const newFilter: SharedFilter = { key: 'Dashboard Filter ' + (dashboardConfig.sharedFilters.length + 1) }
|
|
58
|
-
dashboardConfig.sharedFilters.push(newFilter)
|
|
59
|
-
|
|
60
|
-
dispatch({ type: 'UPDATE_CONFIG', payload: [{ ...config, dashboard: dashboardConfig }] })
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const removeFilter = index => {
|
|
64
|
-
let dashboardConfig = { ...config.dashboard }
|
|
65
|
-
let visualizations = { ...config.visualizations }
|
|
66
|
-
|
|
67
|
-
dashboardConfig.sharedFilters?.splice(index, 1)
|
|
68
|
-
|
|
69
|
-
Object.keys(visualizations).forEach(vizKey => {
|
|
70
|
-
if (visualizations[vizKey].visualizationType === 'filter-dropdowns' && visualizations[vizKey].hide && visualizations[vizKey].hide.length > 0) {
|
|
71
|
-
if (visualizations[vizKey].hide.indexOf(index) !== -1) {
|
|
72
|
-
visualizations[vizKey].hide.splice(visualizations[vizKey].hide.indexOf(index), 1)
|
|
73
|
-
}
|
|
74
|
-
visualizations[vizKey].hide.forEach((hideIndex, i) => {
|
|
75
|
-
if (hideIndex > index) {
|
|
76
|
-
visualizations[vizKey].hide[i] = hideIndex - 1
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
// Ensures URL filters refresh after filter removal
|
|
83
|
-
if (dashboardConfig.datasets) {
|
|
84
|
-
Object.keys(dashboardConfig.datasets).forEach(datasetKey => {
|
|
85
|
-
dashboardConfig.datasets![datasetKey].runtimeDataUrl = ''
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const newConfig = { ...config, visualizations, dashboard: dashboardConfig }
|
|
90
|
-
dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
|
|
91
|
-
|
|
92
|
-
overlay?.actions.toggleOverlay()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
41
|
const convertStateToConfig = (type = 'JSON') => {
|
|
96
42
|
let strippedState = JSON.parse(JSON.stringify(config))
|
|
97
43
|
delete strippedState.newViz
|
|
@@ -166,48 +112,6 @@ const Header = (props: HeaderProps) => {
|
|
|
166
112
|
</ul>
|
|
167
113
|
<div className='heading-body'>
|
|
168
114
|
{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' && (
|
|
170
|
-
<>
|
|
171
|
-
{config.dashboard.sharedFilters &&
|
|
172
|
-
config.dashboard.sharedFilters.map((sharedFilter, index) => (
|
|
173
|
-
<span className='shared-filter-button' key={`shared-filter-${sharedFilter.key}`}>
|
|
174
|
-
<a
|
|
175
|
-
href='#'
|
|
176
|
-
onClick={e => {
|
|
177
|
-
e.preventDefault()
|
|
178
|
-
overlay?.actions.openOverlay(<FilterModal index={index} config={config} filterState={sharedFilter} removeFilter={removeFilter} />)
|
|
179
|
-
}}
|
|
180
|
-
>
|
|
181
|
-
{sharedFilter.key}
|
|
182
|
-
</a>
|
|
183
|
-
<button onClick={() => removeFilter(index)}>X</button>
|
|
184
|
-
</span>
|
|
185
|
-
))}
|
|
186
|
-
<button onClick={addNewFilter}>Add New Filter</button>
|
|
187
|
-
|
|
188
|
-
<Select
|
|
189
|
-
value={config.filterBehavior}
|
|
190
|
-
fieldName='filterBehavior'
|
|
191
|
-
label='Filter Behavior'
|
|
192
|
-
initial='- Select Option -'
|
|
193
|
-
onchange={e => {
|
|
194
|
-
const newConfig = { ...config, filterBehavior: e.target.value }
|
|
195
|
-
dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
|
|
196
|
-
}}
|
|
197
|
-
options={Object.values(FilterBehavior)}
|
|
198
|
-
tooltip={
|
|
199
|
-
<Tooltip style={{ textTransform: 'none' }}>
|
|
200
|
-
<Tooltip.Target>
|
|
201
|
-
<Icon display='question' color='' style={{ marginLeft: '0.5rem' }} />
|
|
202
|
-
</Tooltip.Target>
|
|
203
|
-
<Tooltip.Content>
|
|
204
|
-
<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>
|
|
205
|
-
</Tooltip.Content>
|
|
206
|
-
</Tooltip>
|
|
207
|
-
}
|
|
208
|
-
/>
|
|
209
|
-
</>
|
|
210
|
-
)}
|
|
211
115
|
{tabSelected === 'Data Table Settings' && (
|
|
212
116
|
<>
|
|
213
117
|
<div className='wrap'>
|
|
@@ -65,14 +65,14 @@ const Tab = ({ name, handleClick, tabs, index, active }) => {
|
|
|
65
65
|
|
|
66
66
|
return (
|
|
67
67
|
<li className='nav-item'>
|
|
68
|
-
<
|
|
68
|
+
<div className={`edit nav-link${active ? ' active' : ''}`} aria-current={active ? 'page' : null} onClick={onClick}>
|
|
69
69
|
{canMoveLeft && <button onClick={() => handleReorder(index, -1)}>{'<'}</button>}
|
|
70
70
|
{editing ? <input type='text' defaultValue={name} onBlur={onBlur} ref={inputRef} /> : <>{name}</>}
|
|
71
71
|
{canMoveRight && <button onClick={() => handleReorder(index, 1)}>{'>'}</button>}
|
|
72
72
|
<button className='remove' onClick={handleRemove}>
|
|
73
73
|
X
|
|
74
74
|
</button>
|
|
75
|
-
</
|
|
75
|
+
</div>
|
|
76
76
|
</li>
|
|
77
77
|
)
|
|
78
78
|
}
|
|
@@ -95,9 +95,9 @@ const MultiConfigTabs = () => {
|
|
|
95
95
|
<Tab key={tab + index} name={tab} tabs={tabs} index={index} handleClick={() => saveAndLoad(index)} active={index === activeTab} />
|
|
96
96
|
))}
|
|
97
97
|
<li className='nav-item'>
|
|
98
|
-
<
|
|
98
|
+
<button className='nav-link add' onClick={() => dispatch({ type: 'ADD_NEW_DASHBOARD' })}>
|
|
99
99
|
+
|
|
100
|
-
</
|
|
100
|
+
</button>
|
|
101
101
|
</li>
|
|
102
102
|
</ul>
|
|
103
103
|
)
|
package/src/components/Row.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import { DataDesignerModal } from './DataDesignerModal'
|
|
|
17
17
|
import { useGlobalContext } from '@cdc/core/components/GlobalContext'
|
|
18
18
|
import { iconHash } from '../helpers/iconHash'
|
|
19
19
|
import _ from 'lodash'
|
|
20
|
+
import { Visualization } from '@cdc/core/types/Visualization'
|
|
20
21
|
|
|
21
22
|
type RowMenuProps = {
|
|
22
23
|
rowIdx: number
|
|
@@ -104,9 +105,20 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
const deleteRow = () => {
|
|
107
|
-
|
|
108
|
+
let newVisualizations = { ...config.visualizations }
|
|
109
|
+
|
|
110
|
+
//delete the instantiated widgets
|
|
111
|
+
if (rows[rowIdx] && rows[rowIdx].columns && rows[rowIdx].columns.length && config.visualizations) {
|
|
112
|
+
rows[rowIdx].columns.forEach(column => {
|
|
113
|
+
if (column.widget) {
|
|
114
|
+
delete newVisualizations[column.widget]
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
108
118
|
|
|
109
|
-
|
|
119
|
+
rows.splice(rowIdx, 1) // delete the row
|
|
120
|
+
|
|
121
|
+
updateConfig({ ...config, rows, visualizations: newVisualizations })
|
|
110
122
|
}
|
|
111
123
|
|
|
112
124
|
const layoutList = [
|
|
@@ -149,31 +161,52 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
|
|
|
149
161
|
)
|
|
150
162
|
}
|
|
151
163
|
|
|
152
|
-
|
|
164
|
+
type RowProps = { row: ConfigRow; idx: number; uuid: number | string }
|
|
165
|
+
|
|
166
|
+
const Row: React.FC<RowProps> = ({ row, idx: rowIdx, uuid }) => {
|
|
153
167
|
const { overlay } = useGlobalContext()
|
|
154
|
-
|
|
155
|
-
<div className='builder-row' data-row-id={rowIdx}>
|
|
156
|
-
<RowMenu rowIdx={rowIdx} />
|
|
157
|
-
<div className='column-container'>
|
|
158
|
-
<>
|
|
159
|
-
<button
|
|
160
|
-
title='Configure Data'
|
|
161
|
-
className='btn btn-configure-row'
|
|
162
|
-
onClick={() => {
|
|
163
|
-
overlay?.actions.openOverlay(<DataDesignerModal rowIndex={rowIdx} />)
|
|
164
|
-
}}
|
|
165
|
-
>
|
|
166
|
-
{iconHash['gear']}
|
|
167
|
-
</button>
|
|
168
|
+
const dispatch = useContext(DashboardDispatchContext)
|
|
168
169
|
|
|
170
|
+
const configureFootnotes = () => {
|
|
171
|
+
if (!row.footnotesId) {
|
|
172
|
+
const type = 'footnotes'
|
|
173
|
+
const uid = type + Date.now()
|
|
174
|
+
const newVisualizationConfig = {
|
|
175
|
+
uid,
|
|
176
|
+
type,
|
|
177
|
+
visualizationType: type,
|
|
178
|
+
editing: true
|
|
179
|
+
}
|
|
180
|
+
dispatch({ type: 'ADD_FOOTNOTE', payload: { id: uid, rowIndex: rowIdx, config: newVisualizationConfig as Visualization } })
|
|
181
|
+
} else {
|
|
182
|
+
dispatch({ type: 'UPDATE_VISUALIZATION', payload: { vizKey: row.footnotesId, configureData: { editing: true } } })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return (
|
|
186
|
+
<>
|
|
187
|
+
<div className='builder-row' data-row-id={rowIdx}>
|
|
188
|
+
<RowMenu rowIdx={rowIdx} />
|
|
189
|
+
<button
|
|
190
|
+
title='Configure Data'
|
|
191
|
+
className='btn btn-configure-row'
|
|
192
|
+
onClick={() => {
|
|
193
|
+
overlay?.actions.openOverlay(<DataDesignerModal rowIndex={rowIdx} />)
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
{iconHash['gear']}
|
|
197
|
+
</button>
|
|
198
|
+
<div className='column-container'>
|
|
169
199
|
{row.columns
|
|
170
200
|
.filter(column => column.width)
|
|
171
201
|
.map((column, colIdx) => (
|
|
172
202
|
<Column data={column} key={`row-${uuid}-col-${colIdx}`} rowIdx={rowIdx} colIdx={colIdx} />
|
|
173
203
|
))}
|
|
174
|
-
|
|
204
|
+
</div>
|
|
205
|
+
<button className='btn btn-primary footnotes' onClick={configureFootnotes}>
|
|
206
|
+
{row.footnotesId ? 'Edit' : 'Add'} Footnotes
|
|
207
|
+
</button>
|
|
175
208
|
</div>
|
|
176
|
-
|
|
209
|
+
</>
|
|
177
210
|
)
|
|
178
211
|
}
|
|
179
212
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { useContext } from 'react'
|
|
2
|
-
import { DashboardDispatchContext } from '../../DashboardContext'
|
|
3
1
|
import { ConfigRow } from '../../types/ConfigRow'
|
|
4
|
-
import {
|
|
2
|
+
import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
5
3
|
import { getIcon } from '../../helpers/iconHash'
|
|
6
4
|
import './toggle-style.css'
|
|
7
5
|
import _ from 'lodash'
|
|
@@ -9,7 +7,7 @@ import _ from 'lodash'
|
|
|
9
7
|
type ToggleProps = {
|
|
10
8
|
active: number
|
|
11
9
|
row: ConfigRow
|
|
12
|
-
visualizations: Record<string,
|
|
10
|
+
visualizations: Record<string, AnyVisualization>
|
|
13
11
|
setToggled: (colIndex: number) => void
|
|
14
12
|
}
|
|
15
13
|
const Toggle: React.FC<ToggleProps> = ({ active, row, visualizations, setToggled }) => {
|
|
@@ -1,56 +1,104 @@
|
|
|
1
1
|
import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
|
|
2
|
-
import React, {
|
|
2
|
+
import React, { useContext, useMemo } from 'react'
|
|
3
3
|
import Toggle from './Toggle'
|
|
4
4
|
import _ from 'lodash'
|
|
5
5
|
import { ConfigRow } from '../types/ConfigRow'
|
|
6
|
-
import CdcMap from '@cdc/map'
|
|
7
6
|
import CdcChart from '@cdc/chart'
|
|
8
7
|
import CdcDataBite from '@cdc/data-bite'
|
|
8
|
+
import CdcMap from '@cdc/map'
|
|
9
9
|
import CdcWaffleChart from '@cdc/waffle-chart'
|
|
10
10
|
import CdcMarkupInclude from '@cdc/markup-include'
|
|
11
11
|
import CdcFilteredText from '@cdc/filtered-text'
|
|
12
|
-
import
|
|
13
|
-
import { FilterBehavior } from './Header/Header'
|
|
12
|
+
import DashboardSharedFilters, { APIFilterDropdowns } from './DashboardFilters'
|
|
14
13
|
import { DashboardContext } from '../DashboardContext'
|
|
15
14
|
import { ViewPort } from '@cdc/core/types/ViewPort'
|
|
16
|
-
import { getVizConfig } from '../helpers/getVizConfig'
|
|
15
|
+
import { getFootnotesVizConfig, getVizConfig } from '../helpers/getVizConfig'
|
|
17
16
|
import { TableConfig } from '@cdc/core/components/DataTable/types/TableConfig'
|
|
17
|
+
import FootnotesStandAlone from '@cdc/core/components/Footnotes/FootnotesStandAlone'
|
|
18
|
+
import CollapsibleVisualizationRow from './CollapsibleVisualizationRow'
|
|
19
|
+
import { DashboardFilters } from '../types/DashboardFilters'
|
|
20
|
+
import { hasDashboardApplyBehavior } from '../helpers/hasDashboardApplyBehavior'
|
|
21
|
+
|
|
22
|
+
type VisualizationWrapperProps = {
|
|
23
|
+
allExpanded: boolean
|
|
24
|
+
children: React.ReactNode
|
|
25
|
+
currentViewport: ViewPort
|
|
26
|
+
groupName: string
|
|
27
|
+
row: ConfigRow
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const VisualizationWrapper: React.FC<VisualizationWrapperProps> = ({ allExpanded, currentViewport, groupName, row, children }) => {
|
|
31
|
+
return row.expandCollapseAllButtons ? (
|
|
32
|
+
<div className='collapsable-multiviz-container'>
|
|
33
|
+
<CollapsibleVisualizationRow allExpanded={allExpanded} fontSize={'26px'} groupName={groupName} currentViewport={currentViewport}>
|
|
34
|
+
{children}
|
|
35
|
+
</CollapsibleVisualizationRow>
|
|
36
|
+
</div>
|
|
37
|
+
) : (
|
|
38
|
+
<>
|
|
39
|
+
<h3>{groupName}</h3>
|
|
40
|
+
{children}
|
|
41
|
+
</>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
18
44
|
|
|
19
45
|
type VizRowProps = {
|
|
46
|
+
allExpanded: boolean
|
|
20
47
|
filteredDataOverride?: Object[]
|
|
48
|
+
groupName: string
|
|
21
49
|
row: ConfigRow
|
|
22
50
|
rowIndex: number
|
|
23
51
|
setSharedFilter: Function
|
|
24
52
|
updateChildConfig: Function
|
|
25
|
-
applyFilters: MouseEventHandler<HTMLButtonElement>
|
|
26
53
|
apiFilterDropdowns: APIFilterDropdowns
|
|
27
|
-
handleOnChange: Function
|
|
28
54
|
currentViewport: ViewPort
|
|
29
55
|
}
|
|
30
56
|
|
|
31
|
-
const VisualizationRow: React.FC<VizRowProps> = ({ filteredDataOverride, row, rowIndex: index, setSharedFilter, updateChildConfig, applyFilters, apiFilterDropdowns, handleOnChange, currentViewport }) => {
|
|
57
|
+
const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOverride, groupName, row, rowIndex: index, setSharedFilter, updateChildConfig, applyFilters, apiFilterDropdowns, handleOnChange, currentViewport }) => {
|
|
32
58
|
const { config, filteredData: dashboardFilteredData, data: rawData } = useContext(DashboardContext)
|
|
33
59
|
const [show, setShow] = React.useState(row.columns.map((col, i) => i === 0))
|
|
34
60
|
const setToggled = (colIndex: number) => {
|
|
35
61
|
setShow(show.map((_, i) => i === colIndex))
|
|
36
62
|
}
|
|
37
63
|
const inNoDataState = useMemo(() => {
|
|
38
|
-
const vals = Object.values(rawData)
|
|
64
|
+
const vals = Object.values(rawData).flatMap(val => val)
|
|
39
65
|
if (!vals.length) return true
|
|
40
66
|
return vals.some(val => val === undefined)
|
|
41
67
|
}, [rawData])
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
68
|
+
|
|
69
|
+
const footnotesConfig = useMemo(() => {
|
|
70
|
+
if (row.footnotesId) {
|
|
71
|
+
const footnoteConfig = getFootnotesVizConfig(row.footnotesId, index, config)
|
|
72
|
+
if (row.multiVizColumn && filteredDataOverride) {
|
|
73
|
+
const vizCategory = filteredDataOverride[0][row.multiVizColumn]
|
|
74
|
+
// the multiViz filtering filtering is applied after the dashboard filters
|
|
75
|
+
const categoryFootnote = footnoteConfig.formattedData.filter(d => d[row.multiVizColumn] === vizCategory)
|
|
76
|
+
footnoteConfig.formattedData = categoryFootnote
|
|
77
|
+
}
|
|
78
|
+
return footnoteConfig
|
|
45
79
|
}
|
|
46
80
|
return null
|
|
81
|
+
}, [config, row, rawData, dashboardFilteredData])
|
|
82
|
+
|
|
83
|
+
const applyButtonNotClicked = (vizConfig: DashboardFilters): boolean => {
|
|
84
|
+
const dashboardFilters = Object.values(config.visualizations).filter(v => v.type === 'dashboardFilters') as DashboardFilters[]
|
|
85
|
+
const applyFilters = dashboardFilters.filter(v => !v.autoLoad).flatMap(v => v.sharedFilterIndexes)
|
|
86
|
+
if (hasDashboardApplyBehavior(config.visualizations) && vizConfig.autoLoad) {
|
|
87
|
+
return applyFilters.some(index => {
|
|
88
|
+
const { queuedActive, active } = config.dashboard.sharedFilters[index]
|
|
89
|
+
if (!active && !queuedActive) return true
|
|
90
|
+
if (!queuedActive) return false
|
|
91
|
+
return queuedActive !== active
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
return false
|
|
47
95
|
}
|
|
48
96
|
return (
|
|
49
|
-
<div className={`
|
|
97
|
+
<div className={`row mb-5 ${row.equalHeight ? 'equal-height' : ''} ${row.toggle ? 'toggle' : ''}`} key={`row__${index}`}>
|
|
50
98
|
{row.toggle && <Toggle row={row} visualizations={config.visualizations} active={show.indexOf(true)} setToggled={setToggled} />}
|
|
51
99
|
{row.columns.map((col, colIndex) => {
|
|
52
100
|
if (col.width) {
|
|
53
|
-
if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`
|
|
101
|
+
if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`col-md-${col.width}`}></div>
|
|
54
102
|
|
|
55
103
|
const visualizationConfig = getVizConfig(col.widget, index, config, rawData, dashboardFilteredData)
|
|
56
104
|
if (filteredDataOverride) {
|
|
@@ -67,12 +115,15 @@ const VisualizationRow: React.FC<VizRowProps> = ({ filteredDataOverride, row, ro
|
|
|
67
115
|
{visualizationConfig.dataKey} (Go to Table)
|
|
68
116
|
</a>
|
|
69
117
|
)
|
|
70
|
-
const hideFilter = visualizationConfig.
|
|
118
|
+
const hideFilter = inNoDataState && visualizationConfig.type === 'dashboardFilters' && applyButtonNotClicked(visualizationConfig)
|
|
71
119
|
|
|
72
120
|
const shouldShow = row.toggle === undefined || (row.toggle && show[colIndex])
|
|
121
|
+
|
|
122
|
+
const body = <></>
|
|
123
|
+
|
|
73
124
|
return (
|
|
74
|
-
<
|
|
75
|
-
<
|
|
125
|
+
<div key={`vis__${index}__${colIndex}`} className={`col-${col.width} ${!shouldShow ? 'd-none' : ''}`}>
|
|
126
|
+
<VisualizationWrapper allExpanded={allExpanded} currentViewport={currentViewport} groupName={groupName} row={row}>
|
|
76
127
|
{visualizationConfig.type === 'chart' && (
|
|
77
128
|
<CdcChart
|
|
78
129
|
key={col.widget}
|
|
@@ -153,11 +204,16 @@ const VisualizationRow: React.FC<VizRowProps> = ({ filteredDataOverride, row, ro
|
|
|
153
204
|
configUrl={undefined}
|
|
154
205
|
/>
|
|
155
206
|
)}
|
|
156
|
-
{visualizationConfig.type === '
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
207
|
+
{visualizationConfig.type === 'dashboardFilters' && !hideFilter && (
|
|
208
|
+
<DashboardSharedFilters
|
|
209
|
+
setConfig={newConfig => {
|
|
210
|
+
updateChildConfig(col.widget, newConfig)
|
|
211
|
+
}}
|
|
212
|
+
key={col.widget}
|
|
213
|
+
visualizationConfig={visualizationConfig as DashboardFilters}
|
|
214
|
+
apiFilterDropdowns={apiFilterDropdowns}
|
|
215
|
+
currentViewport={currentViewport}
|
|
216
|
+
/>
|
|
161
217
|
)}
|
|
162
218
|
{visualizationConfig.type === 'table' && (
|
|
163
219
|
<DataTableStandAlone
|
|
@@ -170,12 +226,14 @@ const VisualizationRow: React.FC<VizRowProps> = ({ filteredDataOverride, row, ro
|
|
|
170
226
|
viewport={currentViewport}
|
|
171
227
|
/>
|
|
172
228
|
)}
|
|
173
|
-
|
|
174
|
-
|
|
229
|
+
{visualizationConfig.type === 'footnotes' && <FootnotesStandAlone key={col.widget} visualizationKey={col.widget} config={visualizationConfig} viewport={currentViewport} />}
|
|
230
|
+
</VisualizationWrapper>
|
|
231
|
+
</div>
|
|
175
232
|
)
|
|
176
233
|
}
|
|
177
234
|
return <React.Fragment key={`vis__${index}__${colIndex}`}></React.Fragment>
|
|
178
235
|
})}
|
|
236
|
+
{row.footnotesId ? <FootnotesStandAlone isEditor={false} visualizationKey={row.footnotesId} config={footnotesConfig} viewport={currentViewport} /> : null}
|
|
179
237
|
</div>
|
|
180
238
|
)
|
|
181
239
|
}
|