@cdc/dashboard 4.24.7 → 4.24.9

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.
Files changed (40) hide show
  1. package/dist/cdcdashboard.js +128394 -122305
  2. package/examples/single-state-dashboard-filters.json +421 -0
  3. package/examples/state-level.json +90136 -0
  4. package/examples/state-points.json +10474 -0
  5. package/examples/test-file.json +147 -0
  6. package/examples/testing.json +94456 -0
  7. package/index.html +18 -6
  8. package/package.json +9 -9
  9. package/src/CdcDashboardComponent.tsx +154 -90
  10. package/src/DashboardContext.tsx +7 -1
  11. package/src/_stories/Dashboard.stories.tsx +124 -10
  12. package/src/_stories/_mock/api-filter-map.json +1 -1
  13. package/src/_stories/_mock/bump-chart.json +3554 -0
  14. package/src/_stories/_mock/methodology.json +412 -0
  15. package/src/_stories/_mock/methodologyAPI.ts +90 -0
  16. package/src/_stories/_mock/multi-viz.json +1 -1
  17. package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
  18. package/src/components/DashboardFilters/DashboardFilters.tsx +39 -17
  19. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +2 -2
  20. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +141 -31
  21. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +66 -18
  22. package/src/components/Header/Header.tsx +0 -5
  23. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +20 -8
  24. package/src/components/Row.tsx +1 -1
  25. package/src/components/VisualizationRow.tsx +98 -17
  26. package/src/components/Widget.tsx +1 -0
  27. package/src/helpers/FilterBehavior.ts +4 -0
  28. package/src/helpers/addValuesToDashboardFilters.ts +49 -0
  29. package/src/helpers/apiFilterHelpers.ts +69 -18
  30. package/src/helpers/changeFilterActive.ts +16 -7
  31. package/src/helpers/getFilteredData.ts +4 -4
  32. package/src/helpers/iconHash.tsx +2 -0
  33. package/src/helpers/loadAPIFilters.ts +74 -0
  34. package/src/helpers/reloadURLHelpers.ts +13 -3
  35. package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
  36. package/src/helpers/tests/apiFilterHelpers.test.ts +156 -0
  37. package/src/helpers/tests/getFilteredData.test.ts +86 -0
  38. package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +176 -0
  39. package/src/helpers/tests/reloadURLHelpers.test.ts +195 -0
  40. package/src/types/SharedFilter.ts +2 -1
@@ -23,30 +23,27 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
23
23
  const [columns, setColumns] = useState<string[]>([])
24
24
  const transform = new DataTransform()
25
25
 
26
- const parentFilters: string[] = (config.dashboard.sharedFilters || []).filter(({ key, type }) => key !== filter.key && type !== 'datafilter').map(({ key }) => key)
26
+ const parentFilters: string[] = (config.dashboard.sharedFilters || [])
27
+ .filter(({ key, type }) => key !== filter.key && type !== 'datafilter')
28
+ .map(({ key }) => key)
27
29
 
28
30
  const vizRowColumnLocator = getVizRowColumnLocator(config.rows)
29
31
 
30
32
  const [usedByNameLookup, usedByOptions] = useMemo(() => {
31
33
  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
- })
34
+ const vizOptions = Object.keys(config.visualizations).filter(vizKey => {
35
+ const vizLookup = vizRowColumnLocator[vizKey]
36
+ if (!vizLookup) return false
37
+ const viz = config.visualizations[vizKey] as Visualization
38
+ if (viz.type === 'dashboardFilters') return false
39
+ const vizName = viz.general?.title || viz.title || vizKey
40
+ nameLookup[vizKey] = vizName
41
+ const notAdded = !filter.usedBy || filter.usedBy.indexOf(vizKey) === -1
42
+ const usesSharedFilter = viz.usesSharedFilter
43
+ const rowIndex = vizLookup.row
44
+ const dataConfiguredOnRow = config.rows[rowIndex].dataKey
45
+ return filter.setBy !== vizKey && notAdded && !usesSharedFilter && !dataConfiguredOnRow
46
+ })
50
47
  const rowOptions: number[] = []
