@cdc/dashboard 4.24.10 → 4.24.12-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 +51165 -49100
- package/examples/ed-visits-county-file.json +141 -357
- package/examples/private/DEV-10120.json +1294 -0
- package/examples/private/DEV-9199.json +606 -0
- package/examples/private/DEV-9644.json +20092 -0
- package/examples/private/DEV-9684.json +2135 -0
- package/examples/private/DEV-9989.json +229 -0
- package/examples/private/art-dashboard.json +18174 -0
- package/examples/private/art-scratch.json +2406 -0
- package/examples/private/dashboard-config-ehdi.json +29915 -0
- package/examples/private/dashboard-margins.js +15 -0
- package/examples/private/dataset.json +1452 -0
- package/examples/private/ehdi-data.json +29502 -0
- package/examples/private/gaza-issue.json +1214 -0
- package/examples/private/workforce.json +2041 -0
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +43 -29
- package/src/CdcDashboardComponent.tsx +91 -52
- package/src/DashboardContext.tsx +2 -0
- package/src/_stories/Dashboard.stories.tsx +8 -0
- package/src/_stories/_mock/api-filter-error.json +55 -0
- package/src/_stories/_mock/group-pivot-filter.json +10 -5
- package/src/components/CollapsibleVisualizationRow.tsx +8 -2
- package/src/components/DashboardFilters/DashboardFilters.tsx +121 -58
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +3 -1
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +54 -50
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +13 -7
- package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +21 -0
- package/src/components/DashboardFilters/dashboardfilter.styles.css +27 -0
- package/src/components/Grid.tsx +1 -1
- package/src/components/Header/Header.tsx +71 -10
- package/src/components/Header/index.scss +0 -5
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +28 -6
- package/src/components/MultiConfigTabs/MultiTabs.tsx +2 -0
- package/src/components/MultiConfigTabs/multiconfigtabs.styles.css +4 -11
- package/src/components/Row.tsx +59 -13
- package/src/components/VisualizationRow.tsx +30 -22
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +0 -1
- package/src/components/Widget.tsx +23 -1
- package/src/data/initial-state.js +2 -1
- package/src/helpers/addValuesToDashboardFilters.ts +4 -2
- package/src/helpers/apiFilterHelpers.ts +55 -20
- package/src/helpers/changeFilterActive.ts +3 -0
- package/src/helpers/filterData.ts +1 -1
- package/src/helpers/getVizRowColumnLocator.ts +1 -0
- package/src/helpers/loadAPIFilters.ts +32 -10
- package/src/helpers/reloadURLHelpers.ts +9 -2
- package/src/helpers/shouldLoadAllFilters.ts +30 -0
- package/src/helpers/tests/apiFilterHelpers.test.ts +85 -4
- package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +10 -4
- package/src/helpers/tests/reloadURLHelpers.test.ts +11 -5
- package/src/helpers/tests/shouldLoadAllFilters.test.ts +117 -0
- package/src/scss/editor-panel.scss +0 -3
- package/src/scss/grid.scss +22 -23
- package/src/scss/main.scss +0 -27
- package/src/store/dashboard.reducer.ts +9 -2
- package/src/store/errorMessage/errorMessage.actions.ts +7 -0
- package/src/store/errorMessage/errorMessage.reducer.ts +24 -0
|
@@ -2,22 +2,31 @@ import React from 'react'
|
|
|
2
2
|
import MultiSelect from '@cdc/core/components/MultiSelect'
|
|
3
3
|
import { SharedFilter } from '../../types/SharedFilter'
|
|
4
4
|
import { APIFilterDropdowns, DropdownOptions } from './DashboardFiltersWrapper'
|
|
5
|
-
import NestedDropdown from '../../../../core/components/NestedDropdown/NestedDropdown'
|
|
6
5
|
import { FILTER_STYLE } from '../../types/FilterStyles'
|
|
7
6
|
import { NestedOptions, ValueTextPair } from '@cdc/core/components/NestedDropdown/nestedDropdownHelpers'
|
|
7
|
+
import NestedDropdown from '@cdc/core/components/NestedDropdown'
|
|
8
|
+
import { MouseEventHandler } from 'react'
|
|
9
|
+
import Loader from '@cdc/core/components/Loader'
|
|
10
|
+
import _ from 'lodash'
|
|
8
11
|
|
|
9
12
|
type DashboardFilterProps = {
|
|
10
13
|
show: number[]
|
|
11
14
|
filters: SharedFilter[]
|
|
12
15
|
apiFilterDropdowns: APIFilterDropdowns
|
|
13
16
|
handleOnChange: (index: number, value: string | string[]) => void
|
|
17
|
+
showSubmit: boolean
|
|
18
|
+
applyFilters: MouseEventHandler<HTMLButtonElement>
|
|
19
|
+
applyFiltersButtonText?: string
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
const DashboardFilters: React.FC<DashboardFilterProps> = ({
|
|
17
23
|
show,
|
|
18
24
|
filters: sharedFilters,
|
|
19
25
|
apiFilterDropdowns,
|
|
20
|
-
handleOnChange
|
|
26
|
+
handleOnChange,
|
|
27
|
+
showSubmit,
|
|
28
|
+
applyFilters,
|
|
29
|
+
applyFiltersButtonText
|
|
21
30
|
}) => {
|
|
22
31
|
const nullVal = (filter: SharedFilter) => {
|
|
23
32
|
const val = filter.queuedActive || filter.active
|
|
@@ -38,29 +47,34 @@ const DashboardFilters: React.FC<DashboardFilterProps> = ({
|
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
return (
|
|
41
|
-
|
|
50
|
+
<form className='d-flex flex-wrap'>
|
|
42
51
|
{sharedFilters.map((filter, filterIndex) => {
|
|
43
52
|
const urlFilterType = filter.type === 'urlfilter'
|
|
53
|
+
const label = filter.key
|
|
44
54
|
|
|
45
55
|
if (
|
|
46
56
|
(!urlFilterType && !filter.showDropdown && filter.filterStyle !== FILTER_STYLE.nestedDropdown) ||
|
|
47
57
|
(show && !show.includes(filterIndex))
|
|
48
58
|
)
|
|
49
|
-
return <React.Fragment key={`${
|
|
59
|
+
return <React.Fragment key={`${label}-filtersection-${filterIndex}-option`} />
|
|
50
60
|
const values: JSX.Element[] = []
|
|
51
61
|
|
|
52
|
-
if (filter.resetLabel) {
|
|
53
|
-
values.push(
|
|
54
|
-
<option key={`${filter.resetLabel}-option`} value={filter.resetLabel}>
|
|
55
|
-
{filter.resetLabel}
|
|
56
|
-
</option>
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
62
|
const _key = filter.apiFilter?.apiEndpoint
|
|
63
|
+
const loading = apiFilterDropdowns[_key] === null
|
|
61
64
|
|
|
62
65
|
const multiValues: { value; label }[] = []
|
|
66
|
+
const nestedOptions: NestedOptions = Object.entries(filter?.subGrouping?.valuesLookup || {}).map(
|
|
67
|
+
([key, data]) => [
|
|
68
|
+
[key, key], // Main option: [value, text]
|
|
69
|
+
Array.isArray(data?.values) ? data.values.map(value => [value, value]) : [] // Ensure `values` is an array
|
|
70
|
+
]
|
|
71
|
+
)
|
|
63
72
|
|
|
73
|
+
const activeSubGroupValue = _.get(
|
|
74
|
+
filter?.subGrouping?.valuesLookup,
|
|
75
|
+
[filter?.active as string, 'values', 0],
|
|
76
|
+
null // Default to null if the path is invalid
|
|
77
|
+
)
|
|
64
78
|
if (_key && apiFilterDropdowns[_key]) {
|
|
65
79
|
// URL Filter
|
|
66
80
|
if (filter.filterStyle !== FILTER_STYLE.nestedDropdown) {
|
|
@@ -77,59 +91,108 @@ const DashboardFilters: React.FC<DashboardFilterProps> = ({
|
|
|
77
91
|
// Data Filter
|
|
78
92
|
filter.values?.forEach((filterOption, index) => {
|
|
79
93
|
const labeledOpt = filter.labels && filter.labels[filterOption]
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
const resetLabelHasMatch = (filterOption || labeledOpt) === filter.resetLabel
|
|
95
|
+
|
|
96
|
+
if (!resetLabelHasMatch) {
|
|
97
|
+
values.push(
|
|
98
|
+
<option key={`${filter.key}-option-${index}`} value={filterOption}>
|
|
99
|
+
{labeledOpt || filterOption}
|
|
100
|
+
</option>
|
|
101
|
+
)
|
|
102
|
+
} else {
|
|
103
|
+
// add label to the front of list if it matches with reset label
|
|
104
|
+
values.unshift(
|
|
105
|
+
<option key={`${filter.key}-option-${index}`} value={filterOption}>
|
|
106
|
+
{labeledOpt || filterOption}
|
|
107
|
+
</option>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
multiValues.push({ value: filterOption, label: labeledOpt || filterOption })
|
|
86
112
|
})
|
|
87
113
|
}
|
|
88
114
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
)}
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
115
|
+
const isDisabled = !values.length
|
|
116
|
+
// push reset label only if it does not includes in filter values options
|
|
117
|
+
if (filter.resetLabel && !filter.values.includes(filter.resetLabel)) {
|
|
118
|
+
values.unshift(
|
|
119
|
+
<option key={`${filter.resetLabel}-option`} value={filter.resetLabel}>
|
|
120
|
+
{filter.resetLabel}
|
|
121
|
+
</option>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const formGroupClass = `form-group mr-3 mb-1${loading ? ' loading-filter' : ''}`
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className={formGroupClass} key={`${label}-filtersection-${filterIndex}`}>
|
|
129
|
+
{label && (
|
|
130
|
+
<label className='font-weight-bold mt-1 mb-0' htmlFor={`filter-${filterIndex}`}>
|
|
131
|
+
{label}
|
|
132
|
+
</label>
|
|
133
|
+
)}
|
|
134
|
+
{filter.filterStyle === FILTER_STYLE.multiSelect ? (
|
|
135
|
+
<MultiSelect
|
|
136
|
+
label={label}
|
|
137
|
+
options={multiValues}
|
|
138
|
+
fieldName={filterIndex}
|
|
139
|
+
updateField={updateField}
|
|
140
|
+
selected={filter.active as string[]}
|
|
141
|
+
limit={filter.selectLimit || 5}
|
|
142
|
+
loading={loading}
|
|
143
|
+
/>
|
|
144
|
+
) : filter.filterStyle === FILTER_STYLE.nestedDropdown ? (
|
|
145
|
+
<NestedDropdown
|
|
146
|
+
activeGroup={filter.active as string}
|
|
147
|
+
activeSubGroup={_key ? filter.subGrouping?.active : activeSubGroupValue}
|
|
148
|
+
filterIndex={filterIndex}
|
|
149
|
+
options={_key ? getNestedDropdownOptions(apiFilterDropdowns[_key]) : nestedOptions}
|
|
150
|
+
listLabel={label}
|
|
151
|
+
handleSelectedItems={value => updateField(null, null, filterIndex, value)}
|
|
152
|
+
loading={loading}
|
|
153
|
+
/>
|
|
154
|
+
) : (
|
|
155
|
+
<>
|
|
156
|
+
<select
|
|
157
|
+
id={`filter-${filterIndex}`}
|
|
158
|
+
className='cove-form-select'
|
|
159
|
+
data-index='0'
|
|
160
|
+
value={loading ? 'Loading...' : filter.queuedActive || filter.active}
|
|
161
|
+
onChange={val => {
|
|
162
|
+
handleOnChange(filterIndex, val.target.value)
|
|
163
|
+
}}
|
|
164
|
+
disabled={loading || isDisabled}
|
|
165
|
+
>
|
|
166
|
+
{loading && <option value='Loading...'>Loading...</option>}
|
|
167
|
+
{nullVal(filter) && (
|
|
168
|
+
<option key={`select`} value=''>
|
|
169
|
+
{filter.resetLabel || '- Select -'}
|
|
170
|
+
</option>
|
|
171
|
+
)}
|
|
172
|
+
{values}
|
|
173
|
+
</select>
|
|
174
|
+
{loading && <Loader spinnerType={'text-secondary'} />}
|
|
175
|
+
</>
|
|
176
|
+
)}
|
|
129
177
|
</div>
|
|
130
178
|
)
|
|
131
179
|
})}
|
|
132
|
-
|
|
180
|
+
{showSubmit && (
|
|
181
|
+
<button
|
|
182
|
+
className='btn btn-primary mb-1'
|
|
183
|
+
onClick={applyFilters}
|
|
184
|
+
disabled={show.some(filterIndex => {
|
|
185
|
+
const emptyFilterValues = [undefined, '', '- Select -']
|
|
186
|
+
return (
|
|
187
|
+
emptyFilterValues.includes(sharedFilters[filterIndex].queuedActive) &&
|
|
188
|
+
emptyFilterValues.includes(sharedFilters[filterIndex].active)
|
|
189
|
+
)
|
|
190
|
+
})}
|
|
191
|
+
>
|
|
192
|
+
{applyFiltersButtonText || 'GO!'}
|
|
193
|
+
</button>
|
|
194
|
+
)}
|
|
195
|
+
</form>
|
|
133
196
|
)
|
|
134
197
|
}
|
|
135
198
|
|
|
@@ -274,7 +274,9 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
|
|
|
274
274
|
</select>
|
|
275
275
|
</label>
|
|
276
276
|
) : (
|
|
277
|
-
|
|
277
|
+
|
|
278
|
+
<button onClick={() => setCanAddExisting(true)} className='btn btn-primary full-width mt-2'>
|
|
279
|
+
|
|
278
280
|
Add Existing Dashboard Filter
|
|
279
281
|
</button>
|
|
280
282
|
)}
|
|
@@ -41,11 +41,10 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
|
|
|
41
41
|
if (viz.type === 'dashboardFilters') return false
|
|
42
42
|
const vizName = viz.general?.title || viz.title || vizKey
|
|
43
43
|
nameLookup[vizKey] = vizName
|
|
44
|
-
const notAdded = !filter.usedBy || filter.usedBy.indexOf(vizKey) === -1
|
|
45
44
|
const usesSharedFilter = viz.usesSharedFilter
|
|
46
45
|
const rowIndex = vizLookup.row
|
|
47
46
|
const dataConfiguredOnRow = config.rows[rowIndex].dataKey
|
|
48
|
-
return filter.setBy !== vizKey &&
|
|
47
|
+
return filter.setBy !== vizKey && !usesSharedFilter && !dataConfiguredOnRow
|
|
49
48
|
})
|
|
50
49
|
const rowOptions: number[] = []
|
|
51
50
|
|
|
@@ -363,20 +362,23 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
|
|
|
363
362
|
{isNestedDropDown && <APIInputs isSubgroup={true} />}
|
|
364
363
|
|
|
365
364
|
{!!parentFilters.length && (
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
365
|
+
<label>
|
|
366
|
+
<span className='edit-label column-heading mt-1'>Parent Filter(s): </span>
|
|
367
|
+
<MultiSelect
|
|
368
|
+
label='Parent Filter(s): '
|
|
369
|
+
options={parentFilters.map(key => ({ value: key, label: key }))}
|
|
370
|
+
fieldName='parents'
|
|
371
|
+
selected={filter.parents}
|
|
372
|
+
updateField={(_section, _subsection, _fieldname, newItems) => {
|
|
373
|
+
updateFilterProp('parents', newItems)
|
|
374
|
+
}}
|
|
375
|
+
/>
|
|
376
|
+
</label>
|
|
375
377
|
)}
|
|
376
378
|
|
|
377
|
-
<
|
|
378
|
-
|
|
379
|
-
|
|
379
|
+
<label>
|
|
380
|
+
<span className='edit-label column-heading mt-1'>
|
|
381
|
+
Used By: (optional)
|
|
380
382
|
<Tooltip style={{ textTransform: 'none' }}>
|
|
381
383
|
<Tooltip.Target>
|
|
382
384
|
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
@@ -388,17 +390,19 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
|
|
|
388
390
|
</p>
|
|
389
391
|
</Tooltip.Content>
|
|
390
392
|
</Tooltip>
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
393
|
+
</span>
|
|
394
|
+
<MultiSelect
|
|
395
|
+
options={usedByOptions.map(opt => ({
|
|
396
|
+
value: opt,
|
|
397
|
+
label: usedByNameLookup[opt]
|
|
398
|
+
}))}
|
|
399
|
+
fieldName='usedBy'
|
|
400
|
+
selected={filter.usedBy}
|
|
401
|
+
updateField={(_section, _subsection, _fieldname, newItems) => {
|
|
402
|
+
updateFilterProp('usedBy', newItems)
|
|
403
|
+
}}
|
|
404
|
+
/>
|
|
405
|
+
</label>
|
|
402
406
|
|
|
403
407
|
<TextField
|
|
404
408
|
label='Reset Label: '
|
|
@@ -473,31 +477,31 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
|
|
|
473
477
|
</select>
|
|
474
478
|
</label>
|
|
475
479
|
<label>
|
|
476
|
-
<span className='edit-label column-heading'>
|
|
477
|
-
|
|
478
|
-
{
|
|
479
|
-
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
480
|
+
<span className='edit-label column-heading mt-1'>
|
|
481
|
+
Used By: (optional)
|
|
482
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
483
|
+
<Tooltip.Target>
|
|
484
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
485
|
+
</Tooltip.Target>
|
|
486
|
+
<Tooltip.Content>
|
|
487
|
+
<p>
|
|
488
|
+
Select if you would like specific visualizations or rows to use this filter. Otherwise the
|
|
489
|
+
filter will be added to all api requests.
|
|
490
|
+
</p>
|
|
491
|
+
</Tooltip.Content>
|
|
492
|
+
</Tooltip>
|
|
493
|
+
</span>
|
|
494
|
+
<MultiSelect
|
|
495
|
+
options={usedByOptions.map(opt => ({
|
|
496
|
+
value: opt,
|
|
497
|
+
label: usedByNameLookup[opt]
|
|
498
|
+
}))}
|
|
499
|
+
fieldName='usedBy'
|
|
500
|
+
selected={filter.usedBy}
|
|
501
|
+
updateField={(_section, _subsection, _fieldname, newItems) => {
|
|
502
|
+
updateFilterProp('usedBy', newItems)
|
|
503
|
+
}}
|
|
504
|
+
/>
|
|
501
505
|
</label>
|
|
502
506
|
<TextField
|
|
503
507
|
label='Reset Label: '
|
|
@@ -13,6 +13,7 @@ import { ViewPort } from '@cdc/core/types/ViewPort'
|
|
|
13
13
|
import { hasDashboardApplyBehavior } from '../../helpers/hasDashboardApplyBehavior'
|
|
14
14
|
import * as apiFilterHelpers from '../../helpers/apiFilterHelpers'
|
|
15
15
|
import { applyQueuedActive } from '@cdc/core/components/Filters/helpers/applyQueuedActive'
|
|
16
|
+
import './dashboardfilter.styles.css'
|
|
16
17
|
|
|
17
18
|
type SubOptions = { subOptions?: Record<'value' | 'text', string>[] }
|
|
18
19
|
|
|
@@ -30,6 +31,7 @@ type DashboardFiltersProps = {
|
|
|
30
31
|
isEditor?: boolean
|
|
31
32
|
setConfig: (config: DashboardFilters) => void
|
|
32
33
|
currentViewport?: ViewPort
|
|
34
|
+
setAPILoading: (loading: boolean) => void
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
@@ -40,10 +42,11 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
40
42
|
isEditor = false
|
|
41
43
|
}) => {
|
|
42
44
|
const state = useContext(DashboardContext)
|
|
43
|
-
const { config: dashboardConfig, reloadURLData, loadAPIFilters, setAPIFilterDropdowns } = state
|
|
45
|
+
const { config: dashboardConfig, reloadURLData, loadAPIFilters, setAPIFilterDropdowns, setAPILoading } = state
|
|
44
46
|
const dispatch = useContext(DashboardDispatchContext)
|
|
45
47
|
|
|
46
|
-
const applyFilters =
|
|
48
|
+
const applyFilters = e => {
|
|
49
|
+
e.preventDefault() // prevent form submission
|
|
47
50
|
const dashboardConfig = _.cloneDeep(state.config.dashboard)
|
|
48
51
|
const nonAutoLoadFilterIndexes = Object.values(state.config.visualizations)
|
|
49
52
|
.filter(v => v.type === 'dashboardFilters')
|
|
@@ -79,7 +82,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
79
82
|
updateQueryString(queryParams)
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
|
-
|
|
85
|
+
setAPILoading(true)
|
|
83
86
|
dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
|
|
84
87
|
dispatch({ type: 'SET_FILTERED_DATA', payload: getFilteredData(_.cloneDeep(state)) })
|
|
85
88
|
loadAPIFilters(dashboardConfig.sharedFilters, apiFilterDropdowns)
|
|
@@ -103,6 +106,9 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
103
106
|
visualizationConfig
|
|
104
107
|
)
|
|
105
108
|
|
|
109
|
+
// sets the active filter option that the user just selected.
|
|
110
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
|
|
111
|
+
|
|
106
112
|
if (hasDashboardApplyBehavior(dashboardConfig.visualizations)) {
|
|
107
113
|
const isAutoSelectFilter = visualizationConfig.autoLoad
|
|
108
114
|
const missingFilterSelections = newConfig.dashboard.sharedFilters.some(f => !f.active)
|
|
@@ -171,7 +177,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
171
177
|
{!displayNone && (
|
|
172
178
|
<Layout.Responsive isEditor={isEditor}>
|
|
173
179
|
<div
|
|
174
|
-
className={
|
|
180
|
+
className={`${
|
|
175
181
|
isEditor ? ' is-editor' : ''
|
|
176
182
|
} cove-component__content col-12 cove-dashboard-filters-container`}
|
|
177
183
|
>
|
|
@@ -180,10 +186,10 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
180
186
|
filters={dashboardConfig.dashboard.sharedFilters || []}
|
|
181
187
|
apiFilterDropdowns={apiFilterDropdowns}
|
|
182
188
|
handleOnChange={handleOnChange}
|
|
189
|
+
showSubmit={visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad}
|
|
190
|
+
applyFilters={applyFilters}
|
|
191
|
+
applyFiltersButtonText={visualizationConfig.applyFiltersButtonText}
|
|
183
192
|
/>
|
|
184
|
-
{visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad && (
|
|
185
|
-
<button onClick={applyFilters}>{visualizationConfig.applyFiltersButtonText || 'GO!'}</button>
|
|
186
|
-
)}
|
|
187
193
|
</div>
|
|
188
194
|
</Layout.Responsive>
|
|
189
195
|
)}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import DashboardFilters from '../DashboardFilters'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof DashboardFilters> = {
|
|
5
|
+
title: 'Components/Atoms/Inputs/DashboardFilters',
|
|
6
|
+
component: DashboardFilters
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type Story = StoryObj<typeof DashboardFilters>
|
|
10
|
+
|
|
11
|
+
export const Example_1: Story = {
|
|
12
|
+
args: {
|
|
13
|
+
filters: [
|
|
14
|
+
{ type: 'datafilter', key: 'label here', values: [1, 2, 3, 4] },
|
|
15
|
+
{ type: 'datafilter', key: 'something' }
|
|
16
|
+
],
|
|
17
|
+
handleOnChange: () => {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.cove-dashboard-filters-container {
|
|
2
|
+
:is(label) {
|
|
3
|
+
margin-bottom: 0;
|
|
4
|
+
margin-top: 0.5rem;
|
|
5
|
+
}
|
|
6
|
+
.btn {
|
|
7
|
+
/* this is the height that is defined for the .form-control class in _forms.scss in bootstrap. */
|
|
8
|
+
height: calc(1.5em + 0.75rem + 2px);
|
|
9
|
+
align-self: flex-end;
|
|
10
|
+
}
|
|
11
|
+
.loading-filter {
|
|
12
|
+
position: relative;
|
|
13
|
+
.spinner-border {
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: 55%;
|
|
16
|
+
right: 10%;
|
|
17
|
+
width: 1.5rem;
|
|
18
|
+
height: 1.5rem;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
:is(select):disabled {
|
|
22
|
+
background-color: var(--lightestGray);
|
|
23
|
+
& > :is(option) {
|
|
24
|
+
color: var(--darkGray);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/components/Grid.tsx
CHANGED
|
@@ -24,7 +24,7 @@ const Grid = () => {
|
|
|
24
24
|
{(rows || []).map((row, idx) => (
|
|
25
25
|
<Row row={row} idx={idx} uuid={row.uuid} key={idx} />
|
|
26
26
|
))}
|
|
27
|
-
<button className='btn
|
|
27
|
+
<button className='btn btn-primary col' onClick={addRow}>
|
|
28
28
|
Add Row
|
|
29
29
|
</button>
|
|
30
30
|
</div>
|
|
@@ -6,6 +6,7 @@ import './index.scss'
|
|
|
6
6
|
import MultiConfigTabs from '../MultiConfigTabs'
|
|
7
7
|
import { Tab } from '../../types/Tab'
|
|
8
8
|
import _ from 'lodash'
|
|
9
|
+
import { getVizRowColumnLocator } from '../../helpers/getVizRowColumnLocator'
|
|
9
10
|
|
|
10
11
|
type HeaderProps = {
|
|
11
12
|
back?: any
|
|
@@ -16,7 +17,7 @@ type HeaderProps = {
|
|
|
16
17
|
const Header = (props: HeaderProps) => {
|
|
17
18
|
const tabs: Tab[] = ['Dashboard Description', 'Data Table Settings', 'Dashboard Preview']
|
|
18
19
|
const { visualizationKey, subEditor } = props
|
|
19
|
-
const { config, setParentConfig, tabSelected } = useContext(DashboardContext)
|
|
20
|
+
const { config, setParentConfig, tabSelected, data } = useContext(DashboardContext)
|
|
20
21
|
if (!config) return null
|
|
21
22
|
const dispatch = useContext(DashboardDispatchContext)
|
|
22
23
|
const back = () => {
|
|
@@ -24,6 +25,22 @@ const Header = (props: HeaderProps) => {
|
|
|
24
25
|
const newConfig = _.cloneDeep(config)
|
|
25
26
|
newConfig.visualizations[visualizationKey].editing = false
|
|
26
27
|
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
28
|
+
|
|
29
|
+
// the Widget component will do a data fetch if no data is available for the visualization
|
|
30
|
+
// this is intended to help visualization developers.
|
|
31
|
+
type SampleData = Record<string, { sample: boolean }> & Object[]
|
|
32
|
+
if (Object.values(data).some((d: SampleData) => d.sample)) {
|
|
33
|
+
const sampleDataRemoved = Object.keys(data).reduce((acc, key) => {
|
|
34
|
+
if ((data[key] as SampleData).sample) {
|
|
35
|
+
acc[key] = []
|
|
36
|
+
} else {
|
|
37
|
+
acc[key] = data[key]
|
|
38
|
+
}
|
|
39
|
+
return acc
|
|
40
|
+
}, {})
|
|
41
|
+
|
|
42
|
+
dispatch({ type: 'SET_DATA', payload: sampleDataRemoved })
|
|
43
|
+
}
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
const changeConfigValue = (parentObj, key, value) => {
|
|
@@ -81,10 +98,18 @@ const Header = (props: HeaderProps) => {
|
|
|
81
98
|
<div className='heading-1'>
|
|
82
99
|
Dashboard Editor{' '}
|
|
83
100
|
<span className='small'>
|
|
84
|
-
<input type='checkbox' onChange={handleCheck} checked={multiInitialized} disabled={multiInitialized} /> make
|
|
101
|
+
<input type='checkbox' onChange={handleCheck} checked={multiInitialized} disabled={multiInitialized} /> make
|
|
102
|
+
multidashboard
|
|
85
103
|
</span>
|
|
86
104
|
<br />
|
|
87
|
-
{
|
|
105
|
+
{
|
|
106
|
+
<input
|
|
107
|
+
type='text'
|
|
108
|
+
placeholder='Enter Dashboard Name Here'
|
|
109
|
+
defaultValue={config.dashboard?.title}
|
|
110
|
+
onChange={e => changeConfigValue('dashboard', 'title', e.target.value)}
|
|
111
|
+
/>
|
|
112
|
+
}
|
|
88
113
|
</div>
|
|
89
114
|
)}
|
|
90
115
|
{!subEditor && (
|
|
@@ -106,18 +131,34 @@ const Header = (props: HeaderProps) => {
|
|
|
106
131
|
})}
|
|
107
132
|
</ul>
|
|
108
133
|
<div className='heading-body'>
|
|
109
|
-
{tabSelected === 'Dashboard Description' &&
|
|
134
|
+
{tabSelected === 'Dashboard Description' && (
|
|
135
|
+
<input
|
|
136
|
+
type='text'
|
|
137
|
+
className='description-input'
|
|
138
|
+
placeholder='Type a dashboard description here.'
|
|
139
|
+
defaultValue={config.dashboard?.description}
|
|
140
|
+
onChange={e => changeConfigValue('dashboard', 'description', e.target.value)}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
110
143
|
{tabSelected === 'Data Table Settings' && (
|
|
111
144
|
<>
|
|
112
145
|
<div className='wrap'>
|
|
113
146
|
<label>
|
|
114
|
-
<input
|
|
147
|
+
<input
|
|
148
|
+
type='checkbox'
|
|
149
|
+
defaultChecked={config.table.show}
|
|
150
|
+
onChange={e => changeConfigValue('table', 'show', e.target.checked)}
|
|
151
|
+
/>
|
|
115
152
|
Show Data Table(s)
|
|
116
153
|
</label>
|
|
117
154
|
<br />
|
|
118
155
|
|
|
119
156
|
<label>
|
|
120
|
-
<input
|
|
157
|
+
<input
|
|
158
|
+
type='checkbox'
|
|
159
|
+
defaultChecked={config.table.expanded}
|
|
160
|
+
onChange={e => changeConfigValue('table', 'expanded', e.target.checked)}
|
|
161
|
+
/>
|
|
121
162
|
Expanded by Default
|
|
122
163
|
</label>
|
|
123
164
|
<br />
|
|
@@ -125,19 +166,39 @@ const Header = (props: HeaderProps) => {
|
|
|
125
166
|
|
|
126
167
|
<div className='wrap'>
|
|
127
168
|
<label>
|
|
128
|
-
<input
|
|
169
|
+
<input
|
|
170
|
+
type='checkbox'
|
|
171
|
+
defaultChecked={config.table.limitHeight}
|
|
172
|
+
onChange={e => changeConfigValue('table', 'limitHeight', e.target.checked)}
|
|
173
|
+
/>
|
|
129
174
|
Limit Table Height
|
|
130
175
|
</label>
|
|
131
|
-
{config.table.limitHeight &&
|
|
176
|
+
{config.table.limitHeight && (
|
|
177
|
+
<input
|
|
178
|
+
className='table-height-input'
|
|
179
|
+
type='text'
|
|
180
|
+
placeholder='Height (px)'
|
|
181
|
+
defaultValue={config.table.height}
|
|
182
|
+
onChange={e => changeConfigValue('table', 'height', e.target.value)}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
132
185
|
</div>
|
|
133
186
|
|
|
134
187
|
<div className='wrap'>
|
|
135
188
|
<label>
|
|
136
|
-
<input
|
|
189
|
+
<input
|
|
190
|
+
type='checkbox'
|
|
191
|
+
defaultChecked={config.table.download}
|
|
192
|
+
onChange={e => changeConfigValue('table', 'download', e.target.checked)}
|
|
193
|
+
/>
|
|
137
194
|
Show Download CSV Link
|
|
138
195
|
</label>
|
|
139
196
|
<label>
|
|
140
|
-
<input
|
|
197
|
+
<input
|
|
198
|
+
type='checkbox'
|
|
199
|
+
defaultChecked={config.table.showDownloadUrl}
|
|
200
|
+
onChange={e => changeConfigValue('table', 'showDownloadUrl', e.target.checked)}
|
|
201
|
+
/>
|
|
141
202
|
Show URL to Automatically Updated Data
|
|
142
203
|
</label>
|
|
143
204
|
</div>
|