@cdc/dashboard 4.26.3 → 4.26.5
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/CONFIG.md +219 -0
- package/README.md +60 -20
- package/dist/cdcdashboard-CY9IcPSi.es.js +6 -0
- package/dist/cdcdashboard-DlpiY3fQ.es.js +4 -0
- package/dist/cdcdashboard.js +61559 -58048
- package/examples/__data__/data-2.json +6 -0
- package/examples/__data__/data.json +6 -0
- package/examples/dashboard-conditions-filters-incomplete.json +221 -0
- package/examples/dashboard-missing-datasets-multi.json +174 -0
- package/examples/dashboard-missing-datasets-single.json +121 -0
- package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
- package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
- package/examples/dashboard-stale-dataset-keys.json +181 -0
- package/examples/dashboard-tiered-filter-regression.json +190 -0
- package/examples/legend-issue.json +1 -1
- package/examples/minimal-example.json +34 -0
- package/examples/private/cfa-dashboard.json +651 -0
- package/examples/private/data-bite-wrap.json +6936 -0
- package/examples/private/dengue.json +4640 -0
- package/examples/private/link_to_file.json +16662 -0
- package/examples/private/multi-dash-fix.json +16963 -0
- package/examples/private/versions.json +41612 -0
- package/examples/sankey.json +3 -3
- package/examples/test-api-filter-reset.json +4 -4
- package/examples/tp5-test.json +86 -4
- package/examples/us-map-filter-example.json +1074 -0
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +6 -2
- package/src/CdcDashboardComponent.tsx +179 -88
- package/src/DashboardCopyPasteContext.test.tsx +33 -0
- package/src/DashboardCopyPasteContext.tsx +48 -0
- package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
- package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
- package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
- package/src/_stories/Dashboard.smoke.stories.tsx +33 -0
- package/src/_stories/Dashboard.stories.tsx +337 -2
- package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
- package/src/_stories/_mock/dashboard-data-driven-colors.json +171 -0
- package/src/_stories/_mock/tp5-test.json +86 -5
- package/src/components/Column.test.tsx +176 -0
- package/src/components/Column.tsx +214 -13
- package/src/components/DashboardConditionModal.test.tsx +420 -0
- package/src/components/DashboardConditionModal.tsx +367 -0
- package/src/components/DashboardConditionSummary.tsx +59 -0
- package/src/components/DashboardEditors.tsx +23 -0
- package/src/components/DashboardFilters/DashboardFilters.test.tsx +267 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +193 -172
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +46 -6
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/APIModal.tsx +5 -3
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +59 -58
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +304 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +43 -36
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +2 -2
- package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
- package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
- package/src/components/DataDesignerModal.tsx +2 -1
- package/src/components/ExpandCollapseButtons.tsx +6 -4
- package/src/components/Grid.tsx +12 -7
- package/src/components/Header/Header.tsx +36 -17
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +141 -140
- package/src/components/Row.test.tsx +228 -0
- package/src/components/Row.tsx +104 -28
- package/src/components/VisualizationRow.test.tsx +396 -0
- package/src/components/VisualizationRow.tsx +177 -51
- package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
- package/src/components/Widget/Widget.test.tsx +218 -0
- package/src/components/Widget/Widget.tsx +123 -20
- package/src/components/Widget/widget.styles.css +58 -14
- package/src/components/dashboard-condition-modal.css +76 -0
- package/src/components/dashboard-condition-summary.css +87 -0
- package/src/data/initial-state.js +1 -0
- package/src/helpers/addValuesToDashboardFilters.ts +3 -5
- package/src/helpers/addVisualization.ts +17 -4
- package/src/helpers/cloneDashboardWidget.ts +127 -0
- package/src/helpers/dashboardColumnWidgets.ts +99 -0
- package/src/helpers/dashboardConditionUi.ts +47 -0
- package/src/helpers/dashboardConditions.ts +200 -0
- package/src/helpers/dashboardFilterTargets.ts +156 -0
- package/src/helpers/filterData.ts +4 -9
- package/src/helpers/filterVisibility.ts +20 -0
- package/src/helpers/formatConfigBeforeSave.ts +2 -2
- package/src/helpers/getFilteredData.ts +18 -5
- package/src/helpers/getUpdateConfig.ts +43 -12
- package/src/helpers/getVizRowColumnLocator.ts +11 -1
- package/src/helpers/iconHash.tsx +9 -3
- package/src/helpers/mapDataToConfig.ts +31 -29
- package/src/helpers/reloadURLHelpers.ts +25 -5
- package/src/helpers/removeDashboardFilter.ts +33 -33
- package/src/helpers/tests/addVisualization.test.ts +53 -9
- package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
- package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
- package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
- package/src/helpers/tests/dashboardConditions.test.ts +428 -0
- package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
- package/src/helpers/tests/getFilteredData.test.ts +265 -86
- package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
- package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
- package/src/index.tsx +6 -3
- package/src/scss/grid.scss +281 -22
- package/src/scss/main.scss +215 -64
- package/src/store/dashboard.actions.ts +17 -4
- package/src/store/dashboard.reducer.test.ts +538 -0
- package/src/store/dashboard.reducer.ts +136 -22
- package/src/test/CdcDashboard.test.jsx +24 -0
- package/src/test/CdcDashboard.test.tsx +148 -0
- package/src/test/CdcDashboardComponent.test.tsx +935 -2
- package/src/types/ConfigRow.ts +15 -0
- package/src/types/DashboardFilters.ts +4 -0
- package/src/types/SharedFilter.ts +2 -0
- package/tests/fixtures/dashboard-config-with-metadata.json +1 -1
- package/dist/cdcdashboard-vr9HZwRt.es.js +0 -6
- package/examples/DEV-6574.json +0 -2224
- package/examples/api-dashboard-data.json +0 -272
- package/examples/api-dashboard-years.json +0 -11
- package/examples/api-geographies-data.json +0 -11
- package/examples/chart-data.json +0 -5409
- package/examples/custom/css/respiratory.css +0 -236
- package/examples/custom/js/respiratory.js +0 -242
- package/examples/default-data.json +0 -368
- package/examples/default-filter-control.json +0 -209
- package/examples/default-multi-dataset-shared-filter.json +0 -1729
- package/examples/default-multi-dataset.json +0 -506
- package/examples/ed-visits-county-file.json +0 -402
- package/examples/filters/Alabama.json +0 -72
- package/examples/filters/Alaska.json +0 -1737
- package/examples/filters/Arkansas.json +0 -4713
- package/examples/filters/California.json +0 -212
- package/examples/filters/Colorado.json +0 -1500
- package/examples/filters/Connecticut.json +0 -559
- package/examples/filters/Delaware.json +0 -63
- package/examples/filters/DistrictofColumbia.json +0 -63
- package/examples/filters/Florida.json +0 -4217
- package/examples/filters/States.json +0 -146
- package/examples/state-level.json +0 -90136
- package/examples/state-points.json +0 -10474
- package/examples/temp-example-data.json +0 -130
- package/examples/test-dashboard-simple.json +0 -503
- package/examples/test-example.json +0 -752
- package/examples/test-file.json +0 -147
- package/examples/test.json +0 -752
- package/examples/testing.json +0 -94456
- /package/examples/{data → __data__}/data-with-metadata.json +0 -0
- /package/examples/{legend-issue-data.json → __data__/legend-issue-data.json} +0 -0
- /package/examples/api-test/{categories.json → __data__/categories.json} +0 -0
- /package/examples/api-test/{chart-data.json → __data__/chart-data.json} +0 -0
- /package/examples/api-test/{topics.json → __data__/topics.json} +0 -0
- /package/examples/api-test/{years.json → __data__/years.json} +0 -0
- /package/src/_stories/{Dashboard.Pages.stories.tsx → Dashboard.Pages.smoke.stories.tsx} +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { expect, userEvent, within } from 'storybook/test'
|
|
3
|
+
import {
|
|
4
|
+
assertVisualizationRendered,
|
|
5
|
+
getDisplayValue,
|
|
6
|
+
performAndAssert,
|
|
7
|
+
waitForOptionsToPopulate
|
|
8
|
+
} from '@cdc/core/helpers/testing'
|
|
9
|
+
import Dashboard from '../CdcDashboard'
|
|
10
|
+
import StaleDatasetKeysConfig from '../../examples/dashboard-stale-dataset-keys.json'
|
|
11
|
+
import MissingDatasetsSingleConfig from '../../examples/dashboard-missing-datasets-single.json'
|
|
12
|
+
import MissingDatasetsMultiConfig from '../../examples/dashboard-missing-datasets-multi.json'
|
|
13
|
+
import TieredFilterConfig from '../../examples/dashboard-tiered-filter-regression.json'
|
|
14
|
+
import MultiDashboardVersionConfig from '../../examples/dashboard-multi-dashboard-version-regression.json'
|
|
15
|
+
|
|
16
|
+
const meta: Meta<typeof Dashboard> = {
|
|
17
|
+
title: 'Components/Pages/Dashboard/Regression Smoke',
|
|
18
|
+
component: Dashboard
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof Dashboard>
|
|
23
|
+
|
|
24
|
+
const expectNoCrashText = (canvasElement: HTMLElement) => {
|
|
25
|
+
expect(canvasElement.textContent).not.toContain('Cannot read properties of undefined')
|
|
26
|
+
expect(canvasElement.textContent).not.toContain('Something went wrong')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const Stale_Dataset_Keys_Are_Skipped_Safely: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
config: StaleDatasetKeysConfig,
|
|
32
|
+
isEditor: false
|
|
33
|
+
},
|
|
34
|
+
play: async ({ canvasElement }) => {
|
|
35
|
+
await assertVisualizationRendered(canvasElement)
|
|
36
|
+
expectNoCrashText(canvasElement)
|
|
37
|
+
expect(canvasElement.textContent).toContain('How to use this fixture')
|
|
38
|
+
expect(canvasElement.textContent).toContain('Valid viz.dataKey')
|
|
39
|
+
expect(getDisplayValue(canvasElement)).toContain('123')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const Missing_Datasets_Single_Dashboard_Loads_Safely: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
config: MissingDatasetsSingleConfig,
|
|
46
|
+
isEditor: false
|
|
47
|
+
},
|
|
48
|
+
play: async ({ canvasElement }) => {
|
|
49
|
+
await assertVisualizationRendered(canvasElement)
|
|
50
|
+
expectNoCrashText(canvasElement)
|
|
51
|
+
expect(canvasElement.textContent).toContain('Fixture notes')
|
|
52
|
+
expect(canvasElement.textContent).toContain('Missing dataset')
|
|
53
|
+
expect(canvasElement.textContent).toContain('Empty dataset')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Missing_Datasets_Multi_Dashboard_Loads_Safely: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
config: MissingDatasetsMultiConfig,
|
|
60
|
+
isEditor: false
|
|
61
|
+
},
|
|
62
|
+
play: async ({ canvasElement }) => {
|
|
63
|
+
const canvas = within(canvasElement)
|
|
64
|
+
const user = userEvent.setup()
|
|
65
|
+
|
|
66
|
+
await assertVisualizationRendered(canvasElement)
|
|
67
|
+
expectNoCrashText(canvasElement)
|
|
68
|
+
expect(canvasElement.textContent).toContain('This tab has no datasets object entries for its chart.')
|
|
69
|
+
|
|
70
|
+
const emptyDatasetTab = canvas.getByRole('link', { name: 'Empty Dataset' })
|
|
71
|
+
await user.click(emptyDatasetTab)
|
|
72
|
+
|
|
73
|
+
await performAndAssert(
|
|
74
|
+
'Switch to multi-dashboard tab with empty dataset',
|
|
75
|
+
() => canvasElement.textContent || '',
|
|
76
|
+
async () => {},
|
|
77
|
+
(_before, after) => after.includes('This sub-dashboard has a datasets object, but the dataset is empty.')
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
expectNoCrashText(canvasElement)
|
|
81
|
+
expect(canvasElement.textContent).toContain('Please complete your selection to continue.')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const Tiered_Filtering_Applies_Sequentially: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
config: TieredFilterConfig,
|
|
88
|
+
isEditor: false
|
|
89
|
+
},
|
|
90
|
+
play: async ({ canvasElement }) => {
|
|
91
|
+
const canvas = within(canvasElement)
|
|
92
|
+
const user = userEvent.setup()
|
|
93
|
+
|
|
94
|
+
const regionFilter = (await canvas.findByLabelText('Region', { selector: 'select' })) as HTMLSelectElement
|
|
95
|
+
const categoryFilter = (await canvas.findByLabelText('Category', { selector: 'select' })) as HTMLSelectElement
|
|
96
|
+
const detailFilter = (await canvas.findByLabelText('Detail', { selector: 'select' })) as HTMLSelectElement
|
|
97
|
+
|
|
98
|
+
const getOptions = (select: HTMLSelectElement) =>
|
|
99
|
+
Array.from(select.options)
|
|
100
|
+
.map(option => option.value)
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
|
|
103
|
+
const getState = () => ({
|
|
104
|
+
regionSelected: regionFilter.value,
|
|
105
|
+
categorySelected: categoryFilter.value,
|
|
106
|
+
detailSelected: detailFilter.value,
|
|
107
|
+
categoryOptions: getOptions(categoryFilter),
|
|
108
|
+
detailOptions: getOptions(detailFilter),
|
|
109
|
+
svgCount: canvasElement.querySelectorAll('svg').length,
|
|
110
|
+
noDataVisible: !!canvas.queryByText('No Data Available')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
await waitForOptionsToPopulate(regionFilter, 3)
|
|
114
|
+
await waitForOptionsToPopulate(categoryFilter, 3)
|
|
115
|
+
await waitForOptionsToPopulate(detailFilter, 3)
|
|
116
|
+
|
|
117
|
+
const initialState = getState()
|
|
118
|
+
expect(initialState.regionSelected).toBe('North')
|
|
119
|
+
expect(initialState.categorySelected).toBe('Alpha')
|
|
120
|
+
expect(initialState.detailSelected).toBe('One')
|
|
121
|
+
expect(initialState.svgCount).toBeGreaterThan(0)
|
|
122
|
+
expect(initialState.noDataVisible).toBe(false)
|
|
123
|
+
|
|
124
|
+
await performAndAssert(
|
|
125
|
+
'Change tier-1 filter and repopulate lower tiers',
|
|
126
|
+
getState,
|
|
127
|
+
async () => await user.selectOptions(regionFilter, ['South']),
|
|
128
|
+
(_before, after) =>
|
|
129
|
+
after.regionSelected === 'South' &&
|
|
130
|
+
after.categoryOptions.includes('Alpha') &&
|
|
131
|
+
after.categoryOptions.includes('Beta') &&
|
|
132
|
+
after.categoryOptions.includes(after.categorySelected) &&
|
|
133
|
+
after.detailOptions.includes(after.detailSelected) &&
|
|
134
|
+
after.svgCount > 0 &&
|
|
135
|
+
!after.noDataVisible
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
await performAndAssert(
|
|
139
|
+
'Change tier-2 filter and observe the intermediate no-data state',
|
|
140
|
+
getState,
|
|
141
|
+
async () => await user.selectOptions(categoryFilter, ['Beta']),
|
|
142
|
+
(_before, after) =>
|
|
143
|
+
after.regionSelected === 'South' &&
|
|
144
|
+
after.categorySelected === 'Beta' &&
|
|
145
|
+
after.detailSelected === 'One' &&
|
|
146
|
+
after.noDataVisible
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
await performAndAssert(
|
|
150
|
+
'Change tier-3 filter to restore data after tier-2 change',
|
|
151
|
+
getState,
|
|
152
|
+
async () => await user.selectOptions(detailFilter, ['Two']),
|
|
153
|
+
(_before, after) =>
|
|
154
|
+
after.regionSelected === 'South' &&
|
|
155
|
+
after.categorySelected === 'Beta' &&
|
|
156
|
+
after.detailSelected === 'Two' &&
|
|
157
|
+
after.svgCount > 0 &&
|
|
158
|
+
!after.noDataVisible
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const Multi_Dashboard_Versioning_Remains_Stable: Story = {
|
|
164
|
+
args: {
|
|
165
|
+
config: MultiDashboardVersionConfig,
|
|
166
|
+
isEditor: false
|
|
167
|
+
},
|
|
168
|
+
play: async ({ canvasElement }) => {
|
|
169
|
+
const canvas = within(canvasElement)
|
|
170
|
+
const user = userEvent.setup()
|
|
171
|
+
|
|
172
|
+
await assertVisualizationRendered(canvasElement)
|
|
173
|
+
expectNoCrashText(canvasElement)
|
|
174
|
+
|
|
175
|
+
await user.click(canvas.getByRole('link', { name: 'Explicit Version' }))
|
|
176
|
+
|
|
177
|
+
await performAndAssert(
|
|
178
|
+
'Switch to explicit-version sub-dashboard',
|
|
179
|
+
() => canvasElement.textContent || '',
|
|
180
|
+
async () => {},
|
|
181
|
+
(_before, after) => after.includes('Because this sub-dashboard is already at')
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
await user.click(canvas.getByRole('link', { name: 'Fallback Version' }))
|
|
185
|
+
|
|
186
|
+
await performAndAssert(
|
|
187
|
+
'Switch to fallback-version sub-dashboard',
|
|
188
|
+
() => canvasElement.textContent || '',
|
|
189
|
+
async () => {},
|
|
190
|
+
(_before, after) => after.includes('This tab intentionally omits its own version.')
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
expectNoCrashText(canvasElement)
|
|
194
|
+
expect(canvasElement.textContent).toContain('Fallback version tab')
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { within, userEvent } from 'storybook/test'
|
|
3
|
+
import cloneDeep from 'lodash/cloneDeep'
|
|
4
|
+
import Dashboard from '../CdcDashboard'
|
|
5
|
+
import SingleStateDashboardFilters from './_mock/single-state-dashboard-filters.json'
|
|
6
|
+
import {
|
|
7
|
+
assertVisualizationRendered,
|
|
8
|
+
performAndAssert,
|
|
9
|
+
waitForOptionsToPopulate,
|
|
10
|
+
waitForPresence
|
|
11
|
+
} from '@cdc/core/helpers/testing'
|
|
12
|
+
|
|
13
|
+
type Story = StoryObj<typeof Dashboard>
|
|
14
|
+
|
|
15
|
+
const meta: Meta<typeof Dashboard> = {
|
|
16
|
+
title: 'Components/Pages/Dashboard/Zoom',
|
|
17
|
+
component: Dashboard,
|
|
18
|
+
parameters: {
|
|
19
|
+
layout: 'fullscreen'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default meta
|
|
24
|
+
|
|
25
|
+
const SingleStateDashboardZoomResetConfig = cloneDeep(SingleStateDashboardFilters)
|
|
26
|
+
SingleStateDashboardZoomResetConfig.dashboard.title = 'Single-State Shared Filter Zoom Reset'
|
|
27
|
+
SingleStateDashboardZoomResetConfig.dashboard.sharedFilters[0].key = 'State'
|
|
28
|
+
SingleStateDashboardZoomResetConfig.dashboard.sharedFilters[0].active = 'California'
|
|
29
|
+
SingleStateDashboardZoomResetConfig.visualizations.map1721943918271.general.title =
|
|
30
|
+
'Single-State Shared Filter Zoom Reset'
|
|
31
|
+
SingleStateDashboardZoomResetConfig.visualizations.map1721943918271.general.filterControlsStatesPicked = 'State'
|
|
32
|
+
SingleStateDashboardZoomResetConfig.visualizations.map1721943918271.general.statesPicked = [
|
|
33
|
+
{
|
|
34
|
+
fipsCode: '06',
|
|
35
|
+
stateName: 'California'
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
delete SingleStateDashboardZoomResetConfig.visualizations.map1721943918271.general.filterControlsStatePicked
|
|
39
|
+
delete SingleStateDashboardZoomResetConfig.visualizations.map1721943918271.general.statePicked
|
|
40
|
+
|
|
41
|
+
const readZoomTransform = (canvasElement: HTMLElement) => {
|
|
42
|
+
const zoomLayer = canvasElement.querySelector('svg > g > g[transform]') as SVGGElement | null
|
|
43
|
+
return zoomLayer?.getAttribute('transform') || ''
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const SingleStateSharedFilterReset: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
config: SingleStateDashboardZoomResetConfig,
|
|
49
|
+
isEditor: false
|
|
50
|
+
},
|
|
51
|
+
parameters: {
|
|
52
|
+
docs: {
|
|
53
|
+
description: {
|
|
54
|
+
story:
|
|
55
|
+
'Dashboard zoom regression story for DEV-13086. Zoom the single-state map, then change the shared State filter. The map should refit to the new single state instead of preserving the prior zoom.'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
play: async ({ canvasElement }) => {
|
|
60
|
+
const canvas = within(canvasElement)
|
|
61
|
+
|
|
62
|
+
await assertVisualizationRendered(canvasElement)
|
|
63
|
+
|
|
64
|
+
const stateFilter = canvas.getByLabelText('State', { selector: 'select' }) as HTMLSelectElement
|
|
65
|
+
await waitForOptionsToPopulate(stateFilter, 2)
|
|
66
|
+
await waitForPresence('button[aria-label="Zoom In"]', canvasElement)
|
|
67
|
+
|
|
68
|
+
const zoomInButton = canvas.getByLabelText('Zoom In')
|
|
69
|
+
|
|
70
|
+
await performAndAssert(
|
|
71
|
+
'Single-state dashboard map zooms in',
|
|
72
|
+
() => readZoomTransform(canvasElement),
|
|
73
|
+
async () => {
|
|
74
|
+
await userEvent.click(zoomInButton)
|
|
75
|
+
},
|
|
76
|
+
(before, after) => before !== after && !after.includes('scale(1)')
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
await performAndAssert(
|
|
80
|
+
'Shared filter change refits single-state zoom',
|
|
81
|
+
() => readZoomTransform(canvasElement),
|
|
82
|
+
async () => {
|
|
83
|
+
await userEvent.selectOptions(stateFilter, ['Florida'])
|
|
84
|
+
},
|
|
85
|
+
(before, after) => before !== after && after.includes('scale(1)')
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { expect } from 'storybook/test'
|
|
3
|
+
import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
|
|
4
|
+
import Dashboard from '../CdcDashboard'
|
|
5
|
+
import MinimalExampleConfig from '../../examples/minimal-example.json'
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Dashboard> = {
|
|
8
|
+
title: 'Components/Pages/Dashboard',
|
|
9
|
+
component: Dashboard
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof Dashboard>
|
|
14
|
+
|
|
15
|
+
export const Dashboard_Minimal_Config: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
config: MinimalExampleConfig,
|
|
18
|
+
isEditor: false
|
|
19
|
+
},
|
|
20
|
+
parameters: {
|
|
21
|
+
docs: {
|
|
22
|
+
description: {
|
|
23
|
+
story:
|
|
24
|
+
'Minimum working consumer config. This story validates the source-of-truth minimal example used by the package README and CONFIG reference.'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
play: async ({ canvasElement }) => {
|
|
29
|
+
await assertVisualizationRendered(canvasElement)
|
|
30
|
+
expect(canvasElement.textContent).toContain('Dashboard Example')
|
|
31
|
+
expect(canvasElement.textContent).toContain('Minimal dashboard example')
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -44,13 +44,193 @@ import GalleryDataBiteDashboard from './_mock/gallery-data-bite-dashboard.json'
|
|
|
44
44
|
import TP5TestConfig from './_mock/tp5-test.json'
|
|
45
45
|
import LineChartAnglesConfig from './_mock/dashboard-line-chart-angles.json'
|
|
46
46
|
import TabSimpleFilterConfig from './_mock/tab-simple-filter.json'
|
|
47
|
+
import DataDrivenColorsConfig from './_mock/dashboard-data-driven-colors.json'
|
|
47
48
|
|
|
48
49
|
// Dashboard Filter Updates for Ascending, Descending, and Custom Order
|
|
49
50
|
import DashboardFilterAsc from './_mock/dashboard-filter-asc.json'
|
|
50
51
|
const DashboardFilterDesc = cloneDeep(DashboardFilterAsc)
|
|
51
52
|
const DashboardFilterCust = cloneDeep(DashboardFilterAsc)
|
|
53
|
+
const NestedParentChildFiltersSubgroupOnly = cloneDeep(NestedParentChildFilters)
|
|
52
54
|
DashboardFilterDesc.dashboard.sharedFilters[0].order = 'desc'
|
|
53
55
|
DashboardFilterCust.dashboard.sharedFilters[0].order = 'cust'
|
|
56
|
+
NestedParentChildFiltersSubgroupOnly.dashboard.sharedFilters[1].displaySubgroupingOnly = true
|
|
57
|
+
|
|
58
|
+
const DashboardConditionsConfig: Config = {
|
|
59
|
+
type: 'dashboard',
|
|
60
|
+
version: '4.26.4',
|
|
61
|
+
dashboard: {
|
|
62
|
+
theme: 'theme-blue',
|
|
63
|
+
title: 'Dashboard Conditions',
|
|
64
|
+
titleStyle: 'small',
|
|
65
|
+
sharedFilters: [
|
|
66
|
+
{
|
|
67
|
+
key: 'Required Selection',
|
|
68
|
+
type: 'datafilter',
|
|
69
|
+
filterStyle: 'dropdown',
|
|
70
|
+
columnName: 'selection',
|
|
71
|
+
showDropdown: true,
|
|
72
|
+
usedBy: ['markup-filters-incomplete'],
|
|
73
|
+
values: ['Ready'],
|
|
74
|
+
resetLabel: 'Select a value',
|
|
75
|
+
active: 'Select a value',
|
|
76
|
+
order: 'asc',
|
|
77
|
+
parents: []
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: 'Availability',
|
|
81
|
+
type: 'datafilter',
|
|
82
|
+
filterStyle: 'dropdown',
|
|
83
|
+
columnName: 'availability',
|
|
84
|
+
showDropdown: true,
|
|
85
|
+
usedBy: [2],
|
|
86
|
+
values: ['Show', 'Hide'],
|
|
87
|
+
active: 'Show',
|
|
88
|
+
order: 'asc',
|
|
89
|
+
parents: []
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: 'Region',
|
|
93
|
+
type: 'datafilter',
|
|
94
|
+
filterStyle: 'dropdown',
|
|
95
|
+
columnName: 'region',
|
|
96
|
+
showDropdown: true,
|
|
97
|
+
usedBy: ['markup-condition-a'],
|
|
98
|
+
values: ['East', 'West'],
|
|
99
|
+
active: 'East',
|
|
100
|
+
order: 'asc',
|
|
101
|
+
parents: []
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
data: [],
|
|
106
|
+
datasets: {
|
|
107
|
+
'visual-data': {
|
|
108
|
+
data: [{ title: 'Visualization dataset' }]
|
|
109
|
+
},
|
|
110
|
+
'row-condition-data': {
|
|
111
|
+
data: [{ availability: 'Show' }]
|
|
112
|
+
},
|
|
113
|
+
'column-condition-data': {
|
|
114
|
+
data: [
|
|
115
|
+
{ region: 'East', visibility: 1 },
|
|
116
|
+
{ region: 'West', visibility: 0 }
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
rows: [
|
|
121
|
+
{
|
|
122
|
+
columns: [{ width: 12, widget: 'dashboard-filters-conditions' }],
|
|
123
|
+
expandCollapseAllButtons: false
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
columns: [
|
|
127
|
+
{
|
|
128
|
+
width: 12,
|
|
129
|
+
conditionalWidgets: [
|
|
130
|
+
{
|
|
131
|
+
widget: 'markup-filters-incomplete',
|
|
132
|
+
dashboardCondition: {
|
|
133
|
+
id: 'filters-incomplete-story',
|
|
134
|
+
operator: 'filtersIncomplete'
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
expandCollapseAllButtons: false
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
dashboardCondition: {
|
|
144
|
+
id: 'row-condition-story',
|
|
145
|
+
datasetKey: 'row-condition-data',
|
|
146
|
+
operator: 'hasData'
|
|
147
|
+
},
|
|
148
|
+
columns: [
|
|
149
|
+
{
|
|
150
|
+
width: 6,
|
|
151
|
+
conditionalWidgets: [
|
|
152
|
+
{
|
|
153
|
+
widget: 'markup-condition-a',
|
|
154
|
+
dashboardCondition: {
|
|
155
|
+
id: 'column-condition-story',
|
|
156
|
+
datasetKey: 'column-condition-data',
|
|
157
|
+
operator: 'columnHasAnyValue',
|
|
158
|
+
columnName: 'visibility',
|
|
159
|
+
values: ['1']
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
width: 6,
|
|
166
|
+
widget: 'markup-condition-b'
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
expandCollapseAllButtons: false
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
visualizations: {
|
|
173
|
+
'dashboard-filters-conditions': {
|
|
174
|
+
uid: 'dashboard-filters-conditions',
|
|
175
|
+
type: 'dashboardFilters',
|
|
176
|
+
visualizationType: 'dashboardFilters',
|
|
177
|
+
sharedFilterIndexes: [0, 1, 2],
|
|
178
|
+
filterBehavior: 'Filter Change',
|
|
179
|
+
filters: [],
|
|
180
|
+
autoLoad: true
|
|
181
|
+
},
|
|
182
|
+
'markup-filters-incomplete': {
|
|
183
|
+
uid: 'markup-filters-incomplete',
|
|
184
|
+
type: 'markup-include',
|
|
185
|
+
visualizationType: 'markup-include',
|
|
186
|
+
filterBehavior: 'Filter Change',
|
|
187
|
+
theme: 'theme-blue',
|
|
188
|
+
contentEditor: {
|
|
189
|
+
inlineHTML:
|
|
190
|
+
'<p>This authored module appears because <strong>Required Selection</strong> starts incomplete. Choose Ready to hide it.</p>',
|
|
191
|
+
showHeader: true,
|
|
192
|
+
srcUrl: '',
|
|
193
|
+
title: 'Filters Incomplete Condition',
|
|
194
|
+
useInlineHTML: true
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
'markup-condition-a': {
|
|
198
|
+
uid: 'markup-condition-a',
|
|
199
|
+
type: 'markup-include',
|
|
200
|
+
visualizationType: 'markup-include',
|
|
201
|
+
dataKey: 'visual-data',
|
|
202
|
+
filterBehavior: 'Filter Change',
|
|
203
|
+
theme: 'theme-blue',
|
|
204
|
+
contentEditor: {
|
|
205
|
+
inlineHTML:
|
|
206
|
+
'<p>This column stays visible while <strong>Region</strong> is East and hides while keeping its width when Region is West.</p>',
|
|
207
|
+
showHeader: true,
|
|
208
|
+
srcUrl: '',
|
|
209
|
+
title: 'Component-Level Condition',
|
|
210
|
+
useInlineHTML: true
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
'markup-condition-b': {
|
|
214
|
+
uid: 'markup-condition-b',
|
|
215
|
+
type: 'markup-include',
|
|
216
|
+
visualizationType: 'markup-include',
|
|
217
|
+
dataKey: 'visual-data',
|
|
218
|
+
filterBehavior: 'Filter Change',
|
|
219
|
+
theme: 'theme-blue',
|
|
220
|
+
contentEditor: {
|
|
221
|
+
inlineHTML:
|
|
222
|
+
'<p>This companion column stays rendered so you can see row-level hiding separately from component-level hiding.</p>',
|
|
223
|
+
showHeader: true,
|
|
224
|
+
srcUrl: '',
|
|
225
|
+
title: 'Always Visible Companion',
|
|
226
|
+
useInlineHTML: true
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
table: {
|
|
231
|
+
show: false
|
|
232
|
+
}
|
|
233
|
+
}
|
|
54
234
|
|
|
55
235
|
// On DashboardFilterCust change the sharedFilters[0].values and orderedValues to be in a custom order
|
|
56
236
|
const customOrder = ['American Samoa', 'Alaska', 'Alabama', 'Arizona', 'Arkansas']
|
|
@@ -112,6 +292,123 @@ export const Example_3: Story = {
|
|
|
112
292
|
}
|
|
113
293
|
}
|
|
114
294
|
|
|
295
|
+
export const Dashboard_Conditions: Story = {
|
|
296
|
+
args: {
|
|
297
|
+
config: DashboardConditionsConfig,
|
|
298
|
+
isEditor: false
|
|
299
|
+
},
|
|
300
|
+
play: async ({ canvasElement }) => {
|
|
301
|
+
const canvas = within(canvasElement)
|
|
302
|
+
const user = userEvent.setup()
|
|
303
|
+
|
|
304
|
+
const incompleteTitle = 'Filters Incomplete Condition'
|
|
305
|
+
const componentTitle = 'Component-Level Condition'
|
|
306
|
+
const companionTitle = 'Always Visible Companion'
|
|
307
|
+
const legacyIncompleteMessage = 'Please complete your selection to continue.'
|
|
308
|
+
|
|
309
|
+
const availabilityFilter = (await canvas.findByLabelText('Availability', {
|
|
310
|
+
selector: 'select'
|
|
311
|
+
})) as HTMLSelectElement
|
|
312
|
+
const regionFilter = (await canvas.findByLabelText('Region', { selector: 'select' })) as HTMLSelectElement
|
|
313
|
+
const requiredSelectionFilter = (await canvas.findByLabelText('Required Selection', {
|
|
314
|
+
selector: 'select'
|
|
315
|
+
})) as HTMLSelectElement
|
|
316
|
+
|
|
317
|
+
await waitForOptionsToPopulate(requiredSelectionFilter, 2)
|
|
318
|
+
|
|
319
|
+
const getState = () => {
|
|
320
|
+
const visibleText = canvasElement.innerText
|
|
321
|
+
return {
|
|
322
|
+
availabilitySelected: availabilityFilter.value,
|
|
323
|
+
regionSelected: regionFilter.value,
|
|
324
|
+
requiredSelectionSelected: requiredSelectionFilter.value,
|
|
325
|
+
incompleteVisible: visibleText.includes(incompleteTitle),
|
|
326
|
+
componentVisible: visibleText.includes(componentTitle),
|
|
327
|
+
companionVisible: visibleText.includes(companionTitle),
|
|
328
|
+
legacyIncompleteVisible: visibleText.includes(legacyIncompleteMessage)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const initialState = getState()
|
|
333
|
+
expect(initialState.incompleteVisible).toBe(true)
|
|
334
|
+
expect(initialState.componentVisible).toBe(false)
|
|
335
|
+
expect(initialState.companionVisible).toBe(false)
|
|
336
|
+
expect(initialState.legacyIncompleteVisible).toBe(false)
|
|
337
|
+
|
|
338
|
+
await performAndAssert(
|
|
339
|
+
'Complete required filter -> row and component conditions render',
|
|
340
|
+
getState,
|
|
341
|
+
async () => await user.selectOptions(requiredSelectionFilter, ['Ready']),
|
|
342
|
+
(_before, after) =>
|
|
343
|
+
after.requiredSelectionSelected === 'Ready' &&
|
|
344
|
+
!after.incompleteVisible &&
|
|
345
|
+
after.componentVisible &&
|
|
346
|
+
after.companionVisible &&
|
|
347
|
+
!after.legacyIncompleteVisible
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
await performAndAssert(
|
|
351
|
+
'Select Region=West -> component-level condition hides only the conditioned component',
|
|
352
|
+
getState,
|
|
353
|
+
async () => await user.selectOptions(regionFilter, ['West']),
|
|
354
|
+
(_before, after) =>
|
|
355
|
+
after.regionSelected === 'West' &&
|
|
356
|
+
!after.incompleteVisible &&
|
|
357
|
+
!after.componentVisible &&
|
|
358
|
+
after.companionVisible &&
|
|
359
|
+
!after.legacyIncompleteVisible
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
await performAndAssert(
|
|
363
|
+
'Select Region=East -> component-level condition shows the conditioned component',
|
|
364
|
+
getState,
|
|
365
|
+
async () => await user.selectOptions(regionFilter, ['East']),
|
|
366
|
+
(_before, after) =>
|
|
367
|
+
after.regionSelected === 'East' &&
|
|
368
|
+
!after.incompleteVisible &&
|
|
369
|
+
after.componentVisible &&
|
|
370
|
+
after.companionVisible &&
|
|
371
|
+
!after.legacyIncompleteVisible
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
await performAndAssert(
|
|
375
|
+
'Select Availability=Hide -> row-level condition hides the full conditioned row',
|
|
376
|
+
getState,
|
|
377
|
+
async () => await user.selectOptions(availabilityFilter, ['Hide']),
|
|
378
|
+
(_before, after) =>
|
|
379
|
+
after.availabilitySelected === 'Hide' &&
|
|
380
|
+
!after.incompleteVisible &&
|
|
381
|
+
!after.componentVisible &&
|
|
382
|
+
!after.companionVisible &&
|
|
383
|
+
!after.legacyIncompleteVisible
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
await performAndAssert(
|
|
387
|
+
'Select Availability=Show -> row-level condition shows the full conditioned row',
|
|
388
|
+
getState,
|
|
389
|
+
async () => await user.selectOptions(availabilityFilter, ['Show']),
|
|
390
|
+
(_before, after) =>
|
|
391
|
+
after.availabilitySelected === 'Show' &&
|
|
392
|
+
!after.incompleteVisible &&
|
|
393
|
+
after.componentVisible &&
|
|
394
|
+
after.companionVisible &&
|
|
395
|
+
!after.legacyIncompleteVisible
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
await performAndAssert(
|
|
399
|
+
'Reset required filter -> authored filtersIncomplete module returns',
|
|
400
|
+
getState,
|
|
401
|
+
async () => await user.selectOptions(requiredSelectionFilter, ['Select a value']),
|
|
402
|
+
(_before, after) =>
|
|
403
|
+
after.requiredSelectionSelected === 'Select a value' &&
|
|
404
|
+
after.incompleteVisible &&
|
|
405
|
+
!after.componentVisible &&
|
|
406
|
+
!after.companionVisible &&
|
|
407
|
+
!after.legacyIncompleteVisible
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
115
412
|
export const TP5_Test_Dashboard: Story = {
|
|
116
413
|
args: {
|
|
117
414
|
config: TP5TestConfig,
|
|
@@ -119,6 +416,20 @@ export const TP5_Test_Dashboard: Story = {
|
|
|
119
416
|
}
|
|
120
417
|
}
|
|
121
418
|
|
|
419
|
+
export const DataDrivenColors: Story = {
|
|
420
|
+
args: {
|
|
421
|
+
config: DataDrivenColorsConfig as unknown as Config,
|
|
422
|
+
isEditor: false
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export const DataDrivenColors_Editor: Story = {
|
|
427
|
+
args: {
|
|
428
|
+
config: DataDrivenColorsConfig as unknown as Config,
|
|
429
|
+
isEditor: true
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
122
433
|
export const Line_Chart_Angles: Story = {
|
|
123
434
|
args: {
|
|
124
435
|
config: LineChartAnglesConfig,
|
|
@@ -4788,8 +5099,7 @@ export const Nested_Dropdown_With_Parent_Child: Story = {
|
|
|
4788
5099
|
|
|
4789
5100
|
// Verify defaultValue is applied on initial load (North region, 2023 Q2)
|
|
4790
5101
|
expect(initialState.regionSelected).toBe('North')
|
|
4791
|
-
expect(initialState.yearQuarterSelected).
|
|
4792
|
-
expect(initialState.yearQuarterSelected).toContain('Q2')
|
|
5102
|
+
expect(initialState.yearQuarterSelected).toBe('2023 - Q2')
|
|
4793
5103
|
expect(initialState.chartRendered).toBe(true)
|
|
4794
5104
|
|
|
4795
5105
|
// Test 1: Change region to South → year options should update based on available data
|
|
@@ -4846,6 +5156,31 @@ export const Nested_Dropdown_With_Parent_Child: Story = {
|
|
|
4846
5156
|
}
|
|
4847
5157
|
}
|
|
4848
5158
|
|
|
5159
|
+
export const Nested_Dropdown_With_Parent_Child_Subgroup_Only: Story = {
|
|
5160
|
+
args: {
|
|
5161
|
+
config: NestedParentChildFiltersSubgroupOnly as unknown as Config,
|
|
5162
|
+
isEditor: false
|
|
5163
|
+
},
|
|
5164
|
+
parameters: {
|
|
5165
|
+
docs: {
|
|
5166
|
+
description: {
|
|
5167
|
+
story:
|
|
5168
|
+
'Displays only the selected subgroup in the closed nested dropdown input while preserving the same filter behavior.'
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
},
|
|
5172
|
+
play: async ({ canvasElement }) => {
|
|
5173
|
+
const canvas = within(canvasElement)
|
|
5174
|
+
const regionFilter = (await canvas.findByLabelText('Region', { selector: 'select' })) as HTMLSelectElement
|
|
5175
|
+
const yearQuarterInput = canvasElement.querySelector('.nested-dropdown input') as HTMLInputElement
|
|
5176
|
+
|
|
5177
|
+
await waitForOptionsToPopulate(regionFilter, 4)
|
|
5178
|
+
|
|
5179
|
+
expect(regionFilter.value).toBe('North')
|
|
5180
|
+
expect(yearQuarterInput.value).toBe('Q2')
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
|
|
4849
5184
|
export const Metadata_In_Dashboard: Story = {
|
|
4850
5185
|
args: {
|
|
4851
5186
|
configUrl: '/packages/dashboard/tests/fixtures/dashboard-config-with-metadata.json'
|