51
48
 
52
49
  config.rows.forEach((row, rowIndex) => {
@@ -121,7 +118,11 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
121
118
  <>
122
119
  <label>
123
120
  <span className='edit-label column-heading'>Filter Type: </span>
124
- <select defaultValue={filter.type || ''} onChange={e => updateFilterProp('type', e.target.value)} disabled={!!filter.type}>
121
+ <select
122
+ defaultValue={filter.type || ''}
123
+ onChange={e => updateFilterProp('type', e.target.value)}
124
+ disabled={!!filter.type}
125
+ >
125
126
  <option value=''>- Select Option -</option>
126
127
  <option value='urlfilter'>URL</option>
127
128
  <option value='datafilter'>Data</option>
@@ -129,12 +130,19 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
129
130
  </label>
130
131
  {filter.type === 'urlfilter' && (
131
132
  <>
132
- <TextField label='Label' value={filter.key} updateField={(_section, _subSection, _key, value) => updateFilterProp('key', value)} />
133
+ <TextField
134
+ label='Label'
135
+ value={filter.key}
136
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('key', value)}
137
+ />
133
138
  {!hasDashboardApplyBehavior(config.visualizations) && (
134
139
  <>
135
140
  <label>
136
141
  <span className='edit-label column-heading'>URL to Filter: </span>
137
- <select defaultValue={filter.datasetKey || ''} onChange={e => updateFilterProp('datasetKey', e.target.value)}>
142
+ <select
143
+ defaultValue={filter.datasetKey || ''}
144
+ onChange={e => updateFilterProp('datasetKey', e.target.value)}
145
+ >
138
146
  <option value=''>- Select Option -</option>
139
147
  {Object.keys(config.datasets).map(datasetKey => {
140
148
  if (config.datasets[datasetKey].dataUrl) {
@@ -150,7 +158,10 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
150
158
  </label>
151
159
  <label>
152
160
  <span className='edit-label column-heading'>Filter By: </span>
153
- <select defaultValue={filter.filterBy || ''} onChange={e => updateFilterProp('filterBy', e.target.value)}>
161
+ <select
162
+ defaultValue={filter.filterBy || ''}
163
+ onChange={e => updateFilterProp('filterBy', e.target.value)}
164
+ >
154
165
  <option value=''>- Select Option -</option>
155
166
  <option key={'query-string'} value={'Query String'}>
156
167
  Query String
@@ -190,7 +201,10 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
190
201
  </Tooltip.Content>
191
202
  </Tooltip>
192
203
  </span>
193
- <select defaultValue={filter.whitespaceReplacement || 'Keep Spaces'} onChange={e => updateFilterProp('whitespaceReplacement', e.target.value)}>
204
+ <select
205
+ defaultValue={filter.whitespaceReplacement || 'Keep Spaces'}
206
+ onChange={e => updateFilterProp('whitespaceReplacement', e.target.value)}
207
+ >
194
208
  <option key={'remove-spaces'} value={'Remove Spaces'}>
195
209
  Remove Spaces
196
210
  </option>
@@ -206,8 +220,18 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
206
220
  )}
207
221
  </>
208
222
  )}
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)} />
223
+ {filter.filterBy === 'Query String' && (
224
+ <TextField
225
+ label='Query string parameter'
226
+ value={filter.queryParameter}
227
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('queryParameter', value)}
228
+ />
229
+ )}
230
+ <TextField
231
+ label='Filter API Endpoint: '
232
+ value={filter.apiFilter?.apiEndpoint}
233
+ updateField={(_section, _subSection, _key, value) => updateAPIFilter('apiEndpoint', value)}
234
+ />
211
235
  <TextField
212
236
  label='Option Text Selector:'
213
237
  value={filter.apiFilter?.textSelector}
@@ -257,9 +281,43 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
257
281
  />
258
282
  )}
