@cdc/dashboard 4.25.11 → 4.26.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.
Files changed (77) hide show
  1. package/Dynamic_Data.md +66 -0
  2. package/dist/cdcdashboard-8NmHlKRI.es.js +15 -0
  3. package/dist/cdcdashboard-BPoPzKPz.es.js +6 -0
  4. package/dist/cdcdashboard-Cf9_fbQf.es.js +6 -0
  5. package/dist/{cdcdashboard-dgT_1dIT.es.js → cdcdashboard-DQ00cQCm.es.js} +1 -20
  6. package/dist/cdcdashboard-jiQQPkty.es.js +6 -0
  7. package/dist/cdcdashboard.js +83537 -86913
  8. package/examples/api-dashboard-data.json +272 -0
  9. package/examples/api-dashboard-years.json +11 -0
  10. package/examples/api-geographies-data.json +11 -0
  11. package/examples/default.json +522 -133
  12. package/examples/nested-dropdown.json +6985 -0
  13. package/examples/private/abc.json +467 -0
  14. package/examples/private/cat-y.json +1235 -0
  15. package/examples/private/chronic-dash.json +1584 -0
  16. package/examples/private/dash.json +12696 -0
  17. package/examples/private/map-issue.json +2260 -0
  18. package/examples/private/mpinc-state-reports.json +2260 -0
  19. package/examples/private/npcr.json +1 -0
  20. package/examples/private/nwss/rsv.json +1240 -0
  21. package/examples/private/simple-dash.json +490 -0
  22. package/examples/private/test-dash.json +0 -0
  23. package/examples/private/test.json +125407 -0
  24. package/examples/private/test123.json +491 -0
  25. package/examples/private/timeline-data.json +4994 -0
  26. package/examples/private/timeline.json +1708 -0
  27. package/examples/test-api-filter-reset.json +8 -4
  28. package/examples/test-dashboard-simple.json +503 -0
  29. package/examples/tp5-gauges.json +196 -0
  30. package/examples/tp5-test.json +266 -0
  31. package/index.html +1 -30
  32. package/package.json +39 -40
  33. package/src/CdcDashboardComponent.tsx +18 -5
  34. package/src/_stories/Dashboard.DataSetup.stories.tsx +204 -0
  35. package/src/_stories/Dashboard.stories.tsx +407 -1
  36. package/src/_stories/_mock/dashboard-line-chart-angles.json +1030 -0
  37. package/src/_stories/_mock/filter-cascade.json +3350 -0
  38. package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
  39. package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
  40. package/src/_stories/_mock/parent-child-filters.json +233 -0
  41. package/src/_stories/_mock/tp5-test.json +267 -0
  42. package/src/components/DashboardFilters/DashboardFilters.tsx +20 -11
  43. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +92 -38
  44. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +56 -30
  45. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +151 -10
  46. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +11 -7
  47. package/src/components/DataDesignerModal.tsx +6 -1
  48. package/src/components/Header/Header.tsx +51 -20
  49. package/src/components/VisualizationRow.tsx +76 -5
  50. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -20
  51. package/src/components/Widget/Widget.tsx +1 -1
  52. package/src/data/initial-state.js +1 -0
  53. package/src/helpers/addValuesToDashboardFilters.ts +30 -31
  54. package/src/helpers/apiFilterHelpers.ts +28 -32
  55. package/src/helpers/changeFilterActive.ts +67 -65
  56. package/src/helpers/formatConfigBeforeSave.ts +6 -5
  57. package/src/helpers/getUpdateConfig.ts +91 -91
  58. package/src/helpers/tests/addValuesToDashboardFilters.test.ts +141 -44
  59. package/src/helpers/tests/apiFilterHelpers.test.ts +523 -420
  60. package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
  61. package/src/helpers/updateChildFilters.ts +50 -27
  62. package/src/scss/main.scss +144 -1
  63. package/src/test/CdcDashboard.test.jsx +9 -4
  64. package/src/types/Dashboard.ts +1 -0
  65. package/src/types/FilterStyles.ts +8 -7
  66. package/src/types/SharedFilter.ts +13 -0
  67. package/vite.config.js +7 -1
  68. package/LICENSE +0 -201
  69. package/dist/cdcdashboard-BnB1QM5d.es.js +0 -361528
  70. package/dist/cdcdashboard-Ct2SB0vL.es.js +0 -231049
  71. package/dist/cdcdashboard-D6CG2-Hb.es.js +0 -39377
  72. package/dist/cdcdashboard-MXgURbdZ.es.js +0 -39194
  73. package/examples/private/DEV-10538.json +0 -407
  74. package/examples/private/DEV-11072.json +0 -7591
  75. package/examples/private/DEV-11405.json +0 -39112
  76. package/examples/private/delete.json +0 -32919
  77. package/examples/private/pedro.json +0 -1
