@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.
- package/Dynamic_Data.md +66 -0
- package/dist/cdcdashboard-8NmHlKRI.es.js +15 -0
- package/dist/cdcdashboard-BPoPzKPz.es.js +6 -0
- package/dist/cdcdashboard-Cf9_fbQf.es.js +6 -0
- package/dist/{cdcdashboard-dgT_1dIT.es.js → cdcdashboard-DQ00cQCm.es.js} +1 -20
- package/dist/cdcdashboard-jiQQPkty.es.js +6 -0
- package/dist/cdcdashboard.js +83537 -86913
- package/examples/api-dashboard-data.json +272 -0
- package/examples/api-dashboard-years.json +11 -0
- package/examples/api-geographies-data.json +11 -0
- package/examples/default.json +522 -133
- package/examples/nested-dropdown.json +6985 -0
- package/examples/private/abc.json +467 -0
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/chronic-dash.json +1584 -0
- package/examples/private/dash.json +12696 -0
- package/examples/private/map-issue.json +2260 -0
- package/examples/private/mpinc-state-reports.json +2260 -0
- package/examples/private/npcr.json +1 -0
- package/examples/private/nwss/rsv.json +1240 -0
- package/examples/private/simple-dash.json +490 -0
- package/examples/private/test-dash.json +0 -0
- package/examples/private/test.json +125407 -0
- package/examples/private/test123.json +491 -0
- package/examples/private/timeline-data.json +4994 -0
- package/examples/private/timeline.json +1708 -0
- package/examples/test-api-filter-reset.json +8 -4
- package/examples/test-dashboard-simple.json +503 -0
- package/examples/tp5-gauges.json +196 -0
- package/examples/tp5-test.json +266 -0
- package/index.html +1 -30
- package/package.json +39 -40
- package/src/CdcDashboardComponent.tsx +18 -5
- package/src/_stories/Dashboard.DataSetup.stories.tsx +204 -0
- package/src/_stories/Dashboard.stories.tsx +407 -1
- package/src/_stories/_mock/dashboard-line-chart-angles.json +1030 -0
- package/src/_stories/_mock/filter-cascade.json +3350 -0
- package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
- package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
- package/src/_stories/_mock/parent-child-filters.json +233 -0
- package/src/_stories/_mock/tp5-test.json +267 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +20 -11
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +92 -38
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +56 -30
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +151 -10
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +11 -7
- package/src/components/DataDesignerModal.tsx +6 -1
- package/src/components/Header/Header.tsx +51 -20
- package/src/components/VisualizationRow.tsx +76 -5
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -20
- package/src/components/Widget/Widget.tsx +1 -1
- package/src/data/initial-state.js +1 -0
- package/src/helpers/addValuesToDashboardFilters.ts +30 -31
- package/src/helpers/apiFilterHelpers.ts +28 -32
- package/src/helpers/changeFilterActive.ts +67 -65
- package/src/helpers/formatConfigBeforeSave.ts +6 -5
- package/src/helpers/getUpdateConfig.ts +91 -91
- package/src/helpers/tests/addValuesToDashboardFilters.test.ts +141 -44
- package/src/helpers/tests/apiFilterHelpers.test.ts +523 -420
- package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
- package/src/helpers/updateChildFilters.ts +50 -27
- package/src/scss/main.scss +144 -1
- package/src/test/CdcDashboard.test.jsx +9 -4
- package/src/types/Dashboard.ts +1 -0
- package/src/types/FilterStyles.ts +8 -7
- package/src/types/SharedFilter.ts +13 -0
- package/vite.config.js +7 -1
- package/LICENSE +0 -201
- package/dist/cdcdashboard-BnB1QM5d.es.js +0 -361528
- package/dist/cdcdashboard-Ct2SB0vL.es.js +0 -231049
- package/dist/cdcdashboard-D6CG2-Hb.es.js +0 -39377
- package/dist/cdcdashboard-MXgURbdZ.es.js +0 -39194
- package/examples/private/DEV-10538.json +0 -407
- package/examples/private/DEV-11072.json +0 -7591
- package/examples/private/DEV-11405.json +0 -39112
- package/examples/private/delete.json +0 -32919
- 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
|
|
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
|
-
)
|
|
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:
|
|
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
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
300
|
+
?.map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
|
|
297
301
|
|
|
298
|
-
const displayNone = filters
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
configStringRef.current
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
setParentConfig
|
|
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
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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)
|
|
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' :
|
|
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
|
}
|