259
283
 
260
- <TextField label='Reset Label: ' value={filter.resetLabel || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)} />
284
+ <MultiSelect
285
+ label='Used By: (optional)'
286
+ tooltip={
287
+ <Tooltip style={{ textTransform: 'none' }}>
288
+ <Tooltip.Target>
289
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
290
+ </Tooltip.Target>
291
+ <Tooltip.Content>
292
+ <p>
293
+ Select if you would like specific visualizations or rows to use this filter. Otherwise the filter
294
+ will be added to all api requests.
295
+ </p>
296
+ </Tooltip.Content>
297
+ </Tooltip>
298
+ }
299
+ options={[...usedByOptions, ...(filter.usedBy || [])].map(opt => ({
300
+ value: opt,
301
+ label: usedByNameLookup[opt]
302
+ }))}
303
+ fieldName='usedBy'
304
+ selected={filter.usedBy}
305
+ updateField={(_section, _subsection, _fieldname, newItems) => {
306
+ updateFilterProp('usedBy', newItems)
307
+ }}
308
+ />
261
309
 
262
- <TextField label='Default Value Set By Query String Parameter: ' value={filter.setByQueryParameter || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('setByQueryParameter', value)} />
310
+ <TextField
311
+ label='Reset Label: '
312
+ value={filter.resetLabel || ''}
313
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)}
314
+ />
315
+
316
+ <TextField
317
+ label='Default Value Set By Query String Parameter: '
318
+ value={filter.setByQueryParameter || ''}
319
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('setByQueryParameter', value)}
320
+ />
263
321
  </>
264
322
  )}
265
323
  {filter.type === 'datafilter' && (
@@ -281,7 +339,11 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
281
339
  </select>
282
340
  </label>
283
341
 
284
- <TextField label='Label' value={filter.key} updateField={(_section, _subSection, _key, value) => updateFilterProp('key', value)} />
342
+ <TextField
343
+ label='Label'
344
+ value={filter.key}
345
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('key', value)}
346
+ />
285
347
 
286
348
  <label>
287
349
  <span className='edit-label column-heading'>Show Dropdown</span>
@@ -337,7 +399,11 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
337
399
  ))}
338
400
  </select>
339
401
  </label>
340
- <TextField label='Reset Label: ' value={filter.resetLabel || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)} />
402
+ <TextField
403
+ label='Reset Label: '
404
+ value={filter.resetLabel || ''}
405
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)}
406
+ />
341
407
 
342
408
  <label>
343
409
  <span className='edit-label column-heading'>Parent Filter: </span>
@@ -357,9 +423,53 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
357
423
  </select>
358
424
  </label>
359
425
 
360
- <TextField label='Default Value Set By Query String Parameter: ' value={filter.setByQueryParameter || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('setByQueryParameter', value)} />
426
+ <TextField
427
+ label='Default Value Set By Query String Parameter: '
428
+ value={filter.setByQueryParameter || ''}
429
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('setByQueryParameter', value)}
430
+ />
361
431
  </>
362
432
  )}
433
+ <label>
434
+ <span className='mr-1'>Multi Select</span>
435
+ <input
436
+ type='checkbox'
437
+ checked={filter.multiSelect}
438
+ onChange={e => {
439
+ updateFilterProp('multiSelect', !filter.multiSelect)
440
+ }}
441
+ />
442
+ </label>
443
+
444
+ {filter.multiSelect && (
445
+ <TextField
446
+ label='Select Limit'
447
+ value={filter.selectLimit}
448
+ updateField={(_section, _subSection, _field, value) => updateFilterProp('selectLimit', value)}
449
+ type='number'
450
+ tooltip={
451
+ <Tooltip style={{ textTransform: 'none' }}>
452
+ <Tooltip.Target>
453
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
454
+ </Tooltip.Target>
455
+ <Tooltip.Content>
456
+ <p>The maximum number of items that can be selected.</p>
457
+ </Tooltip.Content>
458
+ </Tooltip>
459
+ }
460
+ />
461
+ )}
462
+
463
+ <label>
464
+ <span className='mr-1'>Show Dropdown</span>
465
+ <input
466
+ type='checkbox'
467
+ checked={filter.showDropdown}
468
+ onChange={e => {
469
+ updateFilterProp('showDropdown', !filter.showDropdown)
470
+ }}
471
+ />
472
+ </label>
363
473
  </>
