@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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ConditionalWidget, DashboardCondition } from '../types/ConfigRow'
|
|
2
|
+
|
|
3
|
+
type ColumnWithWidgets = {
|
|
4
|
+
widget?: string | null
|
|
5
|
+
conditionalWidgets?: ConditionalWidget[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const hasConditionalWidgets = (column?: ColumnWithWidgets): boolean => !!column?.conditionalWidgets?.length
|
|
9
|
+
|
|
10
|
+
export const hasAuthoredWidgetEntries = (column?: ColumnWithWidgets): boolean =>
|
|
11
|
+
getColumnWidgetEntries(column).length > 0
|
|
12
|
+
|
|
13
|
+
export const getConditionalWidgets = (column?: ColumnWithWidgets): ConditionalWidget[] =>
|
|
14
|
+
column?.conditionalWidgets?.filter(entry => !!entry?.widget) || []
|
|
15
|
+
|
|
16
|
+
export const getColumnWidgetEntries = (column?: ColumnWithWidgets): ConditionalWidget[] => {
|
|
17
|
+
if (!column) return []
|
|
18
|
+
if (hasConditionalWidgets(column)) {
|
|
19
|
+
return getConditionalWidgets(column)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return column.widget ? [{ widget: column.widget }] : []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const getColumnWidgetKeys = (column?: ColumnWithWidgets): string[] =>
|
|
26
|
+
getColumnWidgetEntries(column)
|
|
27
|
+
.map(entry => entry.widget)
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
|
|
30
|
+
export const getColumnPrimaryWidget = (column?: ColumnWithWidgets): string | undefined =>
|
|
31
|
+
getColumnWidgetEntries(column)[0]?.widget
|
|
32
|
+
|
|
33
|
+
export type DashboardConditionMatch = {
|
|
34
|
+
matches: boolean
|
|
35
|
+
resolved: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ResolvedColumnWidgetEntry = {
|
|
39
|
+
widget?: string
|
|
40
|
+
dashboardCondition?: DashboardCondition
|
|
41
|
+
} & DashboardConditionMatch
|
|
42
|
+
|
|
43
|
+
type ResolveColumnWidgetOptions = {
|
|
44
|
+
evaluateCondition?: (dashboardCondition?: DashboardCondition) => DashboardConditionMatch
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const resolveColumnWidgetEntry = (
|
|
48
|
+
column?: ColumnWithWidgets,
|
|
49
|
+
{ evaluateCondition = () => ({ matches: true, resolved: true }) }: ResolveColumnWidgetOptions = {}
|
|
50
|
+
): ResolvedColumnWidgetEntry | undefined => {
|
|
51
|
+
const widgetEntries = getColumnWidgetEntries(column)
|
|
52
|
+
|
|
53
|
+
for (const entry of widgetEntries) {
|
|
54
|
+
if (!entry.dashboardCondition) {
|
|
55
|
+
return { ...entry, matches: true, resolved: true }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const evaluation = evaluateCondition(entry.dashboardCondition)
|
|
59
|
+
if (evaluation.matches) {
|
|
60
|
+
return { ...entry, ...evaluation }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return widgetEntries.length ? { widget: undefined, matches: false, resolved: true } : undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const normalizeConditionalColumn = <T extends ColumnWithWidgets>(column: T): T => {
|
|
68
|
+
const conditionalWidgets = getConditionalWidgets(column)
|
|
69
|
+
|
|
70
|
+
if (conditionalWidgets.length > 1) {
|
|
71
|
+
return {
|
|
72
|
+
...column,
|
|
73
|
+
widget: undefined,
|
|
74
|
+
conditionalWidgets
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (conditionalWidgets.length === 1) {
|
|
79
|
+
const [entry] = conditionalWidgets
|
|
80
|
+
if (entry.dashboardCondition) {
|
|
81
|
+
return {
|
|
82
|
+
...column,
|
|
83
|
+
widget: undefined,
|
|
84
|
+
conditionalWidgets: [entry]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
...column,
|
|
90
|
+
widget: entry.widget,
|
|
91
|
+
conditionalWidgets: undefined
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
...column,
|
|
97
|
+
conditionalWidgets: undefined
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DashboardCondition } from '../types/ConfigRow'
|
|
2
|
+
|
|
3
|
+
export type DashboardConditionTypeOption = DashboardCondition['operator'] | ''
|
|
4
|
+
type DashboardConditionOperator = NonNullable<DashboardCondition['operator']>
|
|
5
|
+
|
|
6
|
+
export const DASHBOARD_CONDITION_TYPE_LABELS: Record<DashboardConditionOperator, string> = {
|
|
7
|
+
hasData: "Show when there's data",
|
|
8
|
+
hasNoData: "Show when there's no data",
|
|
9
|
+
columnHasAnyValue: 'Show when column has a value',
|
|
10
|
+
filtersIncomplete: 'Show when filters are incomplete'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DASHBOARD_CONDITION_TYPE_OPTIONS: { value: DashboardConditionTypeOption; label: string }[] = [
|
|
14
|
+
{ value: '', label: 'Always show' },
|
|
15
|
+
{ value: 'hasData', label: DASHBOARD_CONDITION_TYPE_LABELS.hasData },
|
|
16
|
+
{ value: 'hasNoData', label: DASHBOARD_CONDITION_TYPE_LABELS.hasNoData },
|
|
17
|
+
{ value: 'columnHasAnyValue', label: DASHBOARD_CONDITION_TYPE_LABELS.columnHasAnyValue },
|
|
18
|
+
{ value: 'filtersIncomplete', label: DASHBOARD_CONDITION_TYPE_LABELS.filtersIncomplete }
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
export const getColumnHasAnyValueSummaryParts = (columnName?: string) => {
|
|
22
|
+
if (!columnName) return undefined
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
prefix: 'Show based on the value in the ',
|
|
26
|
+
columnName,
|
|
27
|
+
suffix: ' column'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const getColumnHasAnyValueSummary = (columnName?: string) => {
|
|
32
|
+
const summaryParts = getColumnHasAnyValueSummaryParts(columnName)
|
|
33
|
+
|
|
34
|
+
if (!summaryParts) return DASHBOARD_CONDITION_TYPE_LABELS.columnHasAnyValue
|
|
35
|
+
|
|
36
|
+
return `${summaryParts.prefix}${summaryParts.columnName}${summaryParts.suffix}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const getDashboardConditionSummary = (dashboardCondition?: DashboardCondition) => {
|
|
40
|
+
if (!dashboardCondition?.operator) return 'Dashboard condition configured'
|
|
41
|
+
|
|
42
|
+
if (dashboardCondition.operator === 'columnHasAnyValue') {
|
|
43
|
+
return getColumnHasAnyValueSummary(dashboardCondition.columnName)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return DASHBOARD_CONDITION_TYPE_LABELS[dashboardCondition.operator] || 'Dashboard condition configured'
|
|
47
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Dashboard } from '../types/Dashboard'
|
|
2
|
+
import { ConfigRow, DashboardCondition } from '../types/ConfigRow'
|
|
3
|
+
import { createCoveId } from '@cdc/core/helpers/createCoveId'
|
|
4
|
+
import { getConditionalWidgets, hasConditionalWidgets } from './dashboardColumnWidgets'
|
|
5
|
+
import { filterData, isFilterAtResetState } from './filterData'
|
|
6
|
+
import {
|
|
7
|
+
dashboardConditionsSupportedForRow,
|
|
8
|
+
getApplicableFiltersForTarget,
|
|
9
|
+
SharedFilterTarget
|
|
10
|
+
} from './dashboardFilterTargets'
|
|
11
|
+
|
|
12
|
+
export type DashboardConditionEvaluation = {
|
|
13
|
+
matches: boolean
|
|
14
|
+
resolved: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const getDashboardConditionIds = (rows: ConfigRow[] = []) =>
|
|
18
|
+
rows.flatMap(row => {
|
|
19
|
+
if (Array.isArray(row)) return []
|
|
20
|
+
|
|
21
|
+
const rowConditionIds = row.dashboardCondition?.id ? [row.dashboardCondition.id] : []
|
|
22
|
+
const columnConditionIds =
|
|
23
|
+
row.columns?.flatMap(column =>
|
|
24
|
+
getConditionalWidgets(column).flatMap(entry =>
|
|
25
|
+
entry.dashboardCondition?.id ? [entry.dashboardCondition.id] : []
|
|
26
|
+
)
|
|
27
|
+
) || []
|
|
28
|
+
|
|
29
|
+
return [...rowConditionIds, ...columnConditionIds]
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// This runs during initial config preparation before coveUpdateWorker normalizes legacy dashboard rows.
|
|
33
|
+
// Preserve pre-4.24.3 array-shaped rows for packages/core/helpers/ver/4.24.3.ts, and tolerate rows
|
|
34
|
+
// without normalized columns. This constraint can be removed if CdcDashboard.tsx runs coveUpdateWorker
|
|
35
|
+
// before getUpdateConfig in formatInitialState.
|
|
36
|
+
export const ensureRowConditionIds = (rows: ConfigRow[]): ConfigRow[] => {
|
|
37
|
+
const existingConditionIds = new Set(getDashboardConditionIds(rows).map(id => String(id)))
|
|
38
|
+
|
|
39
|
+
return rows.map(row => {
|
|
40
|
+
if (Array.isArray(row)) return row
|
|
41
|
+
|
|
42
|
+
const nextRow = { ...row }
|
|
43
|
+
|
|
44
|
+
if (nextRow.dashboardCondition && !nextRow.dashboardCondition.id) {
|
|
45
|
+
const id = createCoveId('condition', { existingIds: existingConditionIds })
|
|
46
|
+
existingConditionIds.add(id)
|
|
47
|
+
nextRow.dashboardCondition = { ...nextRow.dashboardCondition, id }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(nextRow.columns)) {
|
|
51
|
+
nextRow.columns = nextRow.columns.map(column => {
|
|
52
|
+
if (hasConditionalWidgets(column)) {
|
|
53
|
+
return {
|
|
54
|
+
...column,
|
|
55
|
+
conditionalWidgets: getConditionalWidgets(column).map(entry => {
|
|
56
|
+
if (!entry.dashboardCondition || entry.dashboardCondition.id) return entry
|
|
57
|
+
|
|
58
|
+
const id = createCoveId('condition', { existingIds: existingConditionIds })
|
|
59
|
+
existingConditionIds.add(id)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
...entry,
|
|
63
|
+
dashboardCondition: {
|
|
64
|
+
...entry.dashboardCondition,
|
|
65
|
+
id
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return column
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return nextRow
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const dashboardConditionHasRequiredInputs = (dashboardCondition?: DashboardCondition) => {
|
|
80
|
+
if (!dashboardCondition?.operator) return false
|
|
81
|
+
if (dashboardCondition.operator === 'filtersIncomplete') return true
|
|
82
|
+
if (!dashboardCondition.datasetKey) return false
|
|
83
|
+
if (dashboardCondition.operator !== 'columnHasAnyValue') return true
|
|
84
|
+
|
|
85
|
+
return !!dashboardCondition.columnName && !!dashboardCondition.values?.length
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const dashboardConditionUsesFiltersIncomplete = (dashboardCondition?: DashboardCondition) =>
|
|
89
|
+
dashboardCondition?.operator === 'filtersIncomplete'
|
|
90
|
+
|
|
91
|
+
export const dashboardRowsUseFiltersIncomplete = (rows: ConfigRow[] = []) =>
|
|
92
|
+
rows.some(row => {
|
|
93
|
+
if (!dashboardConditionsSupportedForRow(row)) return false
|
|
94
|
+
if (dashboardConditionUsesFiltersIncomplete(row.dashboardCondition)) return true
|
|
95
|
+
|
|
96
|
+
return row.columns?.some(column =>
|
|
97
|
+
getConditionalWidgets(column).some(entry => dashboardConditionUsesFiltersIncomplete(entry.dashboardCondition))
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
export const getDashboardConditionDatasetKeys = (rows: ConfigRow[] = []) =>
|
|
102
|
+
rows.flatMap(row => {
|
|
103
|
+
if (!dashboardConditionsSupportedForRow(row)) return []
|
|
104
|
+
|
|
105
|
+
const rowDatasetKey = row.dashboardCondition?.datasetKey ? [row.dashboardCondition.datasetKey] : []
|
|
106
|
+
const columnDatasetKeys =
|
|
107
|
+
row.columns?.flatMap(column =>
|
|
108
|
+
getConditionalWidgets(column).flatMap(entry =>
|
|
109
|
+
entry.dashboardCondition?.datasetKey ? [entry.dashboardCondition.datasetKey] : []
|
|
110
|
+
)
|
|
111
|
+
) || []
|
|
112
|
+
|
|
113
|
+
return [...rowDatasetKey, ...columnDatasetKeys]
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const getDashboardConditionApplicableFilters = (
|
|
117
|
+
dashboard: Dashboard,
|
|
118
|
+
filterTarget: SharedFilterTarget,
|
|
119
|
+
datasetColumns: string[]
|
|
120
|
+
) => {
|
|
121
|
+
const candidateFilters = getApplicableFiltersForTarget(dashboard, filterTarget, { includeUnscoped: true })
|
|
122
|
+
if (!candidateFilters) return []
|
|
123
|
+
|
|
124
|
+
return candidateFilters.filter(filter => !!filter.columnName && datasetColumns.includes(filter.columnName))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const hasIncompleteFiltersForDashboardCondition = (
|
|
128
|
+
dashboardCondition: DashboardCondition | undefined,
|
|
129
|
+
dashboard: Dashboard,
|
|
130
|
+
filterTarget: SharedFilterTarget = ''
|
|
131
|
+
) => {
|
|
132
|
+
if (!dashboardCondition?.id) return false
|
|
133
|
+
|
|
134
|
+
const applicableFilters = getApplicableFiltersForTarget(dashboard, filterTarget, { includeUnscoped: true })
|
|
135
|
+
if (!applicableFilters) return false
|
|
136
|
+
|
|
137
|
+
return applicableFilters.some(isFilterAtResetState)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const getDashboardConditionFilteredData = (
|
|
141
|
+
dashboardCondition: DashboardCondition | undefined,
|
|
142
|
+
dashboard: Dashboard,
|
|
143
|
+
data: Record<string, any[]>,
|
|
144
|
+
filterTarget: SharedFilterTarget = ''
|
|
145
|
+
): Record<string, any>[] | undefined => {
|
|
146
|
+
if (!dashboardConditionHasRequiredInputs(dashboardCondition)) {
|
|
147
|
+
return undefined
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (dashboardCondition.operator === 'filtersIncomplete') {
|
|
151
|
+
return hasIncompleteFiltersForDashboardCondition(dashboardCondition, dashboard, filterTarget) ? [{}] : []
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const rawDataset = data[dashboardCondition.datasetKey]
|
|
155
|
+
if (!Array.isArray(rawDataset)) {
|
|
156
|
+
return undefined
|
|
157
|
+
}
|
|
158
|
+
const dataset = rawDataset || []
|
|
159
|
+
|
|
160
|
+
const datasetColumns = dataset[0] ? Object.keys(dataset[0]) : []
|
|
161
|
+
const applicableFilters = getDashboardConditionApplicableFilters(dashboard, filterTarget, datasetColumns)
|
|
162
|
+
|
|
163
|
+
if (applicableFilters.some(isFilterAtResetState)) {
|
|
164
|
+
return undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return applicableFilters.length ? filterData(applicableFilters, dataset) : dataset
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const evaluateDashboardCondition = (
|
|
171
|
+
dashboardCondition: DashboardCondition | undefined,
|
|
172
|
+
filteredData?: Record<string, any>[]
|
|
173
|
+
): DashboardConditionEvaluation => {
|
|
174
|
+
if (!dashboardConditionHasRequiredInputs(dashboardCondition) || filteredData === undefined) {
|
|
175
|
+
return { matches: false, resolved: false }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (dashboardCondition.operator === 'hasData') {
|
|
179
|
+
return { matches: filteredData.length > 0, resolved: true }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (dashboardCondition.operator === 'hasNoData') {
|
|
183
|
+
return { matches: filteredData.length === 0, resolved: true }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (dashboardCondition.operator === 'filtersIncomplete') {
|
|
187
|
+
return { matches: filteredData.length > 0, resolved: true }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const matches = filteredData.some(row => {
|
|
191
|
+
const currentValue = row?.[dashboardCondition.columnName]
|
|
192
|
+
return (
|
|
193
|
+
currentValue !== undefined &&
|
|
194
|
+
currentValue !== null &&
|
|
195
|
+
dashboardCondition.values?.some(value => String(currentValue) === String(value))
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return { matches, resolved: true }
|
|
200
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { ConfigRow, DashboardCondition } from '../types/ConfigRow'
|
|
2
|
+
import { DashboardConfig } from '../types/DashboardConfig'
|
|
3
|
+
import { Dashboard } from '../types/Dashboard'
|
|
4
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
5
|
+
import { getConditionalWidgets, hasConditionalWidgets } from './dashboardColumnWidgets'
|
|
6
|
+
import { getVizRowColumnLocator } from './getVizRowColumnLocator'
|
|
7
|
+
|
|
8
|
+
export type SharedFilterTarget = string | number
|
|
9
|
+
|
|
10
|
+
export type SharedFilterTargetOptions = {
|
|
11
|
+
nameLookup: Record<string, string>
|
|
12
|
+
options: SharedFilterTarget[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type DashboardConditionTarget = {
|
|
16
|
+
id: string
|
|
17
|
+
dashboardCondition: DashboardCondition
|
|
18
|
+
rowIndex: number
|
|
19
|
+
filterTarget: SharedFilterTarget
|
|
20
|
+
columnIndex?: number
|
|
21
|
+
entryIndex?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const dashboardConditionsSupportedForRow = (row?: ConfigRow) =>
|
|
25
|
+
!!row && !row.toggle && !row.multiVizColumn && !row.originalMultiVizColumn
|
|
26
|
+
|
|
27
|
+
const normalizeTarget = (target: SharedFilterTarget) => `${target}`
|
|
28
|
+
|
|
29
|
+
const dedupeSharedFilterTargets = (targets: SharedFilterTarget[]) =>
|
|
30
|
+
targets.filter(
|
|
31
|
+
(value, index, values) => values.findIndex(entry => normalizeTarget(entry) === normalizeTarget(value)) === index
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
export const matchesSharedFilterTarget = (usedBy: SharedFilter['usedBy'], target: SharedFilterTarget) =>
|
|
35
|
+
!!usedBy?.some(entry => normalizeTarget(entry) === normalizeTarget(target))
|
|
36
|
+
|
|
37
|
+
export const getDashboardConditionTargets = (rows: ConfigRow[]): DashboardConditionTarget[] =>
|
|
38
|
+
rows.reduce((targets, row, rowIndex) => {
|
|
39
|
+
if (!dashboardConditionsSupportedForRow(row)) return targets
|
|
40
|
+
|
|
41
|
+
if (row.dashboardCondition?.id) {
|
|
42
|
+
targets.push({
|
|
43
|
+
id: row.dashboardCondition.id,
|
|
44
|
+
dashboardCondition: row.dashboardCondition,
|
|
45
|
+
rowIndex,
|
|
46
|
+
filterTarget: rowIndex
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
row.columns?.forEach((column, columnIndex) => {
|
|
51
|
+
if (hasConditionalWidgets(column)) {
|
|
52
|
+
getConditionalWidgets(column).forEach((entry, entryIndex) => {
|
|
53
|
+
if (!entry.dashboardCondition?.id) return
|
|
54
|
+
targets.push({
|
|
55
|
+
id: entry.dashboardCondition.id,
|
|
56
|
+
dashboardCondition: entry.dashboardCondition,
|
|
57
|
+
rowIndex,
|
|
58
|
+
filterTarget: row.dataKey ? rowIndex : entry.widget,
|
|
59
|
+
columnIndex,
|
|
60
|
+
entryIndex
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return targets
|
|
68
|
+
}, [] as DashboardConditionTarget[])
|
|
69
|
+
|
|
70
|
+
export const getSharedFilterTargetOptions = (
|
|
71
|
+
config: DashboardConfig,
|
|
72
|
+
filter: Pick<SharedFilter, 'usedBy' | 'setBy'>
|
|
73
|
+
): SharedFilterTargetOptions => {
|
|
74
|
+
const nameLookup: Record<string, string> = {}
|
|
75
|
+
const options: SharedFilterTarget[] = []
|
|
76
|
+
const vizRowColumnLocator = getVizRowColumnLocator(config.rows)
|
|
77
|
+
|
|
78
|
+
Object.keys(config.visualizations).forEach(vizKey => {
|
|
79
|
+
const vizLookup = vizRowColumnLocator[vizKey]
|
|
80
|
+
if (!vizLookup) return
|
|
81
|
+
|
|
82
|
+
const viz = config.visualizations[vizKey] as any
|
|
83
|
+
if (viz.type === 'dashboardFilters') return
|
|
84
|
+
|
|
85
|
+
const vizName =
|
|
86
|
+
viz.visualizationType === 'markup-include'
|
|
87
|
+
? viz.contentEditor?.title || vizKey
|
|
88
|
+
: viz.general?.title || viz.title || vizKey
|
|
89
|
+
const dataConfiguredOnRow = config.rows[vizLookup.row]?.dataKey
|
|
90
|
+
|
|
91
|
+
nameLookup[vizKey] = vizName
|
|
92
|
+
if (filter.setBy !== vizKey && !viz.usesSharedFilter && !dataConfiguredOnRow) {
|
|
93
|
+
options.push(vizKey)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
config.rows.forEach((row, rowIndex) => {
|
|
98
|
+
if (row.dataKey || (dashboardConditionsSupportedForRow(row) && row.dashboardCondition)) {
|
|
99
|
+
nameLookup[`${rowIndex}`] = `Row ${rowIndex + 1}`
|
|
100
|
+
options.push(rowIndex)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Preserve legacy row targets that are no longer offered as new selectable targets.
|
|
105
|
+
filter.usedBy?.forEach(target => {
|
|
106
|
+
const isNumericRowTarget = typeof target === 'number' || (typeof target === 'string' && /^-?\d+$/.test(target))
|
|
107
|
+
if (isNumericRowTarget) {
|
|
108
|
+
const rowIndex = Number(target)
|
|
109
|
+
if (!config.rows[rowIndex]) return
|
|
110
|
+
nameLookup[`${target}`] = `Row ${rowIndex + 1}`
|
|
111
|
+
options.push(target)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
nameLookup,
|
|
117
|
+
options: dedupeSharedFilterTargets(options)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const remapRowTargetsInSharedFilters = (
|
|
122
|
+
sharedFilters: SharedFilter[],
|
|
123
|
+
remapRowIndex: (rowIndex: number) => number | null
|
|
124
|
+
): SharedFilter[] =>
|
|
125
|
+
sharedFilters.map(sharedFilter => {
|
|
126
|
+
if (!sharedFilter.usedBy) return sharedFilter
|
|
127
|
+
|
|
128
|
+
const nextUsedBy = dedupeSharedFilterTargets(
|
|
129
|
+
sharedFilter.usedBy.flatMap(target => {
|
|
130
|
+
const isNumericRowTarget = typeof target === 'number' || (typeof target === 'string' && /^-?\d+$/.test(target))
|
|
131
|
+
if (!isNumericRowTarget) return [target]
|
|
132
|
+
|
|
133
|
+
const mappedRowIndex = remapRowIndex(Number(target))
|
|
134
|
+
return mappedRowIndex === null ? [] : [typeof target === 'number' ? mappedRowIndex : `${mappedRowIndex}`]
|
|
135
|
+
})
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return nextUsedBy === sharedFilter.usedBy ? sharedFilter : { ...sharedFilter, usedBy: nextUsedBy }
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
export const getApplicableFiltersForTarget = (
|
|
142
|
+
dashboard: Dashboard,
|
|
143
|
+
target: SharedFilterTarget,
|
|
144
|
+
options?: { includeUnscoped?: boolean }
|
|
145
|
+
): SharedFilter[] | false => {
|
|
146
|
+
const applicableFilters =
|
|
147
|
+
dashboard.sharedFilters?.filter(sharedFilter => {
|
|
148
|
+
if (options?.includeUnscoped && (!sharedFilter.usedBy || sharedFilter.usedBy.length === 0)) {
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return matchesSharedFilterTarget(sharedFilter.usedBy, target)
|
|
153
|
+
}) || []
|
|
154
|
+
|
|
155
|
+
return applicableFilters.length > 0 ? applicableFilters : false
|
|
156
|
+
}
|
|
@@ -108,14 +108,9 @@ export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] =
|
|
|
108
108
|
return []
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const filteredData = filterDataByTier(_data, filters, i + 1)
|
|
115
|
-
|
|
116
|
-
if (lastIteration) {
|
|
117
|
-
// not sure if this last run of filterDataByTier() function is necessary.
|
|
118
|
-
return filterDataByTier(filteredData, filters, maxTier - 1)
|
|
119
|
-
}
|
|
111
|
+
let filteredData = _data
|
|
112
|
+
for (let tier = 1; tier <= maxTier; tier++) {
|
|
113
|
+
filteredData = filterDataByTier(filteredData, filters, tier)
|
|
120
114
|
}
|
|
115
|
+
return filteredData
|
|
121
116
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FILTER_STYLE } from '../types/FilterStyles'
|
|
2
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
3
|
+
|
|
4
|
+
export const isVisibleDashboardFilter = (filter?: SharedFilter | null): boolean =>
|
|
5
|
+
Boolean(
|
|
6
|
+
filter &&
|
|
7
|
+
(filter.type === 'urlfilter' ||
|
|
8
|
+
filter.showDropdown ||
|
|
9
|
+
filter.filterStyle === FILTER_STYLE.nestedDropdown ||
|
|
10
|
+
filter.filterStyle === FILTER_STYLE.tabSimple)
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export const hasVisibleDashboardFiltersForIndexes = (
|
|
14
|
+
sharedFilters?: SharedFilter[] | null,
|
|
15
|
+
sharedFilterIndexes?: (number | string)[] | null
|
|
16
|
+
): boolean =>
|
|
17
|
+
Boolean(
|
|
18
|
+
sharedFilterIndexes?.length &&
|
|
19
|
+
sharedFilterIndexes.map(Number).some(filterIndex => isVisibleDashboardFilter(sharedFilters?.[filterIndex]))
|
|
20
|
+
)
|
|
@@ -51,9 +51,9 @@ export const cleanSharedFilters = (config: DashboardConfig) => {
|
|
|
51
51
|
if (config.dashboard?.sharedFilters) {
|
|
52
52
|
const recursiveRemoveFilters = (sharedFilters, visualizations: Record<string, AnyVisualization>) => {
|
|
53
53
|
const usedFilters = _.uniq(
|
|
54
|
-
Object.values(visualizations).reduce((acc, viz) => {
|
|
54
|
+
Object.values(visualizations).reduce((acc: number[], viz) => {
|
|
55
55
|
if (viz.type === 'dashboardFilters') {
|
|
56
|
-
acc = acc.concat(viz.sharedFilterIndexes)
|
|
56
|
+
acc = acc.concat((viz.sharedFilterIndexes ?? []).map(Number))
|
|
57
57
|
}
|
|
58
58
|
return acc
|
|
59
59
|
}, [])
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { DashboardState } from '../store/dashboard.reducer'
|
|
2
2
|
import { Dashboard } from '../types/Dashboard'
|
|
3
3
|
import { SharedFilter } from '../types/SharedFilter'
|
|
4
|
+
import { getDashboardConditionFilteredData } from './dashboardConditions'
|
|
5
|
+
import { getApplicableFiltersForTarget, getDashboardConditionTargets } from './dashboardFilterTargets'
|
|
4
6
|
import { filterData } from './filterData'
|
|
5
7
|
import { getFormattedData } from './getFormattedData'
|
|
6
8
|
import { getVizKeys } from './getVizKeys'
|
|
7
9
|
|
|
8
10
|
export const getApplicableFilters = (dashboard: Dashboard, key: string | number): false | SharedFilter[] => {
|
|
9
|
-
|
|
10
|
-
sharedFilter =>
|
|
11
|
-
(sharedFilter.usedBy && sharedFilter.usedBy.indexOf(`${key}`) !== -1) || sharedFilter.usedBy?.indexOf(key) !== -1
|
|
12
|
-
)
|
|
13
|
-
return c?.length > 0 ? c : false
|
|
11
|
+
return getApplicableFiltersForTarget(dashboard, key, { includeUnscoped: true })
|
|
14
12
|
}
|
|
15
13
|
export const getFilteredData = (
|
|
16
14
|
state: DashboardState,
|
|
@@ -45,5 +43,20 @@ export const getFilteredData = (
|
|
|
45
43
|
}
|
|
46
44
|
}
|
|
47
45
|
})
|
|
46
|
+
const dashboardConditionTargets = getDashboardConditionTargets(config.rows)
|
|
47
|
+
dashboardConditionTargets.forEach(conditionTarget => {
|
|
48
|
+
delete newFilteredData[conditionTarget.id]
|
|
49
|
+
|
|
50
|
+
const filteredData = getDashboardConditionFilteredData(
|
|
51
|
+
conditionTarget.dashboardCondition,
|
|
52
|
+
config.dashboard,
|
|
53
|
+
(dataOverride || state.data) as Record<string, any[]>,
|
|
54
|
+
conditionTarget.filterTarget
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if (filteredData !== undefined) {
|
|
58
|
+
newFilteredData[conditionTarget.id] = filteredData
|
|
59
|
+
}
|
|
60
|
+
})
|
|
48
61
|
return newFilteredData
|
|
49
62
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { DashboardState } from '../store/dashboard.reducer'
|
|
2
2
|
import { DashboardConfig as Config, DashboardConfig } from '../types/DashboardConfig'
|
|
3
|
+
import { ensureRowConditionIds, getDashboardConditionFilteredData } from './dashboardConditions'
|
|
4
|
+
import { getApplicableFiltersForTarget, getDashboardConditionTargets } from './dashboardFilterTargets'
|
|
3
5
|
import { filterData } from './filterData'
|
|
4
6
|
import { generateValuesForFilter } from './generateValuesForFilter'
|
|
5
7
|
import { getFormattedData } from './getFormattedData'
|
|
@@ -16,6 +18,7 @@ export const getUpdateConfig =
|
|
|
16
18
|
(state: UpdateState) =>
|
|
17
19
|
(newConfig, dataOverride?: Object): [Config, Object] => {
|
|
18
20
|
let newFilteredData = {}
|
|
21
|
+
newConfig.rows = ensureRowConditionIds(newConfig.rows)
|
|
19
22
|
let visualizationKeys = getVizKeys(newConfig)
|
|
20
23
|
|
|
21
24
|
const vizRowColumnLocator = getVizRowColumnLocator(newConfig.rows)
|
|
@@ -55,13 +58,13 @@ export const getUpdateConfig =
|
|
|
55
58
|
})
|
|
56
59
|
|
|
57
60
|
visualizationKeys.forEach(visualizationKey => {
|
|
58
|
-
const
|
|
59
|
-
if (newConfig.rows[row]?.
|
|
60
|
-
const applicableFilters = newConfig.dashboard
|
|
61
|
-
|
|
62
|
-
)
|
|
61
|
+
const locator = vizRowColumnLocator[visualizationKey]
|
|
62
|
+
if (newConfig.rows[locator?.row]?.dataKey) return // data configured on the row level
|
|
63
|
+
const applicableFilters = getApplicableFiltersForTarget(newConfig.dashboard, visualizationKey, {
|
|
64
|
+
includeUnscoped: true
|
|
65
|
+
})
|
|
63
66
|
|
|
64
|
-
if (applicableFilters
|
|
67
|
+
if (applicableFilters) {
|
|
65
68
|
const visualization = newConfig.visualizations[visualizationKey]
|
|
66
69
|
const _newConfigDataSet = newConfig.datasets[visualization.dataKey]
|
|
67
70
|
const formattedData = getFormattedData(
|
|
@@ -74,16 +77,44 @@ export const getUpdateConfig =
|
|
|
74
77
|
})
|
|
75
78
|
|
|
76
79
|
newConfig.rows.forEach((row, rowIndex) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
if (!row.dataKey) return
|
|
81
|
+
|
|
82
|
+
const applicableFilters = getApplicableFiltersForTarget(newConfig.dashboard, rowIndex, {
|
|
83
|
+
includeUnscoped: true
|
|
84
|
+
})
|
|
80
85
|
|
|
81
|
-
if (applicableFilters
|
|
82
|
-
const
|
|
83
|
-
const
|
|
86
|
+
if (applicableFilters) {
|
|
87
|
+
const datasetData = newConfig.datasets?.[row.dataKey]?.data
|
|
88
|
+
const formattedData = getFormattedData(row.data || datasetData, row.dataDescription)
|
|
89
|
+
const _data = formattedData || (dataOverride || state.data)[row.dataKey]
|
|
84
90
|
newFilteredData[rowIndex] = filterData(applicableFilters, _data)
|
|
85
91
|
}
|
|
86
92
|
})
|
|
93
|
+
|
|
94
|
+
const conditionDataSource = {
|
|
95
|
+
...(state.data || {}),
|
|
96
|
+
...(dataOverride || {}),
|
|
97
|
+
...Object.keys(newConfig.datasets || {}).reduce((acc, datasetKey) => {
|
|
98
|
+
const datasetData = newConfig.datasets[datasetKey]?.data
|
|
99
|
+
if (datasetData) {
|
|
100
|
+
acc[datasetKey] = datasetData
|
|
101
|
+
}
|
|
102
|
+
return acc
|
|
103
|
+
}, {} as Record<string, any[]>)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getDashboardConditionTargets(newConfig.rows).forEach(conditionTarget => {
|
|
107
|
+
const filteredData = getDashboardConditionFilteredData(
|
|
108
|
+
conditionTarget.dashboardCondition,
|
|
109
|
+
newConfig.dashboard,
|
|
110
|
+
conditionDataSource,
|
|
111
|
+
conditionTarget.filterTarget
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if (filteredData !== undefined) {
|
|
115
|
+
newFilteredData[conditionTarget.id] = filteredData
|
|
116
|
+
}
|
|
117
|
+
})
|
|
87
118
|
}
|
|
88
119
|
//Enforce default values that need to be calculated at runtime
|
|
89
120
|
newConfig.runtime = {}
|