@cdc/dashboard 4.25.5-1 → 4.25.6
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/LICENSE +201 -0
- package/dist/cdcdashboard.js +80055 -78658
- package/examples/m2.json +32904 -0
- package/examples/special-classes.json +54340 -0
- package/package.json +9 -9
- package/src/CdcDashboardComponent.tsx +36 -214
- package/src/_stories/_mock/api-filter-map.json +1 -0
- package/src/components/CollapsibleVisualizationRow.tsx +2 -4
- package/src/components/DashboardEditors.tsx +143 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +205 -205
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +198 -198
- package/src/components/Header/Header.tsx +7 -9
- package/src/components/Row.tsx +1 -24
- package/src/components/VisualizationRow.tsx +190 -213
- package/src/helpers/getVizConfig.ts +108 -80
- package/src/helpers/getVizRowColumnLocator.ts +0 -1
- package/src/helpers/reloadURLHelpers.ts +10 -13
- package/src/store/dashboard.actions.ts +0 -2
- package/src/store/dashboard.reducer.ts +0 -11
- package/src/types/ConfigRow.ts +0 -1
- package/src/types/Dashboard.ts +1 -1
- package/src/types/DashboardConfig.ts +1 -1
- package/src/types/DataSet.ts +0 -12
|
@@ -1,205 +1,205 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import MultiSelect from '@cdc/core/components/MultiSelect'
|
|
3
|
-
import { SharedFilter } from '../../types/SharedFilter'
|
|
4
|
-
import { APIFilterDropdowns, DropdownOptions } from './DashboardFiltersWrapper'
|
|
5
|
-
import { FILTER_STYLE } from '../../types/FilterStyles'
|
|
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'
|
|
11
|
-
import { DROPDOWN_STYLES } from '@cdc/core/components/Filters/components/Dropdown'
|
|
12
|
-
|
|
13
|
-
type DashboardFilterProps = {
|
|
14
|
-
show: number[]
|
|
15
|
-
filters: SharedFilter[]
|
|
16
|
-
apiFilterDropdowns: APIFilterDropdowns
|
|
17
|
-
handleOnChange: (index: number, value: string | string[]) => void
|
|
18
|
-
showSubmit: boolean
|
|
19
|
-
applyFilters: MouseEventHandler<HTMLButtonElement>
|
|
20
|
-
applyFiltersButtonText?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const DashboardFilters: React.FC<DashboardFilterProps> = ({
|
|
24
|
-
show,
|
|
25
|
-
filters: sharedFilters,
|
|
26
|
-
apiFilterDropdowns,
|
|
27
|
-
handleOnChange,
|
|
28
|
-
showSubmit,
|
|
29
|
-
applyFilters,
|
|
30
|
-
applyFiltersButtonText
|
|
31
|
-
}) => {
|
|
32
|
-
const nullVal = (filter: SharedFilter) => {
|
|
33
|
-
const val = filter.queuedActive || filter.active
|
|
34
|
-
return val === null || val === undefined || val === ''
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const updateField = (_section, _subsection, fieldName, value) => {
|
|
38
|
-
handleOnChange(fieldName, value) // fieldName is the sharedFilterIndex
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const stripDuplicateLabelIncrement = (label: string): string => {
|
|
42
|
-
// converts 'Label (1)' to 'Label'
|
|
43
|
-
return label.replace(/\s\(\d+\)$/, '')
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const getNestedDropdownOptions = (options?: DropdownOptions): NestedOptions => {
|
|
47
|
-
if (!options) return []
|
|
48
|
-
const getValueTextTuple = (value: string, text?: string): ValueTextPair => (text ? [value, text] : [value])
|
|
49
|
-
return options.map(({ value, text, subOptions }) => [
|
|
50
|
-
getValueTextTuple(value, text),
|
|
51
|
-
(subOptions || []).map(({ value, text }) => getValueTextTuple(value, text))
|
|
52
|
-
])
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<form className='d-flex flex-wrap'>
|
|
57
|
-
{sharedFilters.map((filter, filterIndex) => {
|
|
58
|
-
const urlFilterType = filter.type === 'urlfilter'
|
|
59
|
-
const label = stripDuplicateLabelIncrement(filter.key || '')
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
(!urlFilterType && !filter.showDropdown && filter.filterStyle !== FILTER_STYLE.nestedDropdown) ||
|
|
63
|
-
(show && !show.includes(filterIndex))
|
|
64
|
-
)
|
|
65
|
-
return <React.Fragment key={`${filter.key}-filtersection-${filterIndex}-option`} />
|
|
66
|
-
const values: JSX.Element[] = []
|
|
67
|
-
|
|
68
|
-
const _key = filter.apiFilter?.apiEndpoint
|
|
69
|
-
const loading = apiFilterDropdowns[_key] === null
|
|
70
|
-
|
|
71
|
-
const multiValues: { value; label }[] = []
|
|
72
|
-
const nestedOptions: NestedOptions = Object.entries(filter?.subGrouping?.valuesLookup || {}).map(
|
|
73
|
-
([key, data]) => [
|
|
74
|
-
[key, key], // Main option: [value, text]
|
|
75
|
-
Array.isArray(data?.values) ? data.values.map(value => [value, value]) : [] // Ensure `values` is an array
|
|
76
|
-
]
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
const activeSubGroupValue = _.get(
|
|
80
|
-
filter?.subGrouping?.valuesLookup,
|
|
81
|
-
[filter?.active as string, 'values', 0],
|
|
82
|
-
null // Default to null if the path is invalid
|
|
83
|
-
)
|
|
84
|
-
if (_key && apiFilterDropdowns[_key]) {
|
|
85
|
-
// URL Filter
|
|
86
|
-
if (filter.filterStyle !== FILTER_STYLE.nestedDropdown) {
|
|
87
|
-
apiFilterDropdowns[_key].forEach(({ text, value }, index) => {
|
|
88
|
-
values.push(
|
|
89
|
-
<option key={`${value}-option-${index}`} value={value}>
|
|
90
|
-
{text}
|
|
91
|
-
</option>
|
|
92
|
-
)
|
|
93
|
-
multiValues.push({ value, label: text })
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
// Data Filter
|
|
98
|
-
const orderedFilterValues = filter.orderedValues || filter.values
|
|
99
|
-
orderedFilterValues?.forEach((filterOption, index) => {
|
|
100
|
-
const labeledOpt = filter.labels && filter.labels[filterOption]
|
|
101
|
-
const resetLabelHasMatch = (filterOption || labeledOpt) === filter.resetLabel
|
|
102
|
-
|
|
103
|
-
if (!resetLabelHasMatch) {
|
|
104
|
-
values.push(
|
|
105
|
-
<option key={`${filter.key}-option-${index}`} value={filterOption}>
|
|
106
|
-
{labeledOpt || filterOption}
|
|
107
|
-
</option>
|
|
108
|
-
)
|
|
109
|
-
} else {
|
|
110
|
-
// add label to the front of list if it matches with reset label
|
|
111
|
-
values.unshift(
|
|
112
|
-
<option key={`${filter.key}-option-${index}`} value={filterOption}>
|
|
113
|
-
{labeledOpt || filterOption}
|
|
114
|
-
</option>
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
multiValues.push({ value: filterOption, label: labeledOpt || filterOption })
|
|
119
|
-
})
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const isDisabled = !values.length
|
|
123
|
-
// push reset label only if it does not includes in filter values options
|
|
124
|
-
if (filter.resetLabel && !filter.values.includes(filter.resetLabel)) {
|
|
125
|
-
values.unshift(
|
|
126
|
-
<option key={`${filter.resetLabel}-option`} value={filter.resetLabel}>
|
|
127
|
-
{filter.resetLabel}
|
|
128
|
-
</option>
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const formGroupClass = `form-group me-4 mb-1${loading ? ' loading-filter' : ''}`
|
|
133
|
-
return (
|
|
134
|
-
<div className={formGroupClass} key={`${filter.key}-filtersection-${filterIndex}`}>
|
|
135
|
-
{label && (
|
|
136
|
-
<label className='font-weight-bold mb-2' htmlFor={`filter-${filterIndex}`}>
|
|
137
|
-
{label}
|
|
138
|
-
</label>
|
|
139
|
-
)}
|
|
140
|
-
{filter.filterStyle === FILTER_STYLE.multiSelect ? (
|
|
141
|
-
<MultiSelect
|
|
142
|
-
label={label}
|
|
143
|
-
options={multiValues}
|
|
144
|
-
fieldName={filterIndex}
|
|
145
|
-
updateField={updateField}
|
|
146
|
-
selected={filter.active as string[]}
|
|
147
|
-
limit={filter.selectLimit || 5}
|
|
148
|
-
loading={loading}
|
|
149
|
-
/>
|
|
150
|
-
) : filter.filterStyle === FILTER_STYLE.nestedDropdown ? (
|
|
151
|
-
<NestedDropdown
|
|
152
|
-
activeGroup={(filter.queuedActive?.[0] || filter.active) as string}
|
|
153
|
-
activeSubGroup={_key ? filter.queuedActive?.[1] || filter.subGrouping?.active : activeSubGroupValue}
|
|
154
|
-
filterIndex={filterIndex}
|
|
155
|
-
options={_key ? getNestedDropdownOptions(apiFilterDropdowns[_key]) : nestedOptions}
|
|
156
|
-
listLabel={label}
|
|
157
|
-
handleSelectedItems={value => updateField(null, null, filterIndex, value)}
|
|
158
|
-
loading={loading}
|
|
159
|
-
/>
|
|
160
|
-
) : (
|
|
161
|
-
<>
|
|
162
|
-
<select
|
|
163
|
-
id={`filter-${filterIndex}`}
|
|
164
|
-
className={`cove-form-select ${DROPDOWN_STYLES}`}
|
|
165
|
-
data-index='0'
|
|
166
|
-
value={loading ? 'Loading...' : filter.queuedActive || filter.active}
|
|
167
|
-
onChange={val => {
|
|
168
|
-
handleOnChange(filterIndex, val.target.value)
|
|
169
|
-
}}
|
|
170
|
-
disabled={loading || isDisabled}
|
|
171
|
-
>
|
|
172
|
-
{loading && <option value='Loading...'>Loading...</option>}
|
|
173
|
-
{nullVal(filter) && (
|
|
174
|
-
<option key={`select`} value=''>
|
|
175
|
-
{filter.resetLabel || '- Select -'}
|
|
176
|
-
</option>
|
|
177
|
-
)}
|
|
178
|
-
{values}
|
|
179
|
-
</select>
|
|
180
|
-
{loading && <Loader spinnerType={'text-secondary'} />}
|
|
181
|
-
</>
|
|
182
|
-
)}
|
|
183
|
-
</div>
|
|
184
|
-
)
|
|
185
|
-
})}
|
|
186
|
-
{showSubmit && (
|
|
187
|
-
<button
|
|
188
|
-
className='btn btn-primary mb-1'
|
|
189
|
-
onClick={applyFilters}
|
|
190
|
-
disabled={show.some(filterIndex => {
|
|
191
|
-
const emptyFilterValues = [undefined, '', '- Select -']
|
|
192
|
-
return (
|
|
193
|
-
emptyFilterValues.includes(sharedFilters[filterIndex].queuedActive) &&
|
|
194
|
-
emptyFilterValues.includes(sharedFilters[filterIndex].active)
|
|
195
|
-
)
|
|
196
|
-
})}
|
|
197
|
-
>
|
|
198
|
-
{applyFiltersButtonText || 'GO!'}
|
|
199
|
-
</button>
|
|
200
|
-
)}
|
|
201
|
-
</form>
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export default DashboardFilters
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import MultiSelect from '@cdc/core/components/MultiSelect'
|
|
3
|
+
import { SharedFilter } from '../../types/SharedFilter'
|
|
4
|
+
import { APIFilterDropdowns, DropdownOptions } from './DashboardFiltersWrapper'
|
|
5
|
+
import { FILTER_STYLE } from '../../types/FilterStyles'
|
|
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'
|
|
11
|
+
import { DROPDOWN_STYLES } from '@cdc/core/components/Filters/components/Dropdown'
|
|
12
|
+
|
|
13
|
+
type DashboardFilterProps = {
|
|
14
|
+
show: number[]
|
|
15
|
+
filters: SharedFilter[]
|
|
16
|
+
apiFilterDropdowns: APIFilterDropdowns
|
|
17
|
+
handleOnChange: (index: number, value: string | string[]) => void
|
|
18
|
+
showSubmit: boolean
|
|
19
|
+
applyFilters: MouseEventHandler<HTMLButtonElement>
|
|
20
|
+
applyFiltersButtonText?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DashboardFilters: React.FC<DashboardFilterProps> = ({
|
|
24
|
+
show,
|
|
25
|
+
filters: sharedFilters,
|
|
26
|
+
apiFilterDropdowns,
|
|
27
|
+
handleOnChange,
|
|
28
|
+
showSubmit,
|
|
29
|
+
applyFilters,
|
|
30
|
+
applyFiltersButtonText
|
|
31
|
+
}) => {
|
|
32
|
+
const nullVal = (filter: SharedFilter) => {
|
|
33
|
+
const val = filter.queuedActive || filter.active
|
|
34
|
+
return val === null || val === undefined || val === ''
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const updateField = (_section, _subsection, fieldName, value) => {
|
|
38
|
+
handleOnChange(fieldName, value) // fieldName is the sharedFilterIndex
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const stripDuplicateLabelIncrement = (label: string): string => {
|
|
42
|
+
// converts 'Label (1)' to 'Label'
|
|
43
|
+
return label.replace(/\s\(\d+\)$/, '')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const getNestedDropdownOptions = (options?: DropdownOptions): NestedOptions => {
|
|
47
|
+
if (!options) return []
|
|
48
|
+
const getValueTextTuple = (value: string, text?: string): ValueTextPair => (text ? [value, text] : [value])
|
|
49
|
+
return options.map(({ value, text, subOptions }) => [
|
|
50
|
+
getValueTextTuple(value, text),
|
|
51
|
+
(subOptions || []).map(({ value, text }) => getValueTextTuple(value, text))
|
|
52
|
+
])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<form className='d-flex flex-wrap'>
|
|
57
|
+
{sharedFilters.map((filter, filterIndex) => {
|
|
58
|
+
const urlFilterType = filter.type === 'urlfilter'
|
|
59
|
+
const label = stripDuplicateLabelIncrement(filter.key || '')
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
(!urlFilterType && !filter.showDropdown && filter.filterStyle !== FILTER_STYLE.nestedDropdown) ||
|
|
63
|
+
(show && !show.includes(filterIndex))
|
|
64
|
+
)
|
|
65
|
+
return <React.Fragment key={`${filter.key}-filtersection-${filterIndex}-option`} />
|
|
66
|
+
const values: JSX.Element[] = []
|
|
67
|
+
|
|
68
|
+
const _key = filter.apiFilter?.apiEndpoint
|
|
69
|
+
const loading = apiFilterDropdowns[_key] === null
|
|
70
|
+
|
|
71
|
+
const multiValues: { value; label }[] = []
|
|
72
|
+
const nestedOptions: NestedOptions = Object.entries(filter?.subGrouping?.valuesLookup || {}).map(
|
|
73
|
+
([key, data]) => [
|
|
74
|
+
[key, key], // Main option: [value, text]
|
|
75
|
+
Array.isArray(data?.values) ? data.values.map(value => [value, value]) : [] // Ensure `values` is an array
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const activeSubGroupValue = _.get(
|
|
80
|
+
filter?.subGrouping?.valuesLookup,
|
|
81
|
+
[filter?.active as string, 'values', 0],
|
|
82
|
+
null // Default to null if the path is invalid
|
|
83
|
+
)
|
|
84
|
+
if (_key && apiFilterDropdowns[_key]) {
|
|
85
|
+
// URL Filter
|
|
86
|
+
if (filter.filterStyle !== FILTER_STYLE.nestedDropdown) {
|
|
87
|
+
apiFilterDropdowns[_key].forEach(({ text, value }, index) => {
|
|
88
|
+
values.push(
|
|
89
|
+
<option key={`${value}-option-${index}`} value={value}>
|
|
90
|
+
{text}
|
|
91
|
+
</option>
|
|
92
|
+
)
|
|
93
|
+
multiValues.push({ value, label: text })
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// Data Filter
|
|
98
|
+
const orderedFilterValues = filter.orderedValues || filter.values
|
|
99
|
+
orderedFilterValues?.forEach((filterOption, index) => {
|
|
100
|
+
const labeledOpt = filter.labels && filter.labels[filterOption]
|
|
101
|
+
const resetLabelHasMatch = (filterOption || labeledOpt) === filter.resetLabel
|
|
102
|
+
|
|
103
|
+
if (!resetLabelHasMatch) {
|
|
104
|
+
values.push(
|
|
105
|
+
<option key={`${filter.key}-option-${index}`} value={filterOption}>
|
|
106
|
+
{labeledOpt || filterOption}
|
|
107
|
+
</option>
|
|
108
|
+
)
|
|
109
|
+
} else {
|
|
110
|
+
// add label to the front of list if it matches with reset label
|
|
111
|
+
values.unshift(
|
|
112
|
+
<option key={`${filter.key}-option-${index}`} value={filterOption}>
|
|
113
|
+
{labeledOpt || filterOption}
|
|
114
|
+
</option>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
multiValues.push({ value: filterOption, label: labeledOpt || filterOption })
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const isDisabled = !values.length
|
|
123
|
+
// push reset label only if it does not includes in filter values options
|
|
124
|
+
if (filter.resetLabel && !filter.values.includes(filter.resetLabel)) {
|
|
125
|
+
values.unshift(
|
|
126
|
+
<option key={`${filter.resetLabel}-option`} value={filter.resetLabel}>
|
|
127
|
+
{filter.resetLabel}
|
|
128
|
+
</option>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const formGroupClass = `form-group me-4 mb-1${loading ? ' loading-filter' : ''}`
|
|
133
|
+
return (
|
|
134
|
+
<div className={formGroupClass} key={`${filter.key}-filtersection-${filterIndex}`}>
|
|
135
|
+
{label && (
|
|
136
|
+
<label className='font-weight-bold mb-2' htmlFor={`filter-${filterIndex}`}>
|
|
137
|
+
{label}
|
|
138
|
+
</label>
|
|
139
|
+
)}
|
|
140
|
+
{filter.filterStyle === FILTER_STYLE.multiSelect ? (
|
|
141
|
+
<MultiSelect
|
|
142
|
+
label={label}
|
|
143
|
+
options={multiValues}
|
|
144
|
+
fieldName={filterIndex}
|
|
145
|
+
updateField={updateField}
|
|
146
|
+
selected={filter.active as string[]}
|
|
147
|
+
limit={filter.selectLimit || 5}
|
|
148
|
+
loading={loading}
|
|
149
|
+
/>
|
|
150
|
+
) : filter.filterStyle === FILTER_STYLE.nestedDropdown ? (
|
|
151
|
+
<NestedDropdown
|
|
152
|
+
activeGroup={(filter.queuedActive?.[0] || filter.active) as string}
|
|
153
|
+
activeSubGroup={_key ? filter.queuedActive?.[1] || filter.subGrouping?.active : activeSubGroupValue}
|
|
154
|
+
filterIndex={filterIndex}
|
|
155
|
+
options={_key ? getNestedDropdownOptions(apiFilterDropdowns[_key]) : nestedOptions}
|
|
156
|
+
listLabel={label}
|
|
157
|
+
handleSelectedItems={value => updateField(null, null, filterIndex, value)}
|
|
158
|
+
loading={loading}
|
|
159
|
+
/>
|
|
160
|
+
) : (
|
|
161
|
+
<>
|
|
162
|
+
<select
|
|
163
|
+
id={`filter-${filterIndex}`}
|
|
164
|
+
className={`cove-form-select ${DROPDOWN_STYLES}`}
|
|
165
|
+
data-index='0'
|
|
166
|
+
value={loading ? 'Loading...' : filter.queuedActive || filter.active}
|
|
167
|
+
onChange={val => {
|
|
168
|
+
handleOnChange(filterIndex, val.target.value)
|
|
169
|
+
}}
|
|
170
|
+
disabled={loading || isDisabled}
|
|
171
|
+
>
|
|
172
|
+
{loading && <option value='Loading...'>Loading...</option>}
|
|
173
|
+
{nullVal(filter) && (
|
|
174
|
+
<option key={`select`} value=''>
|
|
175
|
+
{filter.resetLabel || '- Select -'}
|
|
176
|
+
</option>
|
|
177
|
+
)}
|
|
178
|
+
{values}
|
|
179
|
+
</select>
|
|
180
|
+
{loading && <Loader spinnerType={'text-secondary'} />}
|
|
181
|
+
</>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
)
|
|
185
|
+
})}
|
|
186
|
+
{showSubmit && (
|
|
187
|
+
<button
|
|
188
|
+
className='btn btn-primary mb-1'
|
|
189
|
+
onClick={applyFilters}
|
|
190
|
+
disabled={show.some(filterIndex => {
|
|
191
|
+
const emptyFilterValues = [undefined, '', '- Select -']
|
|
192
|
+
return (
|
|
193
|
+
emptyFilterValues.includes(sharedFilters[filterIndex].queuedActive) &&
|
|
194
|
+
emptyFilterValues.includes(sharedFilters[filterIndex].active)
|
|
195
|
+
)
|
|
196
|
+
})}
|
|
197
|
+
>
|
|
198
|
+
{applyFiltersButtonText || 'GO!'}
|
|
199
|
+
</button>
|
|
200
|
+
)}
|
|
201
|
+
</form>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default DashboardFilters
|