@@ -1,21 +1,26 @@
1
1
  import { DashboardConfig } from '../../../../types/DashboardConfig'
2
2
  import { SharedFilter } from '../../../../types/SharedFilter'
3
3
  import _ from 'lodash'
4
- import { SubGrouping } from '@cdc/core/types/VizFilter'
4
+ import { SubGrouping, OrderBy } from '@cdc/core/types/VizFilter'
5
5
  import { TextField, Select } from '@cdc/core/components/EditorPanel/Inputs'
6
+ import { handleSorting } from '@cdc/core/components/Filters/helpers/handleSorting'
7
+ import { filterOrderOptions } from '@cdc/core/helpers/filterOrderOptions'
8
+ import FilterOrder from '@cdc/core/components/EditorPanel/VizFilterEditor/components/FilterOrder'
6
9
 
7
10
  type NestedDropDownEditorDashboardProps = {
8
11
  config: DashboardConfig
9
12
  filter: SharedFilter
10
13
  isDashboard: boolean
11
14
  updateFilterProp: Function
15
+ onNestedDragAreaHover?: (isHovering: boolean) => void
12
16
  }
13
17
 
14
18
  const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
15
19
  filter,
16
20
  config,
17
21
  isDashboard = false,
18
- updateFilterProp
22
+ updateFilterProp,
23
+ onNestedDragAreaHover
19
24
  }) => {
20
25
  const subGrouping = filter?.subGrouping
21
26
 
@@ -50,12 +55,10 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
50
55
  const handleFitlerGroupColumnNameChange = (value: string) => {
51
56
  if (!value) {
52
57
  updateFilterProp('columnName', '')
53
- updateFilterProp('defaultValue', '')
54
58
  return
55
59
  }
56
60
  const [newColumnName, selectedOptionDatasetName] = value.split('|')
57
61
  updateFilterProp('columnName', newColumnName)
58
- updateFilterProp('defaultValue', '') // Reset default value when column changes
59
62
  populateSubGroupingOptions(selectedOptionDatasetName, newColumnName)
60
63
  }
61
64
 
@@ -66,18 +69,23 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
66
69
  }
67
70
  const [newColumnName, selectedOptionDatasetName] = value.split('|')
68
71
 
72
+ const order = subGrouping?.order || 'asc'
73
+
69
74
  const valuesLookup = filter.values.reduce((acc, groupName) => {
70
- const values: string[] = _.uniq(
75
+ const rawValues: string[] = _.uniq(
71
76
  config.datasets[selectedOptionDatasetName].data
72
77
  .map(d => {
73
78
  return d[filter.columnName] === groupName ? d[newColumnName] : ''
74
79
  })
75
80
  .filter(value => value !== '')
76
- ).sort()
81
+ )
82
+
83
+ // Sort values according to the order setting
84
+ const { values: sortedValues } = handleSorting({ values: rawValues, order })
77
85
 
78
86
  acc[groupName] = {
79
- values,
80
- orderedValues: values
87
+ values: sortedValues,
88
+ orderedValues: sortedValues
81
89
  }
82
90
  return acc
83
91
  }, {})