364
474
  )
365
475
  }
@@ -3,7 +3,7 @@ import { DashboardContext, DashboardDispatchContext } from '../../DashboardConte
3
3
  import Filters from './DashboardFilters'
4
4
  import { changeFilterActive } from '../../helpers/changeFilterActive'
5
5
  import _ from 'lodash'
6
- import { FilterBehavior } from '../Header/Header'
6
+ import { FilterBehavior } from '../../helpers/FilterBehavior'
7
7
  import { getFilteredData } from '../../helpers/getFilteredData'
8
8
  import { DashboardFilters } from '../../types/DashboardFilters'
9
9
  import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
@@ -11,12 +11,14 @@ import Layout from '@cdc/core/components/Layout'
11
11
  import DashboardFiltersEditor from './DashboardFiltersEditor'
12
12
  import { ViewPort } from '@cdc/core/types/ViewPort'
13
13
  import { hasDashboardApplyBehavior } from '../../helpers/hasDashboardApplyBehavior'
14
+ import * as apiFilterHelpers from '../../helpers/apiFilterHelpers'
14
15
 
15
16
  export type DropdownOptions = Record<'value' | 'text', string>[]
16
17
 
18
+ /** the cached dropdown options for each filter */
17
19
  export type APIFilterDropdowns = {
18
20
  // null means still loading
19
- [filtername: string]: null | DropdownOptions
21
+ [dropdownsKey: string]: null | DropdownOptions
20
22
  }
21
23
 
