@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.
Files changed (91) hide show
  1. package/CONFIG.md +77 -30
  2. package/LICENSE +201 -0
  3. package/dist/cdcdashboard.js +49936 -49166
  4. package/examples/dashboard-conditions-filters-incomplete.json +221 -0
  5. package/examples/dashboard-missing-datasets-multi.json +174 -0
  6. package/examples/dashboard-missing-datasets-single.json +121 -0
  7. package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
  8. package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
  9. package/examples/dashboard-stale-dataset-keys.json +181 -0
  10. package/examples/dashboard-tiered-filter-regression.json +190 -0
  11. package/examples/private/cfa-dashboard.json +651 -0
  12. package/examples/private/data-bite-wrap.json +6936 -0
  13. package/examples/private/multi-dash-fix.json +16963 -0
  14. package/examples/private/versions.json +41612 -0
  15. package/examples/us-map-filter-example.json +1074 -0
  16. package/package.json +9 -9
  17. package/src/CdcDashboard.tsx +6 -2
  18. package/src/CdcDashboardComponent.tsx +178 -87
  19. package/src/DashboardCopyPasteContext.test.tsx +33 -0
  20. package/src/DashboardCopyPasteContext.tsx +48 -0
  21. package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
  22. package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
  23. package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
  24. package/src/_stories/Dashboard.stories.tsx +294 -0
  25. package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
  26. package/src/components/Column.test.tsx +176 -0
  27. package/src/components/Column.tsx +214 -13
  28. package/src/components/DashboardConditionModal.test.tsx +420 -0
  29. package/src/components/DashboardConditionModal.tsx +367 -0
  30. package/src/components/DashboardConditionSummary.tsx +59 -0
  31. package/src/components/DashboardEditors.tsx +8 -0
  32. package/src/components/DashboardFilters/DashboardFilters.test.tsx +139 -1
  33. package/src/components/DashboardFilters/DashboardFilters.tsx +192 -174
  34. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
  35. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +41 -2
  36. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +180 -3
  37. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +15 -32
  38. package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
  39. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
  40. package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
  41. package/src/components/DataDesignerModal.tsx +2 -1
  42. package/src/components/Grid.tsx +8 -4
  43. package/src/components/Header/Header.tsx +36 -17
  44. package/src/components/Row.test.tsx +228 -0
  45. package/src/components/Row.tsx +93 -18
  46. package/src/components/VisualizationRow.test.tsx +396 -0
  47. package/src/components/VisualizationRow.tsx +110 -35
  48. package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
  49. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
  50. package/src/components/Widget/Widget.test.tsx +218 -0
  51. package/src/components/Widget/Widget.tsx +119 -17
  52. package/src/components/Widget/widget.styles.css +31 -18
  53. package/src/components/dashboard-condition-modal.css +76 -0
  54. package/src/components/dashboard-condition-summary.css +87 -0
  55. package/src/helpers/addValuesToDashboardFilters.ts +3 -5
  56. package/src/helpers/addVisualization.ts +15 -4
  57. package/src/helpers/cloneDashboardWidget.ts +127 -0
  58. package/src/helpers/dashboardColumnWidgets.ts +99 -0
  59. package/src/helpers/dashboardConditionUi.ts +47 -0
  60. package/src/helpers/dashboardConditions.ts +200 -0
  61. package/src/helpers/dashboardFilterTargets.ts +156 -0
  62. package/src/helpers/filterData.ts +4 -9
  63. package/src/helpers/filterVisibility.ts +20 -0
  64. package/src/helpers/formatConfigBeforeSave.ts +2 -2
  65. package/src/helpers/getFilteredData.ts +18 -5
  66. package/src/helpers/getUpdateConfig.ts +43 -12
  67. package/src/helpers/getVizRowColumnLocator.ts +11 -1
  68. package/src/helpers/iconHash.tsx +9 -3
  69. package/src/helpers/mapDataToConfig.ts +31 -29
  70. package/src/helpers/reloadURLHelpers.ts +25 -5
  71. package/src/helpers/removeDashboardFilter.ts +33 -33
  72. package/src/helpers/tests/addVisualization.test.ts +53 -9
  73. package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
  74. package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
  75. package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
  76. package/src/helpers/tests/dashboardConditions.test.ts +428 -0
  77. package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
  78. package/src/helpers/tests/getFilteredData.test.ts +265 -86
  79. package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
  80. package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
  81. package/src/index.tsx +6 -3
  82. package/src/scss/grid.scss +249 -20
  83. package/src/scss/main.scss +108 -29
  84. package/src/store/dashboard.actions.ts +17 -4
  85. package/src/store/dashboard.reducer.test.ts +538 -0
  86. package/src/store/dashboard.reducer.ts +135 -22
  87. package/src/test/CdcDashboard.test.tsx +148 -0
  88. package/src/test/CdcDashboardComponent.test.tsx +935 -2
  89. package/src/types/ConfigRow.ts +15 -0
  90. package/src/types/DashboardFilters.ts +4 -0
  91. 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
- for (let i = 0; i < maxTier; i++) {
112
- const lastIteration = i === maxTier - 1
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
- const c = dashboard.sharedFilters?.filter(
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 row = vizRowColumnLocator[visualizationKey]
59
- if (newConfig.rows[row]?.datakey) return // data configured on the row level
60
- const applicableFilters = newConfig.dashboard.sharedFilters.filter(
61
- sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1
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.length > 0) {
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
- const applicableFilters = newConfig.dashboard.sharedFilters.filter(
78
- sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(rowIndex) !== -1
79
- )
80
+ if (!row.dataKey) return
81
+
82
+ const applicableFilters = getApplicableFiltersForTarget(newConfig.dashboard, rowIndex, {
83
+ includeUnscoped: true
84
+ })
80
85
 
81
- if (applicableFilters.length > 0) {
82
- const formattedData = getFormattedData(row.data, row.dataDescription)
83
- const _data = formattedData || (dataOverride || state.data)[rowIndex]
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 = {}