@@ -86,12 +94,94 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
86
94
  ...subGrouping,
87
95
  columnName: newColumnName,
88
96
  valuesLookup,
97
+ order,
89
98
  defaultValue: '' // Reset default value when column changes
90
99
  }
91
100
 
92
101
  updateFilterProp('subGrouping', newSubGrouping)
93
102
  }
94
103
 
104
+ // Handle group order change (asc/desc/cust)
105
+ const handleGroupingOrderBy = (order: OrderBy) => {
106
+ const groupSortObject = {
107
+ values: _.cloneDeep(filter.values),
108
+ order
109
+ }
110
+ const { values: newOrderedValues } = handleSorting(groupSortObject)
111
+
112
+ const updates: Partial<SharedFilter> = {
113
+ values: newOrderedValues,
114
+ order
115
+ }
116
+
117
+ if (order === 'cust') {
118
+ updates.orderedValues = newOrderedValues
119
+ } else {
120
+ updates.orderedValues = undefined
121
+ }
122
+
123
+ // Update filter with new order and values
124
+ updateFilterProp('order', order)
125
+ }
126
+
127
+ // Handle drag-drop reorder for group values
128
+ const handleGroupingCustomOrder = (sourceIndex: number, destinationIndex: number) => {
129
+ if (sourceIndex === undefined || destinationIndex === undefined || sourceIndex === destinationIndex) return
130
+
131
+ const orderedValues = _.cloneDeep(filter.orderedValues || filter.values)
132
+ const [movedItem] = orderedValues.splice(sourceIndex, 1)
133
+ orderedValues.splice(destinationIndex, 0, movedItem)
134
+
135
+ // Update both values and orderedValues, and ensure order is 'cust'
136
+ updateFilterProp('orderedValues', orderedValues)
137
+ if (filter.order !== 'cust') {
138
+ updateFilterProp('order', 'cust')
139
+ }
140
+ }
141
+
142
+ // Handle subgroup order change (asc/desc/cust)
143
+ const handleSubGroupingOrderBy = (order: OrderBy) => {
144
+ const newValuesLookup = Object.keys(subGrouping.valuesLookup).reduce((acc, groupName) => {
145
+ const subGroup = subGrouping.valuesLookup[groupName]
146
+ const { values: sortedValues } = handleSorting({ values: _.cloneDeep(subGroup.values), order })
147
+
148
+ acc[groupName] = {
149
+ values: sortedValues,
150
+ orderedValues: order === 'cust' ? sortedValues : undefined
151
+ }
152
+ return acc
153
+ }, {})
154
+
155
+ const newSubGrouping: SubGrouping = {
156
+ ...subGrouping,
157
+ order,
158
+ valuesLookup: newValuesLookup
159
+ }
160
+
161
+ updateFilterProp('subGrouping', newSubGrouping)
162
+ }
163
+
164
+ // Handle drag-drop reorder for subgroup values within a specific group
165
+ const handleSubGroupingCustomOrder = (
166
+ sourceIndex: number,
167
+ destinationIndex: number,
168
+ currentOrderedValues: string[],
169
+ groupName: string
170
+ ) => {
171
+ if (sourceIndex === undefined || destinationIndex === undefined || sourceIndex === destinationIndex) return
172
+
173
+ const updatedGroupOrderedValues = _.cloneDeep(currentOrderedValues)
174
+ const [movedItem] = updatedGroupOrderedValues.splice(sourceIndex, 1)
175
+ updatedGroupOrderedValues.splice(destinationIndex, 0, movedItem)
176
+
177
+ const newSubGrouping = _.cloneDeep(subGrouping)
178
+ newSubGrouping.valuesLookup[groupName].values = updatedGroupOrderedValues
179
+ newSubGrouping.valuesLookup[groupName].orderedValues = updatedGroupOrderedValues
180
+ newSubGrouping.order = 'cust'
181
+
182
+ updateFilterProp('subGrouping', newSubGrouping)
183
+ }
184
+
95
185
  return (
96
186
  <div className='nesteddropdown-editor'>
97
187
  {!isDashboard && (
@@ -144,7 +234,7 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
144
234
  {filter.columnName && filter.values && filter.values.length > 0 && (
145
235
  <Select
146
236
  value={filter.defaultValue}
147
- options={filter.values}
237
+ options={filter.orderedValues || filter.values}
148
238
  updateField={(_section, _subSection, _key, value) => updateFilterProp('defaultValue', value)}
149
239
  label={'Group Default Value'}
150
240
  initial={'Select'}
@@ -157,7 +247,8 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
157
247
  value={subGrouping.defaultValue}
158
248
  options={(() => {
159
249
  const groupKey = filter.defaultValue || (Array.isArray(filter.active) ? filter.active[0] : filter.active)
160
- return subGrouping.valuesLookup[groupKey as string]?.values || []
250
+ const lookup = subGrouping.valuesLookup[groupKey as string]
251
+ return lookup?.orderedValues || lookup?.values || []
161
252
  })()}
162
253
  updateField={(_section, _subSection, _key, value) => {
163
254
  const newSubGrouping = { ...subGrouping, defaultValue: value }
@@ -167,6 +258,56 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
167
258
  initial={'Select'}
168
259
  />
169
260
  )}
261
+
262
+ {/* Group Order */}
263
+ {filter.columnName && filter.values && filter.values.length > 0 && (
264
+ <div className='mt-2'>
265
+ <Select
266
+ label='Group Order'
267
+ value={filter.order || 'asc'}
268
+ options={filterOrderOptions}
269
+ onChange={e => handleGroupingOrderBy(e.target.value as OrderBy)}
270
+ />
271
+ {filter.order === 'cust' && (
272
+ <FilterOrder
273
+ orderedValues={filter.orderedValues || filter.values}
274
+ handleFilterOrder={handleGroupingCustomOrder}
275
+ onNestedDragAreaHover={onNestedDragAreaHover}
276
+ />
277
+ )}
278
+ </div>
279
+ )}
280
+
281
+ {/* SubGrouping Order */}
282
+ {subGrouping?.columnName && subGrouping.valuesLookup && Object.keys(subGrouping.valuesLookup).length > 0 && (
283
+ <div className='mt-2'>
284
+ <Select
285
+ label='SubGrouping Order'
286
+ value={subGrouping.order || 'asc'}
287
+ options={filterOrderOptions}
288
+ onChange={e => handleSubGroupingOrderBy(e.target.value as OrderBy)}
289
+ />
290
+ {subGrouping.order === 'cust' &&
291
+ (filter.orderedValues || filter.values)?.map((groupName, i) => {
292
+ const lookup = subGrouping.valuesLookup[groupName]
293
+ if (!lookup) return null
294
+ const orderedSubGroupValues = lookup.orderedValues || lookup.values
295
+ return (
296
+ <div key={`group-subgroup-values-${groupName}-${i}`}>
297
+ <span className='font-weight-bold fw-bold'>{groupName}</span>
298
+ <FilterOrder
299
+ key={`subgroup-values-${groupName}-${i}`}
300
+ orderedValues={orderedSubGroupValues}
301
+ handleFilterOrder={(sourceIndex, destinationIndex) => {
302
+ handleSubGroupingCustomOrder(sourceIndex, destinationIndex, orderedSubGroupValues, groupName)
303
+ }}
304
+ onNestedDragAreaHover={onNestedDragAreaHover}
305
+ />
306
+ </div>
307
+ )
308
+ })}
309
+ </div>
310
+ )}
170
311
  </div>
171
312
  )
172
313
  }
@@ -177,15 +177,18 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
177
177
  // Reset filtersApplied state to false when clearing filters
178
178
  dispatch({ type: 'SET_FILTERS_APPLIED', payload: false })
179
179
 
180
+ // Update child filter values before filtering data
181
+ const updatedFilters = updateChildFilters(dashboardConfig.sharedFilters, state.data)
182
+
180
183
  // Update filtered data immediately after resetting filters
181
- // Use the updated dashboardConfig filters instead of state
184
+ // Use the updated filters instead of state
182
185
  const clonedState = {
183
186
  ...state,
184
187
  config: {
185
188
  ...state.config,
186
189
  dashboard: {
187
190
  ...state.config.dashboard,
188
- sharedFilters: dashboardConfig.sharedFilters
191
+ sharedFilters: updatedFilters
189
192
  }
190
193
  }
191
194
  }
@@ -262,8 +265,9 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
262
265
  loadAPIFilters(newSharedFilters, loadingFilterMemo, undefined, undefined, isStale)
263
266
  }
264
267
  } else {
268
+ const updatedFilters = updateChildFilters(newSharedFilters, state.data)
265
269
  if (newSharedFilters[index].type === 'urlfilter' && newSharedFilters[index].apiFilter) {
266
- reloadURLData(newSharedFilters)
270
+ reloadURLData(updatedFilters)
267
271
  } else {
268
272
  const clonedState = {
269
273
  ...state,
@@ -271,13 +275,13 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
271
275
  ...state.config,
272
276
  dashboard: {
273
277
  ...state.config.dashboard,
274
- sharedFilters: newSharedFilters
278
+ sharedFilters: updatedFilters
275
279
  }
276
280
  }
277
281
  }
278
282
  const newFilteredData = getFilteredData(clonedState)
279
283
  dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
280
- dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
284
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: updatedFilters })
281
285
  }