22
24
  type DashboardFiltersProps = {
@@ -27,9 +29,15 @@ type DashboardFiltersProps = {
27
29
  currentViewport?: ViewPort
28
30
  }
29
31
 
30
- const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({ apiFilterDropdowns, visualizationConfig, setConfig: updateConfig, currentViewport, isEditor = false }) => {
32
+ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
33
+ apiFilterDropdowns,
34
+ visualizationConfig,
35
+ setConfig: updateConfig,
36
+ currentViewport,
37
+ isEditor = false
38
+ }) => {
31
39
  const state = useContext(DashboardContext)
32
- const { config: dashboardConfig, reloadURLData, loadAPIFilters } = state
40
+ const { config: dashboardConfig, reloadURLData, loadAPIFilters, setAPIFilterDropdowns } = state
33
41
  const dispatch = useContext(DashboardDispatchContext)
34
42
 
35
43
  const applyFilters = () => {
@@ -54,8 +62,13 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({ apiFilterDro
54
62
  dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
55
63
  delete dashboardConfig.sharedFilters[index].queuedActive
56
64
 
57
- if (sharedFilter.setByQueryParameter && queryParams[sharedFilter.setByQueryParameter] !== sharedFilter.active) {
58
- queryParams[sharedFilter.setByQueryParameter] = sharedFilter.active
65
+ if (
66
+ sharedFilter.setByQueryParameter &&
67
+ queryParams[sharedFilter.setByQueryParameter] !== sharedFilter.active
68
+ ) {
69
+ queryParams[sharedFilter.setByQueryParameter] = Array.isArray(sharedFilter.active)
70
+ ? sharedFilter.active.join(',')
71
+ : sharedFilter.active
59
72
  needsQueryUpdate = true
60
73
  }
61
74
  }
@@ -68,7 +81,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({ apiFilterDro
68
81
 
69
82
  dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
70
83
  dispatch({ type: 'SET_FILTERED_DATA', payload: getFilteredData(_.cloneDeep(state)) })
71
- loadAPIFilters(dashboardConfig.sharedFilters)
84
+ loadAPIFilters(dashboardConfig.sharedFilters, apiFilterDropdowns)
72
85
  .then(newFilters => {
73
86
  reloadURLData(newFilters)
74
87
  })
@@ -82,24 +95,36 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({ apiFilterDro
82
95
 
83
96
  const handleOnChange = (index: number, value: string | string[]) => {
84
97
  const newConfig = _.cloneDeep(dashboardConfig)
85
- let newSharedFilters = changeFilterActive(index, value, newConfig.dashboard.sharedFilters, visualizationConfig)
98
+ let [newSharedFilters, changedFilterIndexes] = changeFilterActive(
99
+ index,
100
+ value,
101
+ newConfig.dashboard.sharedFilters,
102
+ visualizationConfig
103
+ )
86
104
 
87
105
  if (hasDashboardApplyBehavior(dashboardConfig.visualizations)) {
88
106
  const isAutoSelectFilter = visualizationConfig.autoLoad
89
107
  const missingFilterSelections = newConfig.dashboard.sharedFilters.some(f => !f.active)
108
+ const apiEndpoints = newSharedFilters.filter(f => f.apiFilter).map(f => f.apiFilter.apiEndpoint)
109
+ const loadingFilterMemo = apiFilterHelpers.getLoadingFilterMemo(
110
+ apiEndpoints,
111
+ apiFilterDropdowns,
112
+ changedFilterIndexes
113
+ )
90
114
  if (isAutoSelectFilter && !missingFilterSelections) {
91
115
  // a dropdown has been selected that doesn't
92
116
  // require the Go Button
93
- loadAPIFilters(newSharedFilters).then(filters => {
117
+ setAPIFilterDropdowns(loadingFilterMemo)
118
+ loadAPIFilters(newSharedFilters, loadingFilterMemo).then(filters => {
94
119
  reloadURLData(filters)
95
120
  })
96
121
  } else {
97
- if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
98
122
  newSharedFilters[index].queuedActive = value
99
123
  // setData to empty object because we no longer have a data state.
100
124
  dispatch({ type: 'SET_DATA', payload: {} })
101
125
  dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
102
- loadAPIFilters(newSharedFilters)
126
+ setAPIFilterDropdowns(loadingFilterMemo)
127
+ loadAPIFilters(newSharedFilters, loadingFilterMemo)
103
128
  }
104
129
  } else {
105
130
  if (newSharedFilters[index].apiFilter) {
@@ -122,20 +147,43 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({ apiFilterDro
122
147
  })
123
148
  }
124
149
 
150
+ // if all of the filters are hidden filters don't display the VisualizationWrapper
151
+ const filters = visualizationConfig?.sharedFilterIndexes
152
+ ?.map(Number)
153
+ .map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
154
+
155
+ const displayNone = filters.length ? filters.every(filter => filter.showDropdown === false) : false
156
+ if (displayNone && !isEditor) return <></>
125
157
  return (
126
158
  <Layout.VisualizationWrapper config={visualizationConfig} isEditor={isEditor} currentViewport={currentViewport}>
127
159
  {isEditor && (
128
- <Layout.Sidebar displayPanel={displayPanel} isDashboard={true} title={'Configure Dashboard Filters'} onBackClick={onBackClick}>
160
+ <Layout.Sidebar
161
+ displayPanel={displayPanel}
162
+ isDashboard={true}
163
+ title={'Configure Dashboard Filters'}
164
+ onBackClick={onBackClick}
165
+ >
129
166
  <DashboardFiltersEditor updateConfig={updateConfig} vizConfig={visualizationConfig} />
130
167
  </Layout.Sidebar>
131
168
  )}
132
169
 
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>
170
+ {!displayNone && (
171
+ <Layout.Responsive isEditor={isEditor}>
172
+ <div
173
+ className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''} cove-component__content col-12`}
174
+ >
175
+ <Filters
176
+ show={visualizationConfig?.sharedFilterIndexes?.map(Number)}
177
+ filters={dashboardConfig.dashboard.sharedFilters || []}
178
+ apiFilterDropdowns={apiFilterDropdowns}
179
+ handleOnChange={handleOnChange}
180
+ />
181
+ {visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad && (
182
+ <button onClick={applyFilters}>GO!</button>
183
+ )}
184
+ </div>
185
+ </Layout.Responsive>
186
+ )}
139
187
  </Layout.VisualizationWrapper>
140
188
  )
141
189
  }
@@ -13,11 +13,6 @@ type HeaderProps = {
13
13
  visualizationKey?: string
14
14
  }
15
15
 
16
- export const FilterBehavior = {
17
- Apply: 'Apply Button',
18
- OnChange: 'Filter Change'
19
- }
20
-
21
16
  const Header = (props: HeaderProps) => {
22
17
  const tabs: Tab[] = ['Dashboard Description', 'Data Table Settings', 'Dashboard Preview']
23
18
  const { visualizationKey, subEditor } = props
@@ -23,7 +23,8 @@ const Tab = ({ name, handleClick, tabs, index, active }) => {
23
23
  const { overlay } = useGlobalContext()
24
24
  const inputRef = createRef<HTMLInputElement>()
25
25
 
26
- const onBlur = () => {
26
+ const saveName = e => {
27
+ e.stopPropagation()
27
28
  const newVal = inputRef.current.value
28
29
  const sameName = newVal === name
29
30
  const blankName = !newVal
@@ -64,15 +65,26 @@ const Tab = ({ name, handleClick, tabs, index, active }) => {
64
65
  const canMoveRight = index <= tabs.length - 2
65
66
 
66
67
  return (
67
- <li className='nav-item'>
68
+ <li className='nav-item d-flex mt-0'>
69
+ {canMoveLeft && editing && <button onClick={() => handleReorder(index, -1)}>{'<'}</button>}
68
70
  <div className={`edit nav-link${active ? ' active' : ''}`} aria-current={active ? 'page' : null} onClick={onClick}>
69
- {canMoveLeft && <button onClick={() => handleReorder(index, -1)}>{'<'}</button>}
70
- {editing ? <input type='text' defaultValue={name} onBlur={onBlur} ref={inputRef} /> : <>{name}</>}
71
- {canMoveRight && <button onClick={() => handleReorder(index, 1)}>{'>'}</button>}
72
- <button className='remove' onClick={handleRemove}>
73
- X
74
- </button>
71
+ {editing ? (
72
+ <div className='d-flex'>
73
+ <input type='text' defaultValue={name} onBlur={saveName} ref={inputRef} />
74
+ <button className='btn btn-link save' onClick={saveName}>
75
+ save
76
+ </button>
77
+ </div>
78
+ ) : (
79
+ <>
80
+ {name}
81
+ <button className='remove' onClick={handleRemove}>
82
+ X
83
+ </button>
84
+ </>
85
+ )}
75
86
  </div>
87
+ {canMoveRight && editing && <button onClick={() => handleReorder(index, 1)}>{'>'}</button>}
76
88
  </li>
77
89
  )
78
90
  }
@@ -193,7 +193,7 @@ const Row: React.FC<RowProps> = ({ row, idx: rowIdx, uuid }) => {
193
193
  overlay?.actions.openOverlay(<DataDesignerModal rowIndex={rowIdx} />)
194
194
  }}
195
195
  >
196
- {iconHash['gear']}
196
+ {iconHash['gearMulti']}
197
197
  </button>
198
198
  <div className='column-container'>
199
199
  {row.columns