@cdc/dashboard 4.26.4 → 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 +77 -30
- package/LICENSE +201 -0
- package/dist/cdcdashboard.js +49936 -49166
- 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/private/cfa-dashboard.json +651 -0
- package/examples/private/data-bite-wrap.json +6936 -0
- package/examples/private/multi-dash-fix.json +16963 -0
- package/examples/private/versions.json +41612 -0
- package/examples/us-map-filter-example.json +1074 -0
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +6 -2
- package/src/CdcDashboardComponent.tsx +178 -87
- 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.stories.tsx +294 -0
- package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
- 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 +8 -0
- package/src/components/DashboardFilters/DashboardFilters.test.tsx +139 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +192 -174
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +41 -2
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +180 -3
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +15 -32
- 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/Grid.tsx +8 -4
- package/src/components/Header/Header.tsx +36 -17
- package/src/components/Row.test.tsx +228 -0
- package/src/components/Row.tsx +93 -18
- package/src/components/VisualizationRow.test.tsx +396 -0
- package/src/components/VisualizationRow.tsx +110 -35
- 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 +119 -17
- package/src/components/Widget/widget.styles.css +31 -18
- package/src/components/dashboard-condition-modal.css +76 -0
- package/src/components/dashboard-condition-summary.css +87 -0
- package/src/helpers/addValuesToDashboardFilters.ts +3 -5
- package/src/helpers/addVisualization.ts +15 -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 +249 -20
- package/src/scss/main.scss +108 -29
- package/src/store/dashboard.actions.ts +17 -4
- package/src/store/dashboard.reducer.test.ts +538 -0
- package/src/store/dashboard.reducer.ts +135 -22
- 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 +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
|
|
2
|
-
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import React, { useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
3
3
|
import Toggle from './Toggle'
|
|
4
4
|
import cloneDeep from 'lodash/cloneDeep'
|
|
5
5
|
import { ConfigRow } from '../types/ConfigRow'
|
|
@@ -11,6 +11,12 @@ import CdcFilteredText from '@cdc/filtered-text/src/CdcFilteredText'
|
|
|
11
11
|
import DashboardSharedFilters, { APIFilterDropdowns } from './DashboardFilters'
|
|
12
12
|
import { DashboardContext } from '../DashboardContext'
|
|
13
13
|
import { ViewPort } from '@cdc/core/types/ViewPort'
|
|
14
|
+
import { evaluateDashboardCondition } from '../helpers/dashboardConditions'
|
|
15
|
+
import { dashboardConditionsSupportedForRow } from '../helpers/dashboardFilterTargets'
|
|
16
|
+
import {
|
|
17
|
+
hasAuthoredWidgetEntries,
|
|
18
|
+
resolveColumnWidgetEntry as resolveDashboardColumnWidgetEntry
|
|
19
|
+
} from '../helpers/dashboardColumnWidgets'
|
|
14
20
|
import { getVizConfig } from '../helpers/getVizConfig'
|
|
15
21
|
import { TableConfig } from '@cdc/core/components/DataTable/types/TableConfig'
|
|
16
22
|
import CollapsibleVisualizationRow from './CollapsibleVisualizationRow'
|
|
@@ -21,6 +27,7 @@ import ExpandCollapseButtons from './ExpandCollapseButtons'
|
|
|
21
27
|
import { ChartConfig } from '@cdc/chart/src/types/ChartConfig'
|
|
22
28
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
23
29
|
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
30
|
+
import { hasVisibleDashboardFiltersForIndexes } from '../helpers/filterVisibility'
|
|
24
31
|
|
|
25
32
|
type VisualizationWrapperProps = {
|
|
26
33
|
allExpanded: boolean
|
|
@@ -63,6 +70,8 @@ type VizRowProps = {
|
|
|
63
70
|
rowIndex: number
|
|
64
71
|
inNoDataState: boolean
|
|
65
72
|
setSharedFilter: Function
|
|
73
|
+
clearSharedFilter: (key: string) => void
|
|
74
|
+
hasActiveSharedFilter: (key: string) => boolean
|
|
66
75
|
updateChildConfig: Function
|
|
67
76
|
apiFilterDropdowns: APIFilterDropdowns
|
|
68
77
|
currentViewport: ViewPort
|
|
@@ -79,6 +88,8 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
79
88
|
rowIndex: index,
|
|
80
89
|
inNoDataState,
|
|
81
90
|
setSharedFilter,
|
|
91
|
+
clearSharedFilter,
|
|
92
|
+
hasActiveSharedFilter,
|
|
82
93
|
updateChildConfig,
|
|
83
94
|
apiFilterDropdowns,
|
|
84
95
|
currentViewport,
|
|
@@ -87,7 +98,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
87
98
|
interactionLabel = ''
|
|
88
99
|
}) => {
|
|
89
100
|
const { config, filteredData: dashboardFilteredData, data: rawData } = useContext(DashboardContext)
|
|
90
|
-
const [toggledRow, setToggled] =
|
|
101
|
+
const [toggledRow, setToggled] = useState<number>(0)
|
|
91
102
|
const rowRef = useRef<HTMLDivElement>(null)
|
|
92
103
|
|
|
93
104
|
useEffect(() => {
|
|
@@ -166,9 +177,27 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
166
177
|
}
|
|
167
178
|
}
|
|
168
179
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
180
|
+
const shouldIgnoreDashboardConditions = !dashboardConditionsSupportedForRow(row)
|
|
181
|
+
const columnDashboardConditionEvaluations = useMemo(
|
|
182
|
+
() =>
|
|
183
|
+
row.columns.map(column =>
|
|
184
|
+
resolveDashboardColumnWidgetEntry(column, {
|
|
185
|
+
evaluateCondition: dashboardCondition => {
|
|
186
|
+
if (shouldIgnoreDashboardConditions || !dashboardCondition) {
|
|
187
|
+
return { matches: true, resolved: true }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return evaluateDashboardCondition(dashboardCondition, dashboardFilteredData[dashboardCondition.id])
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
),
|
|
194
|
+
[dashboardFilteredData, row.columns, shouldIgnoreDashboardConditions]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const tp5CountInRow = columnDashboardConditionEvaluations.reduce((count, evaluation) => {
|
|
198
|
+
const widgetKey = evaluation?.widget
|
|
199
|
+
if (!widgetKey) return count
|
|
200
|
+
const viz = config.visualizations[widgetKey]
|
|
172
201
|
if (!viz) return count
|
|
173
202
|
|
|
174
203
|
const isTp5DataBite = viz.type === 'data-bite' && (viz as any).biteStyle === 'tp5'
|
|
@@ -182,7 +211,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
182
211
|
const shouldEqualizeRow = !!row.equalHeight || needsTP5AutoEqualization
|
|
183
212
|
|
|
184
213
|
// Layer TP5 equalization for row-level title consistency and same-type internals.
|
|
185
|
-
|
|
214
|
+
useLayoutEffect(() => {
|
|
186
215
|
if (!shouldEqualizeRow) return
|
|
187
216
|
|
|
188
217
|
const rowElement = rowRef.current
|
|
@@ -205,9 +234,10 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
205
234
|
}
|
|
206
235
|
}, [shouldEqualizeRow, row.columns, config.activeDashboard, filteredDataOverride, dashboardFilteredData[index]])
|
|
207
236
|
|
|
208
|
-
const isFilterRow =
|
|
209
|
-
|
|
210
|
-
|
|
237
|
+
const isFilterRow = columnDashboardConditionEvaluations.some(evaluation => {
|
|
238
|
+
const widgetKey = evaluation?.widget
|
|
239
|
+
return widgetKey && config.visualizations[widgetKey]?.type === 'dashboardFilters'
|
|
240
|
+
})
|
|
211
241
|
const needsEqualHeight = shouldEqualizeRow && !isFilterRow
|
|
212
242
|
|
|
213
243
|
const show = useMemo(() => {
|
|
@@ -219,6 +249,33 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
219
249
|
}, [config.activeDashboard, toggledRow])
|
|
220
250
|
|
|
221
251
|
const _data = dashboardFilteredData[index] || row.formattedData || []
|
|
252
|
+
const isMultiVizGroupRow = !!row.originalMultiVizColumn && !!filteredDataOverride
|
|
253
|
+
const rowDashboardCondition = useMemo(() => {
|
|
254
|
+
if (shouldIgnoreDashboardConditions || !row.dashboardCondition) {
|
|
255
|
+
return { matches: true, resolved: true }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return evaluateDashboardCondition(row.dashboardCondition, dashboardFilteredData[row.dashboardCondition.id])
|
|
259
|
+
}, [dashboardFilteredData, row.dashboardCondition, shouldIgnoreDashboardConditions])
|
|
260
|
+
const hasVisibleWidgetColumn = row.columns.some((_column, columnIndex) => {
|
|
261
|
+
if (!row.columns[columnIndex].width) return false
|
|
262
|
+
|
|
263
|
+
const widgetKey = columnDashboardConditionEvaluations[columnIndex]?.widget
|
|
264
|
+
if (!widgetKey) return false
|
|
265
|
+
|
|
266
|
+
const visualization = config.visualizations[widgetKey]
|
|
267
|
+
if (visualization?.type !== 'dashboardFilters') return true
|
|
268
|
+
|
|
269
|
+
return hasVisibleDashboardFiltersForIndexes(
|
|
270
|
+
config.dashboard.sharedFilters,
|
|
271
|
+
(visualization as DashboardFilters).sharedFilterIndexes
|
|
272
|
+
)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
if (!rowDashboardCondition.matches || !hasVisibleWidgetColumn) {
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
|
|
222
279
|
const dataGroups =
|
|
223
280
|
row.multiVizColumn &&
|
|
224
281
|
_data.reduce((acc, dataRow) => {
|
|
@@ -235,7 +292,9 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
235
292
|
const applyFilters = dashboardFilters.filter(v => !v.autoLoad).flatMap(v => v.sharedFilterIndexes)
|
|
236
293
|
if (hasDashboardApplyBehavior(config.visualizations) && vizConfig.autoLoad) {
|
|
237
294
|
return applyFilters.some(index => {
|
|
238
|
-
const
|
|
295
|
+
const filter = config.dashboard.sharedFilters[index]
|
|
296
|
+
if (!filter) return false
|
|
297
|
+
const { queuedActive, active, subGrouping } = filter
|
|
239
298
|
if (!active && !queuedActive) return true
|
|
240
299
|
if (!queuedActive) return false
|
|
241
300
|
// for nested dropdowns
|
|
@@ -294,11 +353,27 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
294
353
|
)}
|
|
295
354
|
{row.columns.map((col, colIndex) => {
|
|
296
355
|
if (col.width) {
|
|
297
|
-
|
|
298
|
-
|
|
356
|
+
const resolvedWidget = columnDashboardConditionEvaluations[colIndex]?.widget
|
|
357
|
+
const hasAuthoredWidgets = hasAuthoredWidgetEntries(col)
|
|
358
|
+
const hiddenByDashboardCondition = hasAuthoredWidgets && !resolvedWidget
|
|
359
|
+
if (!resolvedWidget) {
|
|
360
|
+
if (!hasAuthoredWidgets && (isMultiVizGroupRow || row.toggle)) {
|
|
361
|
+
return null
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<div
|
|
366
|
+
key={`row__${index}__col__${colIndex}`}
|
|
367
|
+
className={`col-12 col-md-${col.width}${
|
|
368
|
+
hiddenByDashboardCondition ? ' dashboard-condition-hidden' : ''
|
|
369
|
+
}`}
|
|
370
|
+
data-dashboard-condition-hidden={hiddenByDashboardCondition || undefined}
|
|
371
|
+
></div>
|
|
372
|
+
)
|
|
373
|
+
}
|
|
299
374
|
|
|
300
375
|
const visualizationConfig = getVizConfig(
|
|
301
|
-
|
|
376
|
+
resolvedWidget,
|
|
302
377
|
index,
|
|
303
378
|
config,
|
|
304
379
|
rawData,
|
|
@@ -311,9 +386,9 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
311
386
|
|
|
312
387
|
const setsSharedFilter =
|
|
313
388
|
config.dashboard.sharedFilters &&
|
|
314
|
-
config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy ===
|
|
389
|
+
config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === resolvedWidget).length > 0
|
|
315
390
|
const setSharedFilterValue = setsSharedFilter
|
|
316
|
-
? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy ===
|
|
391
|
+
? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === resolvedWidget)[0].active
|
|
317
392
|
: undefined
|
|
318
393
|
const tableLink = (
|
|
319
394
|
<a
|
|
@@ -349,9 +424,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
349
424
|
|
|
350
425
|
const hiddenDashboardFilters =
|
|
351
426
|
type === 'dashboardFilters' &&
|
|
352
|
-
sharedFilterIndexes
|
|
353
|
-
sharedFilterIndexes.filter(idx => config.dashboard.sharedFilters?.[idx]?.showDropdown === false).length ===
|
|
354
|
-
sharedFilterIndexes.length
|
|
427
|
+
!hasVisibleDashboardFiltersForIndexes(config.dashboard.sharedFilters, sharedFilterIndexes)
|
|
355
428
|
|
|
356
429
|
const vizWrapperClass = `col-12 col-md-${col.width}${!shouldShow ? ' d-none' : ''}${
|
|
357
430
|
hideVisualization ? ' hide-parent-visualization' : ''
|
|
@@ -371,12 +444,12 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
371
444
|
>
|
|
372
445
|
{type === 'chart' && (
|
|
373
446
|
<CdcChart
|
|
374
|
-
key={
|
|
447
|
+
key={resolvedWidget}
|
|
375
448
|
config={visualizationConfig as ChartConfig}
|
|
376
449
|
dashboardConfig={config}
|
|
377
450
|
datasets={config.datasets}
|
|
378
451
|
setConfig={newConfig => {
|
|
379
|
-
updateChildConfig(
|
|
452
|
+
updateChildConfig(resolvedWidget, newConfig)
|
|
380
453
|
}}
|
|
381
454
|
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
382
455
|
isDashboard={true}
|
|
@@ -386,13 +459,15 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
386
459
|
)}
|
|
387
460
|
{type === 'map' && (
|
|
388
461
|
<CdcMap
|
|
389
|
-
key={
|
|
462
|
+
key={resolvedWidget}
|
|
390
463
|
config={visualizationConfig}
|
|
391
464
|
setConfig={newConfig => {
|
|
392
|
-
updateChildConfig(
|
|
465
|
+
updateChildConfig(resolvedWidget, newConfig)
|
|
393
466
|
}}
|
|
394
467
|
showLoader={false}
|
|
395
468
|
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
469
|
+
clearSharedFilter={setsSharedFilter ? clearSharedFilter : undefined}
|
|
470
|
+
hasActiveSharedFilter={setsSharedFilter ? hasActiveSharedFilter(resolvedWidget) : false}
|
|
396
471
|
setSharedFilterValue={setSharedFilterValue}
|
|
397
472
|
isDashboard={true}
|
|
398
473
|
link={link}
|
|
@@ -402,11 +477,11 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
402
477
|
)}
|
|
403
478
|
{type === 'data-bite' && (
|
|
404
479
|
<CdcDataBite
|
|
405
|
-
key={
|
|
480
|
+
key={resolvedWidget}
|
|
406
481
|
config={visualizationConfig}
|
|
407
482
|
rawData={rawData?.[visualizationConfig.dataKey] || []}
|
|
408
483
|
setConfig={newConfig => {
|
|
409
|
-
updateChildConfig(
|
|
484
|
+
updateChildConfig(resolvedWidget, newConfig)
|
|
410
485
|
}}
|
|
411
486
|
isDashboard={true}
|
|
412
487
|
isEditor={config.editing === true}
|
|
@@ -415,11 +490,11 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
415
490
|
)}
|
|
416
491
|
{type === 'waffle-chart' && (
|
|
417
492
|
<CdcWaffleChart
|
|
418
|
-
key={
|
|
493
|
+
key={resolvedWidget}
|
|
419
494
|
config={visualizationConfig}
|
|
420
495
|
rawData={rawData?.[visualizationConfig.dataKey] || []}
|
|
421
496
|
setConfig={newConfig => {
|
|
422
|
-
updateChildConfig(
|
|
497
|
+
updateChildConfig(resolvedWidget, newConfig)
|
|
423
498
|
}}
|
|
424
499
|
isDashboard={true}
|
|
425
500
|
interactionLabel={interactionLabel}
|
|
@@ -427,22 +502,22 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
427
502
|
)}
|
|
428
503
|
{type === 'markup-include' && (
|
|
429
504
|
<CdcMarkupInclude
|
|
430
|
-
key={
|
|
505
|
+
key={resolvedWidget}
|
|
431
506
|
config={visualizationConfig}
|
|
432
507
|
datasets={config.datasets}
|
|
433
508
|
isDashboard={true}
|
|
434
509
|
setConfig={newConfig => {
|
|
435
|
-
updateChildConfig(
|
|
510
|
+
updateChildConfig(resolvedWidget, newConfig)
|
|
436
511
|
}}
|
|
437
512
|
interactionLabel={interactionLabel}
|
|
438
513
|
/>
|
|
439
514
|
)}
|
|
440
515
|
{type === 'filtered-text' && (
|
|
441
516
|
<CdcFilteredText
|
|
442
|
-
key={
|
|
517
|
+
key={resolvedWidget}
|
|
443
518
|
config={visualizationConfig}
|
|
444
519
|
setConfig={newConfig => {
|
|
445
|
-
updateChildConfig(
|
|
520
|
+
updateChildConfig(resolvedWidget, newConfig)
|
|
446
521
|
}}
|
|
447
522
|
isDashboard={true}
|
|
448
523
|
/>
|
|
@@ -450,9 +525,9 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
450
525
|
{type === 'dashboardFilters' && (
|
|
451
526
|
<DashboardSharedFilters
|
|
452
527
|
setConfig={newConfig => {
|
|
453
|
-
updateChildConfig(
|
|
528
|
+
updateChildConfig(resolvedWidget, newConfig)
|
|
454
529
|
}}
|
|
455
|
-
key={
|
|
530
|
+
key={resolvedWidget}
|
|
456
531
|
visualizationConfig={visualizationConfig as DashboardFilters}
|
|
457
532
|
apiFilterDropdowns={apiFilterDropdowns}
|
|
458
533
|
currentViewport={currentViewport}
|
|
@@ -461,11 +536,11 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
461
536
|
)}
|
|
462
537
|
{type === 'table' && (
|
|
463
538
|
<DataTableStandAlone
|
|
464
|
-
key={
|
|
539
|
+
key={resolvedWidget}
|
|
465
540
|
updateConfig={newConfig => {
|
|
466
|
-
updateChildConfig(
|
|
541
|
+
updateChildConfig(resolvedWidget, newConfig)
|
|
467
542
|
}}
|
|
468
|
-
visualizationKey={
|
|
543
|
+
visualizationKey={resolvedWidget}
|
|
469
544
|
config={visualizationConfig as TableConfig}
|
|
470
545
|
viewport={currentViewport}
|
|
471
546
|
interactionLabel={interactionLabel}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { DashboardContext, DashboardDispatchContext, initialState } from '../../DashboardContext'
|
|
5
|
+
import VisualizationsPanel from './VisualizationsPanel'
|
|
6
|
+
|
|
7
|
+
vi.mock('../Widget/Widget', () => ({
|
|
8
|
+
default: ({ type }: { type: string }) => <div data-testid='creation-widget'>{type}</div>
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
vi.mock('@cdc/core/components/AdvancedEditor', () => ({
|
|
12
|
+
default: () => <div data-testid='advanced-editor' />
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
describe('VisualizationsPanel', () => {
|
|
16
|
+
it('does not expose filtered-text in dashboard creation surfaces', () => {
|
|
17
|
+
render(
|
|
18
|
+
<DashboardContext.Provider
|
|
19
|
+
value={{
|
|
20
|
+
...initialState,
|
|
21
|
+
config: {
|
|
22
|
+
type: 'dashboard',
|
|
23
|
+
dashboard: { sharedFilters: [] },
|
|
24
|
+
datasets: {},
|
|
25
|
+
rows: [],
|
|
26
|
+
visualizations: {}
|
|
27
|
+
} as any,
|
|
28
|
+
outerContainerRef: vi.fn(),
|
|
29
|
+
setParentConfig: vi.fn(),
|
|
30
|
+
isDebug: false,
|
|
31
|
+
isEditor: true,
|
|
32
|
+
reloadURLData: vi.fn(),
|
|
33
|
+
loadAPIFilters: vi.fn(),
|
|
34
|
+
setAPIFilterDropdowns: vi.fn(),
|
|
35
|
+
setAPILoading: vi.fn(),
|
|
36
|
+
data: {}
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<DashboardDispatchContext.Provider value={vi.fn()}>
|
|
40
|
+
<VisualizationsPanel />
|
|
41
|
+
</DashboardDispatchContext.Provider>
|
|
42
|
+
</DashboardContext.Provider>
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const creationTypes = screen.getAllByTestId('creation-widget').map(widget => widget.textContent)
|
|
46
|
+
expect(creationTypes).toContain('markup-include')
|
|
47
|
+
expect(creationTypes).not.toContain('filtered-text')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -12,6 +12,8 @@ const VisualizationsPanel = () => {
|
|
|
12
12
|
const [advancedEditing, setAdvancedEditing] = useState(false)
|
|
13
13
|
const { config, isEditor } = useContext(DashboardContext)
|
|
14
14
|
const dispatch = useContext(DashboardDispatchContext)
|
|
15
|
+
const createVisualization = (type, subType) =>
|
|
16
|
+
addVisualization(type, subType, { existingIds: Object.keys(config.visualizations || {}) })
|
|
15
17
|
const loadConfig = incomingConfig => {
|
|
16
18
|
const newConfig = !incomingConfig.multiDashboards
|
|
17
19
|
? incomingConfig
|
|
@@ -29,25 +31,24 @@ const VisualizationsPanel = () => {
|
|
|
29
31
|
<p style={{ fontSize: '14px' }}>Click and drag an item onto the grid to add it to your dashboard.</p>
|
|
30
32
|
<span className='subheading-3'>Chart</span>
|
|
31
33
|
<div className='drag-grid'>
|
|
32
|
-
<Widget addVisualization={() =>
|
|
33
|
-
<Widget addVisualization={() =>
|
|
34
|
-
<Widget addVisualization={() =>
|
|
35
|
-
<Widget addVisualization={() =>
|
|
34
|
+
<Widget addVisualization={() => createVisualization('chart', 'Bar')} type='Bar' />
|
|
35
|
+
<Widget addVisualization={() => createVisualization('chart', 'Line')} type='Line' />
|
|
36
|
+
<Widget addVisualization={() => createVisualization('chart', 'Pie')} type='Pie' />
|
|
37
|
+
<Widget addVisualization={() => createVisualization('chart', 'Sankey')} type='Sankey' />
|
|
36
38
|
</div>
|
|
37
39
|
<span className='subheading-3'>Map</span>
|
|
38
40
|
<div className='drag-grid'>
|
|
39
|
-
<Widget addVisualization={() =>
|
|
40
|
-
<Widget addVisualization={() =>
|
|
41
|
-
<Widget addVisualization={() =>
|
|
41
|
+
<Widget addVisualization={() => createVisualization('map', 'us')} type='us' />
|
|
42
|
+
<Widget addVisualization={() => createVisualization('map', 'world')} type='world' />
|
|
43
|
+
<Widget addVisualization={() => createVisualization('map', 'single-state')} type='single-state' />
|
|
42
44
|
</div>
|
|
43
45
|
<span className='subheading-3'>Misc.</span>
|
|
44
46
|
<div className='drag-grid'>
|
|
45
|
-
<Widget addVisualization={() =>
|
|
46
|
-
<Widget addVisualization={() =>
|
|
47
|
-
<Widget addVisualization={() =>
|
|
48
|
-
<Widget addVisualization={() =>
|
|
49
|
-
<Widget addVisualization={() =>
|
|
50
|
-
<Widget addVisualization={() => addVisualization('table', '')} type='table' />
|
|
47
|
+
<Widget addVisualization={() => createVisualization('data-bite', '')} type='data-bite' />
|
|
48
|
+
<Widget addVisualization={() => createVisualization('waffle-chart', 'Waffle')} type='waffle-chart' />
|
|
49
|
+
<Widget addVisualization={() => createVisualization('markup-include', '')} type='markup-include' />
|
|
50
|
+
<Widget addVisualization={() => createVisualization('dashboardFilters', '')} type='dashboardFilters' />
|
|
51
|
+
<Widget addVisualization={() => createVisualization('table', '')} type='table' />
|
|
51
52
|
</div>
|
|
52
53
|
<AdvancedEditor
|
|
53
54
|
loadConfig={loadConfig}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { DashboardContext, DashboardDispatchContext, initialState } from '../../DashboardContext'
|
|
5
|
+
import { DashboardCopyPasteContext } from '../../DashboardCopyPasteContext'
|
|
6
|
+
import { GlobalContext } from '@cdc/core/components/GlobalContext'
|
|
7
|
+
import Widget from './Widget'
|
|
8
|
+
|
|
9
|
+
vi.mock('@cdc/core/components/ui/Icon', () => ({
|
|
10
|
+
default: props => <span data-testid='mock-icon' {...props} />
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
let latestDragSpec: any
|
|
14
|
+
|
|
15
|
+
vi.mock('react-dnd', () => ({
|
|
16
|
+
useDrag: spec => {
|
|
17
|
+
latestDragSpec = spec
|
|
18
|
+
return [{ isDragging: false }, () => {}]
|
|
19
|
+
}
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
const renderWidget = (
|
|
23
|
+
options: {
|
|
24
|
+
copiedWidget?: any
|
|
25
|
+
copyWidget?: any
|
|
26
|
+
clearCopiedWidget?: any
|
|
27
|
+
dispatch?: any
|
|
28
|
+
type?: string
|
|
29
|
+
title?: string
|
|
30
|
+
widgetConfig?: any
|
|
31
|
+
} = {}
|
|
32
|
+
) => {
|
|
33
|
+
const openOverlay = vi.fn()
|
|
34
|
+
const copyWidget = options.copyWidget || vi.fn()
|
|
35
|
+
const clearCopiedWidget = options.clearCopiedWidget || vi.fn()
|
|
36
|
+
const dispatch = options.dispatch || vi.fn()
|
|
37
|
+
const type = options.type || 'markup-include'
|
|
38
|
+
const title = options.title ?? 'Example'
|
|
39
|
+
const widgetConfig = options.widgetConfig || {
|
|
40
|
+
uid: 'markup-1',
|
|
41
|
+
rowIdx: 0,
|
|
42
|
+
colIdx: 0,
|
|
43
|
+
entryIdx: 0,
|
|
44
|
+
type: 'markup-include',
|
|
45
|
+
visualizationType: 'markup-include',
|
|
46
|
+
contentEditor: { title: 'Example' }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const renderResult = render(
|
|
50
|
+
<GlobalContext.Provider
|
|
51
|
+
value={{
|
|
52
|
+
overlay: {
|
|
53
|
+
object: null,
|
|
54
|
+
show: false,
|
|
55
|
+
disableBgClose: false,
|
|
56
|
+
actions: {
|
|
57
|
+
openOverlay,
|
|
58
|
+
toggleOverlay: vi.fn()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<DashboardContext.Provider
|
|
64
|
+
value={{
|
|
65
|
+
...initialState,
|
|
66
|
+
config: {
|
|
67
|
+
type: 'dashboard',
|
|
68
|
+
activeDashboard: 0,
|
|
69
|
+
dashboard: { sharedFilters: [] },
|
|
70
|
+
datasets: {},
|
|
71
|
+
rows: [
|
|
72
|
+
{
|
|
73
|
+
columns: [
|
|
74
|
+
{
|
|
75
|
+
width: 12,
|
|
76
|
+
conditionalWidgets: [
|
|
77
|
+
{
|
|
78
|
+
widget: 'markup-1',
|
|
79
|
+
dashboardCondition: {
|
|
80
|
+
id: 'column-condition-1',
|
|
81
|
+
datasetKey: 'condition-data',
|
|
82
|
+
operator: 'hasData'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
expandCollapseAllButtons: false
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
visualizations: {
|
|
92
|
+
'markup-1': {
|
|
93
|
+
uid: 'markup-1',
|
|
94
|
+
type: 'markup-include',
|
|
95
|
+
visualizationType: 'markup-include',
|
|
96
|
+
contentEditor: { title: 'Example' }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} as any,
|
|
100
|
+
outerContainerRef: vi.fn(),
|
|
101
|
+
setParentConfig: vi.fn(),
|
|
102
|
+
isDebug: false,
|
|
103
|
+
isEditor: true,
|
|
104
|
+
reloadURLData: vi.fn(),
|
|
105
|
+
loadAPIFilters: vi.fn(),
|
|
106
|
+
setAPIFilterDropdowns: vi.fn(),
|
|
107
|
+
setAPILoading: vi.fn(),
|
|
108
|
+
data: {}
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<DashboardDispatchContext.Provider value={dispatch}>
|
|
112
|
+
<DashboardCopyPasteContext.Provider
|
|
113
|
+
value={{ copiedWidget: options.copiedWidget, copyWidget, clearCopiedWidget }}
|
|
114
|
+
>
|
|
115
|
+
<Widget title={title} toggleRow={false} type={type} widgetConfig={widgetConfig} widgetInRow />
|
|
116
|
+
</DashboardCopyPasteContext.Provider>
|
|
117
|
+
</DashboardDispatchContext.Provider>
|
|
118
|
+
</DashboardContext.Provider>
|
|
119
|
+
</GlobalContext.Provider>
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return { ...renderResult, openOverlay, copyWidget, clearCopiedWidget, dispatch }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe('Widget', () => {
|
|
126
|
+
it('shows the active widget condition button and summary strip when a column condition exists', () => {
|
|
127
|
+
renderWidget()
|
|
128
|
+
|
|
129
|
+
expect(screen.getByTitle('Configure Dashboard Condition')).toHaveClass('is-active')
|
|
130
|
+
expect(screen.getByRole('button', { name: "Configure Dashboard Condition: Show when there's data" })).toHaveClass(
|
|
131
|
+
'dashboard-condition-summary'
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('opens the condition modal from the widget condition button or summary strip', () => {
|
|
136
|
+
const { openOverlay } = renderWidget()
|
|
137
|
+
|
|
138
|
+
fireEvent.click(screen.getByTitle('Configure Dashboard Condition'))
|
|
139
|
+
fireEvent.click(screen.getByRole('button', { name: "Configure Dashboard Condition: Show when there's data" }))
|
|
140
|
+
|
|
141
|
+
expect(openOverlay).toHaveBeenCalledTimes(2)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('shows a copy button for populated widgets and stores the copied widget label', () => {
|
|
145
|
+
const { copyWidget } = renderWidget()
|
|
146
|
+
|
|
147
|
+
fireEvent.click(screen.getByTitle('Copy Component'))
|
|
148
|
+
|
|
149
|
+
expect(copyWidget).toHaveBeenCalledWith({ sourceWidgetKey: 'markup-1', label: 'Example' })
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('uses a delete button for removing widgets', () => {
|
|
153
|
+
const { dispatch } = renderWidget()
|
|
154
|
+
|
|
155
|
+
fireEvent.click(screen.getByRole('button', { name: 'Delete Component' }))
|
|
156
|
+
|
|
157
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
158
|
+
type: 'DELETE_WIDGET',
|
|
159
|
+
payload: { uid: 'markup-1' }
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('shows labels for waffle and gauge visualization type aliases', () => {
|
|
164
|
+
renderWidget({
|
|
165
|
+
type: 'TP5 Waffle',
|
|
166
|
+
title: '',
|
|
167
|
+
widgetConfig: {
|
|
168
|
+
uid: 'waffle-1',
|
|
169
|
+
rowIdx: 0,
|
|
170
|
+
colIdx: 0,
|
|
171
|
+
type: 'waffle-chart',
|
|
172
|
+
visualizationType: 'TP5 Waffle'
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
expect(screen.getByText('Waffle Chart')).toBeInTheDocument()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('marks the copied widget and lets the active copy button cancel copy mode', () => {
|
|
180
|
+
const clearCopiedWidget = vi.fn()
|
|
181
|
+
const { container, copyWidget } = renderWidget({
|
|
182
|
+
copiedWidget: { sourceWidgetKey: 'markup-1', label: 'Example' },
|
|
183
|
+
clearCopiedWidget
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
expect(container.querySelector('.widget')).toHaveClass('widget--copied-source')
|
|
187
|
+
expect(screen.getByTitle('Copy Component')).toHaveClass('is-active')
|
|
188
|
+
expect(screen.getByRole('button', { name: 'Clear copied component' })).toHaveTextContent('Copied')
|
|
189
|
+
|
|
190
|
+
fireEvent.click(screen.getByRole('button', { name: 'Clear copied component' }))
|
|
191
|
+
|
|
192
|
+
expect(clearCopiedWidget).toHaveBeenCalledTimes(1)
|
|
193
|
+
|
|
194
|
+
fireEvent.click(screen.getByTitle('Copy Component'))
|
|
195
|
+
|
|
196
|
+
expect(clearCopiedWidget).toHaveBeenCalledTimes(2)
|
|
197
|
+
expect(copyWidget).not.toHaveBeenCalled()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('clears copy mode after a successful widget move', () => {
|
|
201
|
+
const clearCopiedWidget = vi.fn()
|
|
202
|
+
const dispatch = vi.fn()
|
|
203
|
+
renderWidget({ clearCopiedWidget, dispatch })
|
|
204
|
+
|
|
205
|
+
latestDragSpec.end(null, { getDropResult: () => ({ rowIdx: 0, colIdx: 0 }) })
|
|
206
|
+
|
|
207
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
208
|
+
type: 'MOVE_VISUALIZATION',
|
|
209
|
+
payload: {
|
|
210
|
+
rowIdx: 0,
|
|
211
|
+
colIdx: 0,
|
|
212
|
+
entryIdx: undefined,
|
|
213
|
+
widget: expect.objectContaining({ uid: 'markup-1' })
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
expect(clearCopiedWidget).toHaveBeenCalled()
|
|
217
|
+
})
|
|
218
|
+
})
|