282
286
  }
283
287
  }
@@ -293,9 +297,9 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
293
297
  // if all of the filters are hidden filters don't display the VisualizationWrapper
294
298
  const filters = visualizationConfig?.sharedFilterIndexes
295
299
  ?.map(Number)
296
- .map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
300
+ ?.map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
297
301
 
298
- const displayNone = filters.length ? filters.every(filter => filter.showDropdown === false) : false
302
+ const displayNone = filters?.length ? filters.every(filter => filter.showDropdown === false) : false
299
303
  if (displayNone && !isEditor) return <></>
300
304
  return (
301
305
  <Layout.VisualizationWrapper config={visualizationConfig} isEditor={isEditor} currentViewport={currentViewport}>
@@ -194,7 +194,12 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
194
194
  ) : (
195
195
  <>
196
196
  <Select
197
- options={Object.keys(config.datasets[configureData.dataKey]?.data[0] || {})}
197
+ options={Object.keys(
198
+ config.rows[rowIndex]?.data?.[0] ||
199
+ configureData.data?.[0] ||
200
+ config.datasets[configureData.dataKey]?.data?.[0] ||
201
+ {}
202
+ )}
198
203
  value={config.rows[rowIndex].multiVizColumn}
199
204
  label='Multi-Visualization Column'
200
205
  initial='--Select--'
@@ -1,4 +1,4 @@
1
- import { useContext, useRef } from 'react'
1
+ import { useContext, useRef, useEffect } from 'react'
2
2
  import cloneConfig from '@cdc/core/helpers/cloneConfig'
3
3
  import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
4
4
 
@@ -20,8 +20,22 @@ const Header = (props: HeaderProps) => {
20
20
  const dispatch = useContext(DashboardDispatchContext)
21
21
  const back = () => {
22
22
  if (!visualizationKey) return
23
+
23
24
  const newConfig = cloneConfig(config)
24
- newConfig.visualizations[visualizationKey].editing = false
25
+
26
+ // Ensure visualizations object exists
27
+ if (!newConfig.visualizations || !newConfig.visualizations[visualizationKey]) {
28
+ console.error(`Visualization ${visualizationKey} not found in config`)
29
+ return
30
+ }
31
+
32
+ // Explicitly set editing to false
33
+ newConfig.visualizations[visualizationKey] = {
34
+ ...newConfig.visualizations[visualizationKey],
35
+ editing: false,
36
+ showEditorPanel: false
37
+ }
38
+
25
39
  dispatch({ type: 'SET_CONFIG', payload: newConfig })
26
40
 
27
41
  // the Widget component will do a data fetch if no data is available for the visualization
@@ -59,19 +73,21 @@ const Header = (props: HeaderProps) => {
59
73
  const configStringRef = useRef<string>()
60
74
 
61
75
  // Only update parent when config content actually changes (not just reference)
62
- const configString = JSON.stringify(convertStateToConfig())
63
- if (configStringRef.current !== configString) {
64
- configStringRef.current = configString
65
-
66
- // Emit the data in a regular JS event so it can be consumed by anything.
67
- const event = new CustomEvent('updateVizConfig', { detail: configString })
68
- window.dispatchEvent(event)
69
-
70
- // Pass up to Editor if needed
71
- if (setParentConfig) {
72
- setParentConfig(JSON.parse(configString))
76
+ useEffect(() => {
77
+ const configString = JSON.stringify(convertStateToConfig())
78
+ if (configStringRef.current !== configString) {
79
+ configStringRef.current = configString
80
+
81
+ // Emit the data in a regular JS event so it can be consumed by anything.
82
+ const event = new CustomEvent('updateVizConfig', { detail: configString })
83
+ window.dispatchEvent(event)
84
+
85
+ // Pass up to Editor if needed
86
+ if (setParentConfig) {
87
+ setParentConfig(JSON.parse(configString))
88
+ }
73
89
  }
74
- }
90
+ }, [config, setParentConfig])
75
91
 
76
92
  const handleCheck = e => {
77
93
  const { checked } = e.currentTarget
@@ -97,12 +113,27 @@ const Header = (props: HeaderProps) => {
97
113
  multidashboard
98
114
  </span>
99
115
  <br />
100
- <input
101
- type='text'
102
- placeholder='Enter Dashboard Name Here'
103
- defaultValue={config.dashboard?.title}
104
- onChange={e => changeConfigValue('dashboard', 'title', e.target.value)}
105
- />
116
+ <div style={{ display: 'flex', alignItems: 'flex-end', gap: '10px' }}>
117
+ <input
118
+ type='text'
119
+ placeholder='Enter Dashboard Name Here'
120
+ defaultValue={config.dashboard?.title}
121
+ onChange={e => changeConfigValue('dashboard', 'title', e.target.value)}
122
+ style={{ flex: 1 }}
123
+ />
124
+ <label style={{ display: 'flex', flexDirection: 'column', gap: '3px', fontSize: '0.85em' }}>
125
+ <span style={{ fontSize: '0.8em' }}>Title Style</span>
126
+ <select
127
+ value={config.dashboard.titleStyle}
128
+ onChange={e => changeConfigValue('dashboard', 'titleStyle', e.target.value)}
129
+ style={{ fontSize: '0.9em' }}
130
+ >
131
+ <option value='small'>Small</option>
132
+ <option value='large'>Large</option>
133
+ <option value='legacy'>Legacy</option>
134
+ </select>
135
+ </label>
136
+ </div>
106
137
  </div>
107
138
  )}
108
139
  {!subEditor && (
@@ -93,6 +93,68 @@ const VisualizationRow: React.FC<VizRowProps> = ({
93
93
  if (row.toggle) setToggled(0)
94
94
  }, [config.activeDashboard, index])
95
95
 
96
+ useEffect(() => {
97
+ // Trigger window resize event when tab changes to force chart re-render
98
+ if (row.toggle && toggledRow !== undefined) {
99
+ // Use setTimeout to ensure the d-none class has been removed first
100
+ setTimeout(() => {
101
+ window.dispatchEvent(new Event('resize'))
102
+ }, 50)
103
+ }
104
+ }, [toggledRow, row.toggle])
105
+
106
+ const setupTP5MinHeightEqualizer = (rowElement: Element, itemSelector: string) => {
107
+ const items = Array.from(rowElement.querySelectorAll(itemSelector)) as HTMLElement[]
108
+ if (items.length <= 1) return undefined
109
+
110
+ const equalizeHeights = () => {
111
+ items.forEach(item => {
112
+ item.style.minHeight = ''
113
+ })
114
+
115
+ let maxHeight = 0
116
+ items.forEach(item => {
117
+ const height = item.offsetHeight
118
+ if (height > maxHeight) maxHeight = height
119
+ })
120
+
121
+ if (maxHeight > 0) {
122
+ items.forEach(item => {
123
+ item.style.minHeight = `${maxHeight}px`
124
+ })
125
+ }
126
+ }
127
+
128
+ equalizeHeights()
129
+
130
+ const resizeObserver = new ResizeObserver(() => {
131
+ equalizeHeights()
132
+ })
133
+
134
+ items.forEach(item => {
135
+ resizeObserver.observe(item)
136
+ })
137
+
138
+ return () => resizeObserver.disconnect()
139
+ }
140
+
141
+ // Equalize TP5 callout title heights and TP5 gauge message blocks for like visualizations in the same row
142
+ useEffect(() => {
143
+ const rowElement = document.querySelector(`[data-row-index="${index}"]`)
144
+ if (!rowElement) return
145
+
146
+ const cleanups = [
147
+ setupTP5MinHeightEqualizer(rowElement, '.bite__style--tp5 .cdc-callout__heading'),
148
+ setupTP5MinHeightEqualizer(rowElement, '.waffle__style--tp5 .cdc-callout__heading'),
149
+ setupTP5MinHeightEqualizer(rowElement, '.gauge__style--tp5 .cdc-callout__heading'),
150
+ setupTP5MinHeightEqualizer(rowElement, '.gauge__style--tp5 .cove-gauge-chart__content')
151
+ ].filter(Boolean) as Array<() => void>
152
+
153
+ return () => {
154
+ cleanups.forEach(cleanup => cleanup())
155
+ }
156
+ }, [index, row, config, filteredDataOverride])
157
+
96
158
  const show = useMemo(() => {
97
159
  if (row.toggle) {
98
160
  return row.columns.map((col, i) => i === toggledRow)
@@ -166,13 +228,18 @@ const VisualizationRow: React.FC<VizRowProps> = ({
166
228
  }
167
229
 
168
230
  return (
169
- <div className={`row${row.equalHeight ? ' equal-height' : ''}${row.toggle ? ' toggle' : ''}`} key={`row__${index}`}>
231
+ <div
232
+ className={`row${row.equalHeight ? ' equal-height' : ''}${row.toggle ? ' toggle' : ''}`}
233
+ key={`row__${index}`}
234
+ data-row-index={index}
235
+ >
170
236
  {row.toggle && !inNoDataState && (
171
237
  <Toggle row={row} visualizations={config.visualizations} active={toggledRow} setToggled={setToggled} />
172
238
  )}
173
239
  {row.columns.map((col, colIndex) => {
174
240
  if (col.width) {
175
- if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`col col-${col.width}`}></div>
241
+ if (!col.widget)
242
+ return <div key={`row__${index}__col__${colIndex}`} className={`col-12 col-md-${col.width}`}></div>
176
243
 
177
244
  const visualizationConfig = getVizConfig(
178
245
  col.widget,
@@ -212,9 +279,14 @@ const VisualizationRow: React.FC<VizRowProps> = ({
212
279
  </a>
213
280
  )
214
281
 
282
+ // Markup-includes with external URLs don't depend on dashboard data
283
+ const isMarkupIncludeWithoutDataDependency =
284
+ type === 'markup-include' && !visualizationConfig.dataKey && !visualizationConfig.data?.length
285
+
215
286
  const hideVisualization =
216
287
  inNoDataState &&
217
288
  filterBehavior !== 'Apply Button' &&
289
+ !isMarkupIncludeWithoutDataDependency &&
218
290
  (type !== 'dashboardFilters' || applyButtonNotClicked(visualizationConfig))
219
291
 
220
292
  const shouldShow = row.toggle === undefined || (row.toggle && show[colIndex])
@@ -224,11 +296,10 @@ const VisualizationRow: React.FC<VizRowProps> = ({
224
296
  sharedFilterIndexes &&
225
297
  sharedFilterIndexes.filter(idx => config.dashboard.sharedFilters?.[idx]?.showDropdown === false).length ===
226
298
  sharedFilterIndexes.length
227
- const hasMarginBottom = !isLastRow && !hiddenDashboardFilters
228
299
 
229
300
  const vizWrapperClass = `col-12 col-md-${col.width}${!shouldShow ? ' d-none' : ''}${
230
- hideVisualization ? ' hide-parent-visualization' : hasMarginBottom ? ' mb-4' : ''
231
- }`
301
+ hideVisualization ? ' hide-parent-visualization' : ''
302
+ }${hiddenDashboardFilters ? ' hidden-dashboard-filters' : ''}`
232
303
  const link =
233
304
  config.table && config.table.show && config.datasets && table && table.showDataTableLink
234
305
  ? tableLink
@@ -46,25 +46,7 @@ const addVisualization = (type, subType) => {
46
46
  newVisualizationConfig.visualizationType = type
47
47
  break
48
48
  case 'markup-include':
49
- newVisualizationConfig.contentEditor = {
50
- inlineHTML: '<h2>Inline HTML</h2>',
51
- markupVariables: [],
52
- showHeader: true,
53
- srcUrl: '#example',
54
- title: 'Markup Include',
55
- useInlineHTML: true
56
- }
57
- newVisualizationConfig.theme = 'theme-blue'
58
- newVisualizationConfig.visual = {
59
- border: false,
60
- accent: false,
61
- background: false,
62
- hideBackgroundColor: false,
63
- borderColorTheme: false
64
- }
65
- newVisualizationConfig.showEditorPanel = true
66
49
  newVisualizationConfig.visualizationType = type
67
-
68
50
  break
69
51
  case 'dashboardFilters': {
70
52
  newVisualizationConfig.sharedFilterIndexes = []
@@ -81,7 +63,7 @@ const addVisualization = (type, subType) => {
81
63
 
82
64
  const VisualizationsPanel = () => {
83
65
  const [advancedEditing, setAdvancedEditing] = useState(false)
84
- const { config } = useContext(DashboardContext)
66
+ const { config, isEditor } = useContext(DashboardContext)
85
67
  const dispatch = useContext(DashboardDispatchContext)
86
68
  const loadConfig = incomingConfig => {
87
69
  const newConfig = !incomingConfig.multiDashboards
@@ -124,7 +106,7 @@ const VisualizationsPanel = () => {
124
106
  loadConfig={loadConfig}
125
107
  config={config}
126
108
  convertStateToConfig={() => undefined}
127
- stripConfig={stripConfig}
109
+ stripConfig={cfg => stripConfig(cfg, isEditor)}
128
110
  onExpandCollapse={() => {
129
111
  setAdvancedEditing(!advancedEditing)
130
112
  }}
@@ -123,7 +123,7 @@ const Widget = ({
123
123
  if (!widgetConfig) return
124
124
  dispatch({
125
125
  type: 'UPDATE_VISUALIZATION',
126
- payload: { vizKey: widgetConfig.uid as string, configureData: { editing: true } }
126
+ payload: { vizKey: widgetConfig.uid as string, configureData: { editing: true, showEditorPanel: true } }
127
127
  })
128
128
  loadSampleData()
129
129
  }
@@ -1,6 +1,7 @@
1
1
  export default {
2
2
  dashboard: {
3
3
  theme: 'theme-blue',
4
+ titleStyle: 'small',
4
5
  sharedFilters: []
5
6
  },
6
7
  rows: [[{ width: 12 }, {}, {}]],