@cdc/dashboard 4.24.1 → 4.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdcdashboard.js +117195 -108041
- package/examples/{private/DEV-6574.json → DEV-6574.json} +8 -8
- package/examples/filters/Alabama.json +72 -0
- package/examples/zika.json +2274 -0
- package/index.html +5 -3
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +124 -991
- package/src/CdcDashboardComponent.tsx +903 -0
- package/src/_stories/Dashboard.stories.tsx +2 -2
- package/src/components/Column.tsx +15 -12
- package/src/components/Header/Header.tsx +18 -4
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +106 -0
- package/src/components/MultiConfigTabs/MultiTabs.tsx +30 -0
- package/src/components/MultiConfigTabs/index.tsx +8 -0
- package/src/components/MultiConfigTabs/multiconfigtabs.styles.css +32 -0
- package/src/components/Widget.tsx +25 -9
- package/src/helpers/generateValuesForFilter.ts +1 -1
- package/src/helpers/getUpdateConfig.ts +6 -2
- package/src/helpers/processData.ts +13 -0
- package/src/helpers/processDataLegacy.ts +14 -0
- package/src/{index.jsx → index.tsx} +2 -2
- package/src/scss/editor-panel.scss +14 -11
- package/src/scss/grid.scss +4 -6
- package/src/scss/main.scss +2 -8
- package/src/store/dashboard.actions.ts +10 -4
- package/src/store/dashboard.reducer.ts +74 -3
- package/src/types/ConfigRow.ts +6 -0
- package/src/types/Dashboard.ts +11 -0
- package/src/types/{Config.ts → DashboardConfig.ts} +8 -14
- package/src/types/InitialState.ts +10 -0
- package/src/types/MultiDashboard.ts +11 -0
- package/examples/private/DEV-5189-2.json +0 -935
- package/examples/private/DEV-5189.json +0 -1266
- package/examples/private/dash-scaling.json +0 -45325
- package/examples/private/epi-chart.json +0 -813
- package/examples/private/epi-dash.json +0 -457
- package/examples/private/epi-new.json +0 -994
- package/examples/private/filters/Alabama.json +0 -4217
- package/examples/private/new-epi-csv.json +0 -358
- package/examples/private/new-epi.json +0 -358
- package/examples/private/no-lines.json +0 -8432
- package/examples/private/tick-bold-data.json +0 -82325
- package/examples/private/tick-bold.json +0 -164678
- package/examples/private/visits.json +0 -9985
- /package/examples/{private/filters → filters}/Alaska.json +0 -0
- /package/examples/{private/filters → filters}/Arkansas.json +0 -0
- /package/examples/{private/filters → filters}/California.json +0 -0
- /package/examples/{private/filters → filters}/Colorado.json +0 -0
- /package/examples/{private/filters → filters}/Connecticut.json +0 -0
- /package/examples/{private/filters → filters}/Delaware.json +0 -0
- /package/examples/{private/filters → filters}/DistrictofColumbia.json +0 -0
- /package/examples/{private/filters → filters}/Florida.json +0 -0
- /package/examples/{private/filters → filters}/States.json +0 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useMemo, useReducer, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
// IE11
|
|
4
|
+
// import 'core-js/stable'
|
|
5
|
+
import 'whatwg-fetch'
|
|
6
|
+
import ResizeObserver from 'resize-observer-polyfill'
|
|
7
|
+
|
|
8
|
+
import { DndProvider } from 'react-dnd'
|
|
9
|
+
import { HTML5Backend } from 'react-dnd-html5-backend'
|
|
10
|
+
|
|
11
|
+
import parse from 'html-react-parser'
|
|
12
|
+
|
|
13
|
+
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
14
|
+
import { GlobalContextProvider } from '@cdc/core/components/GlobalContext'
|
|
15
|
+
import { DashboardContext, DashboardDispatchContext } from './DashboardContext'
|
|
16
|
+
|
|
17
|
+
import OverlayFrame from '@cdc/core/components/ui/OverlayFrame'
|
|
18
|
+
import Loading from '@cdc/core/components/Loading'
|
|
19
|
+
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
20
|
+
import getViewport from '@cdc/core/helpers/getViewport'
|
|
21
|
+
|
|
22
|
+
import CdcMap from '@cdc/map'
|
|
23
|
+
import CdcChart from '@cdc/chart'
|
|
24
|
+
import CdcDataBite from '@cdc/data-bite'
|
|
25
|
+
import CdcWaffleChart from '@cdc/waffle-chart'
|
|
26
|
+
import CdcMarkupInclude from '@cdc/markup-include'
|
|
27
|
+
import CdcFilteredText from '@cdc/filtered-text'
|
|
28
|
+
|
|
29
|
+
import Grid from './components/Grid'
|
|
30
|
+
import Header, { FilterBehavior } from './components/Header/Header'
|
|
31
|
+
import DataTable from '@cdc/core/components/DataTable'
|
|
32
|
+
import MediaControls from '@cdc/core/components/MediaControls'
|
|
33
|
+
|
|
34
|
+
import './scss/main.scss'
|
|
35
|
+
import '@cdc/core/styles/v2/main.scss'
|
|
36
|
+
import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
|
|
37
|
+
import { capitalizeSplitAndJoin } from '@cdc/core/helpers/cove/string'
|
|
38
|
+
|
|
39
|
+
import VisualizationsPanel from './components/VisualizationsPanel'
|
|
40
|
+
import dashboardReducer from './store/dashboard.reducer'
|
|
41
|
+
import { filterData } from './helpers/filterData'
|
|
42
|
+
import { getFormattedData } from './helpers/getFormattedData'
|
|
43
|
+
import { getVizKeys } from './helpers/getVizKeys'
|
|
44
|
+
import Title from '@cdc/core/components/ui/Title'
|
|
45
|
+
import { type TableConfig } from '@cdc/core/components/DataTable/types/TableConfig'
|
|
46
|
+
|
|
47
|
+
// types
|
|
48
|
+
import { type SharedFilter } from './types/SharedFilter'
|
|
49
|
+
import { type APIFilter } from './types/APIFilter'
|
|
50
|
+
import { type Visualization } from '@cdc/core/types/Visualization'
|
|
51
|
+
import { type WCMSProps } from '@cdc/core/types/WCMSProps'
|
|
52
|
+
import { type InitialState } from './types/InitialState'
|
|
53
|
+
import MultiTabs from './components/MultiConfigTabs'
|
|
54
|
+
import _ from 'lodash'
|
|
55
|
+
import EditorContext from '../../editor/src/ConfigContext'
|
|
56
|
+
|
|
57
|
+
type DropdownOptions = Record<'value' | 'text', string>[]
|
|
58
|
+
|
|
59
|
+
type APIFilterDropdowns = {
|
|
60
|
+
// null means still loading
|
|
61
|
+
[filtername: string]: null | DropdownOptions
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
|
|
65
|
+
initialState: InitialState
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default function CdcDashboard({ initialState, isEditor = false, isDebug = false }: DashboardProps) {
|
|
69
|
+
const [state, dispatch] = useReducer(dashboardReducer, initialState)
|
|
70
|
+
console.log('state', state)
|
|
71
|
+
const editorContext = useContext(EditorContext)
|
|
72
|
+
const [apiFilterDropdowns, setAPIFilterDropdowns] = useState<APIFilterDropdowns>({})
|
|
73
|
+
const [currentViewport, setCurrentViewport] = useState('lg')
|
|
74
|
+
const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
|
|
75
|
+
|
|
76
|
+
const replacements = {
|
|
77
|
+
'Remove Spaces': '',
|
|
78
|
+
'Keep Spaces': ' ',
|
|
79
|
+
'Replace With Underscore': '_'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const inNoDataState = useMemo(() => {
|
|
83
|
+
const vals = Object.values(state.data)
|
|
84
|
+
if (!vals.length) return true
|
|
85
|
+
return vals.some(val => val === undefined)
|
|
86
|
+
}, [state.data])
|
|
87
|
+
|
|
88
|
+
const getAutoLoadVisualization = (): Visualization | undefined => {
|
|
89
|
+
const autoLoadViz = Object.values(state.config.visualizations).filter(vis => {
|
|
90
|
+
return vis.autoLoad && vis.type === 'filter-dropdowns'
|
|
91
|
+
})
|
|
92
|
+
if (autoLoadViz.length === 0) return
|
|
93
|
+
if (autoLoadViz.length > 1) throw new Error('Only one filter row can be autoloaded')
|
|
94
|
+
return autoLoadViz[0]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const transform = new DataTransform()
|
|
98
|
+
|
|
99
|
+
const getApiFilterKey = ({ apiEndpoint, heirarchyLookup }: APIFilter) => {
|
|
100
|
+
return apiEndpoint + (heirarchyLookup || '')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const setAutoLoadDefaultValue = (sharedFilterIndex: number, filterDropdowns: DropdownOptions) => {
|
|
104
|
+
const autoLoadViz = getAutoLoadVisualization()
|
|
105
|
+
if (!autoLoadViz) return // no autoLoading happening
|
|
106
|
+
const notIncludedInAutoLoad = autoLoadViz.hide
|
|
107
|
+
if (notIncludedInAutoLoad.includes(sharedFilterIndex)) {
|
|
108
|
+
// we don't want to auto load it
|
|
109
|
+
return
|
|
110
|
+
} else {
|
|
111
|
+
const sharedFilter = state.config.dashboard.sharedFilters[sharedFilterIndex]
|
|
112
|
+
if (sharedFilter.active) return // a value has already been selected.
|
|
113
|
+
const filterParents = state.config.dashboard.sharedFilters.filter(f => sharedFilter.parents?.includes(f.key))
|
|
114
|
+
const notAllParentFiltersSelected = filterParents.some(p => !p.active)
|
|
115
|
+
if (filterParents && notAllParentFiltersSelected) return
|
|
116
|
+
const defaultFilterDropdown = filterDropdowns.find(({ value }) => value === sharedFilter.apiFilter!.defaultValue)
|
|
117
|
+
let defaultValue = defaultFilterDropdown?.value || filterDropdowns[0].value
|
|
118
|
+
changeFilterActive(sharedFilterIndex, defaultValue)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const loadAPIFilters = async () => {
|
|
123
|
+
if (state.config.dashboard.sharedFilters) {
|
|
124
|
+
const sharedAPIFilters = state.config.dashboard.sharedFilters.filter(f => f.apiFilter)
|
|
125
|
+
const loadingFilterMemo: APIFilterDropdowns = sharedAPIFilters.reduce((acc, curr) => {
|
|
126
|
+
const _key = getApiFilterKey(curr.apiFilter!)
|
|
127
|
+
if (apiFilterDropdowns[_key] != null) return acc // don't overwrite fetched data.
|
|
128
|
+
acc[_key] = null
|
|
129
|
+
return acc
|
|
130
|
+
}, {})
|
|
131
|
+
setAPIFilterDropdowns({ ...apiFilterDropdowns, ...loadingFilterMemo })
|
|
132
|
+
const filterLookup = new Map(sharedAPIFilters.map(filter => [getApiFilterKey(filter.apiFilter!), filter.apiFilter!]))
|
|
133
|
+
const getParentParams = (childFilter: SharedFilter): Record<'key' | 'value', string>[] | null => {
|
|
134
|
+
const _parents = sharedAPIFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
|
|
135
|
+
if (!_parents.length) return null
|
|
136
|
+
return _parents.map(({ queryParameter, queuedActive }) => ({ key: queryParameter || '', value: queuedActive || '' }))
|
|
137
|
+
}
|
|
138
|
+
const getFilterValues = (filterData: Object | Array<Object>, apiFilter: APIFilter): DropdownOptions => {
|
|
139
|
+
const { textSelector, valueSelector, heirarchyLookup } = apiFilter
|
|
140
|
+
if (heirarchyLookup) {
|
|
141
|
+
const heirarchy = heirarchyLookup!.split('.')
|
|
142
|
+
const selector = heirarchy.shift() // pop first element
|
|
143
|
+
return getFilterValues(selector ? filterData[selector] : filterData, { ...apiFilter, heirarchyLookup: heirarchy.join('.') })
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(filterData)) throw new Error('the filter data has requires a heirarchy path to access the filter values, This should be in the format key.subkey.subsubkey')
|
|
146
|
+
return filterData.map(v => ({ text: v[textSelector], value: v[valueSelector] }))
|
|
147
|
+
}
|
|
148
|
+
state.config.dashboard.sharedFilters.forEach(async (filter, index) => {
|
|
149
|
+
if (!filter.apiFilter) return
|
|
150
|
+
const baseEndpoint = filter.apiFilter.apiEndpoint
|
|
151
|
+
const _key = getApiFilterKey(filter.apiFilter)
|
|
152
|
+
const params = getParentParams(filter)
|
|
153
|
+
const notAllParentsSelected = params?.some(({ value }) => value === '')
|
|
154
|
+
if (notAllParentsSelected) return // don't send request for dependent children filter options
|
|
155
|
+
if (apiFilterDropdowns[_key] && !params && filter.filterBy === 'Query String') return // don't reload filter unless it's a child
|
|
156
|
+
const endpoint = baseEndpoint + (params ? gatherQueryParams(params) : '')
|
|
157
|
+
|
|
158
|
+
fetch(endpoint)
|
|
159
|
+
.then(resp => resp.json())
|
|
160
|
+
.then(data => {
|
|
161
|
+
const apiFilter = filterLookup.get(_key) as APIFilter
|
|
162
|
+
const _filterValues = getFilterValues(data, apiFilter)
|
|
163
|
+
setAPIFilterDropdowns(dropdowns => ({ ...dropdowns, [_key]: _filterValues }))
|
|
164
|
+
setAutoLoadDefaultValue(index, _filterValues)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const reloadURLData = async () => {
|
|
171
|
+
const { config } = state
|
|
172
|
+
if (config.datasets) {
|
|
173
|
+
let newData = { ...state.data }
|
|
174
|
+
let newDatasets = { ...config.datasets }
|
|
175
|
+
let datasetsNeedsUpdate = false
|
|
176
|
+
let datasetKeys = Object.keys(config.datasets)
|
|
177
|
+
let newFileName = ''
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < datasetKeys.length; i++) {
|
|
180
|
+
const datasetKey = datasetKeys[i]
|
|
181
|
+
const dataset = config.datasets[datasetKey]
|
|
182
|
+
if (dataset.dataUrl && config.dashboard && config.dashboard.sharedFilters) {
|
|
183
|
+
const dataUrl = new URL(dataset.runtimeDataUrl || dataset.dataUrl, window.location.origin)
|
|
184
|
+
let currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
|
|
185
|
+
let updatedQSParams = {}
|
|
186
|
+
let isUpdateNeeded = false
|
|
187
|
+
|
|
188
|
+
config.dashboard.sharedFilters.forEach(filter => {
|
|
189
|
+
if (filter.filterBy === 'File Name') {
|
|
190
|
+
// if no file name is entered use the default active filter. ie. /activeFilter.json
|
|
191
|
+
if (!filter.fileName && filter.datasetKey === datasetKey) newFileName = filter.active
|
|
192
|
+
// if a file name is found, ie, state_${query}, use that, ie. state_activeFilter.json
|
|
193
|
+
if (filter.datasetKey === datasetKey && filter.fileName) newFileName = capitalizeSplitAndJoin.call(String(filter.fileName), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces'])
|
|
194
|
+
if (newFileName && newFileName.includes('${query}')) {
|
|
195
|
+
newFileName = newFileName.replace('${query}', capitalizeSplitAndJoin.call(String(filter.active), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces']))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (filter.type === 'urlfilter' && !!filter.queryParameter) {
|
|
200
|
+
if (updatedQSParams[filter.queryParameter]) {
|
|
201
|
+
updatedQSParams[filter.queryParameter] = updatedQSParams[filter.queryParameter] + filter.active
|
|
202
|
+
} else {
|
|
203
|
+
updatedQSParams[filter.queryParameter] = filter.active
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (filter.filterBy === 'File Name') {
|
|
207
|
+
isUpdateNeeded = true
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
Object.keys(updatedQSParams).forEach(updatedParam => {
|
|
212
|
+
if (decodeURIComponent(updatedQSParams[updatedParam]) !== currentQSParams[updatedParam]) {
|
|
213
|
+
isUpdateNeeded = true
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
if (!isUpdateNeeded) return
|
|
218
|
+
|
|
219
|
+
Object.keys(currentQSParams).forEach(currentParam => {
|
|
220
|
+
if (!updatedQSParams[currentParam]) {
|
|
221
|
+
updatedQSParams[currentParam] = currentQSParams[currentParam]
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
const _params = Object.keys(updatedQSParams).map(key => ({ key, value: updatedQSParams[key] }))
|
|
225
|
+
let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${gatherQueryParams(_params)}`
|
|
226
|
+
|
|
227
|
+
if (newFileName !== '') {
|
|
228
|
+
let fileExtension = dataUrl.pathname.split('.').pop()
|
|
229
|
+
let pathWithoutFilename = dataUrl.pathname.substring(0, dataUrl.pathname.lastIndexOf('/'))
|
|
230
|
+
dataUrlFinal = `${dataUrl.origin}${pathWithoutFilename}/${newFileName}.${fileExtension}${gatherQueryParams(_params)}`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let newDataset = await fetchRemoteData(`${dataUrlFinal}`)
|
|
234
|
+
|
|
235
|
+
if (newDataset && dataset.dataDescription) {
|
|
236
|
+
try {
|
|
237
|
+
newDataset = transform.autoStandardize(newDataset)
|
|
238
|
+
newDataset = transform.developerStandardize(newDataset, dataset.dataDescription)
|
|
239
|
+
} catch (e) {
|
|
240
|
+
//Data not able to be standardized, leave as is
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
newDatasets[datasetKey].data = newDataset
|
|
244
|
+
newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
|
|
245
|
+
newData[datasetKey] = newDataset
|
|
246
|
+
datasetsNeedsUpdate = true
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (datasetsNeedsUpdate) {
|
|
251
|
+
dispatch({ type: 'SET_DATA', payload: newData })
|
|
252
|
+
|
|
253
|
+
let newFilteredData = {}
|
|
254
|
+
let newConfig = { ...config }
|
|
255
|
+
getVizKeys(config).forEach(key => {
|
|
256
|
+
let dataKey = config.visualizations[key].dataKey
|
|
257
|
+
|
|
258
|
+
let applicableFilters = config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(key) !== -1)
|
|
259
|
+
if (applicableFilters.length > 0) {
|
|
260
|
+
newFilteredData[key] = filterData(applicableFilters, newData[dataKey], state.config.filterBehavior)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (newData[dataKey]) {
|
|
264
|
+
newConfig.visualizations[key].formattedData = newData[dataKey]
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
269
|
+
newConfig.datasets = newDatasets
|
|
270
|
+
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter) => {
|
|
276
|
+
if (!sharedFilter.parents?.length) {
|
|
277
|
+
return 1
|
|
278
|
+
} else {
|
|
279
|
+
let parent = filters.find(filter => sharedFilter.parents!.includes(filter.key))
|
|
280
|
+
if (!parent) return 1
|
|
281
|
+
return 1 + findFilterTier(filters, parent)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const setSharedFilter = (key, datum) => {
|
|
286
|
+
const { config } = state
|
|
287
|
+
let newConfig = { ...config }
|
|
288
|
+
let newFilteredData = { ...state.filteredData }
|
|
289
|
+
for (let i = 0; i < newConfig.dashboard.sharedFilters.length; i++) {
|
|
290
|
+
const filter = newConfig.dashboard.sharedFilters[i]
|
|
291
|
+
if (filter.setBy === key) {
|
|
292
|
+
if (!!filter.columnName) {
|
|
293
|
+
filter.active = datum[filter.columnName]
|
|
294
|
+
}
|
|
295
|
+
break
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
getVizKeys(newConfig).forEach(visualizationKey => {
|
|
300
|
+
let applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1)
|
|
301
|
+
|
|
302
|
+
if (applicableFilters.length > 0) {
|
|
303
|
+
const visualization = newConfig.visualizations[visualizationKey]
|
|
304
|
+
|
|
305
|
+
const formattedData = visualization.dataDescription ? getFormattedData(state.data[visualization.dataKey] || visualization.data, visualization.dataDescription) : undefined
|
|
306
|
+
|
|
307
|
+
newFilteredData[visualizationKey] = filterData(applicableFilters, formattedData || state.data[visualization.dataKey], state.config.filterBehavior)
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
312
|
+
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
const { config } = state
|
|
317
|
+
if (config.filterBehavior !== FilterBehavior.Apply) {
|
|
318
|
+
reloadURLData()
|
|
319
|
+
}
|
|
320
|
+
loadAPIFilters()
|
|
321
|
+
}, [JSON.stringify(state.config.dashboard ? state.config.dashboard.sharedFilters : undefined)])
|
|
322
|
+
|
|
323
|
+
const updateChildConfig = (visualizationKey, newConfig) => {
|
|
324
|
+
const { config } = state
|
|
325
|
+
let updatedConfig = { ...config }
|
|
326
|
+
updatedConfig.visualizations[visualizationKey] = newConfig
|
|
327
|
+
updatedConfig.visualizations[visualizationKey].formattedData = config.visualizations[visualizationKey].formattedData
|
|
328
|
+
if (config.multiDashboards) {
|
|
329
|
+
const activeDashboard = config.activeDashboard
|
|
330
|
+
const multiDashboards = [...config.multiDashboards]
|
|
331
|
+
const label = multiDashboards[activeDashboard].label
|
|
332
|
+
const toSave = _.pick(updatedConfig, ['dashboard', 'visualizations', 'rows'])
|
|
333
|
+
multiDashboards[activeDashboard] = { ...toSave, label }
|
|
334
|
+
updatedConfig.multiDashboards = multiDashboards
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
dispatch({ type: 'SET_CONFIG', payload: updatedConfig })
|
|
338
|
+
// Pass up to <CdcEditor /> if it exists when config state changes
|
|
339
|
+
if (isEditor) {
|
|
340
|
+
editorContext.setTempConfig(updatedConfig)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const applyFilters = () => {
|
|
345
|
+
let dashboardConfig = state.config.dashboard
|
|
346
|
+
const allFiltersSelected = !state.config.dashboard.sharedFilters.some(filter => !filter.active && !filter.queuedActive)
|
|
347
|
+
if (allFiltersSelected) {
|
|
348
|
+
if (state.config.filterBehavior === FilterBehavior.Apply) {
|
|
349
|
+
state.config.dashboard.sharedFilters.forEach((sharedFilter, index) => {
|
|
350
|
+
if (sharedFilter.queuedActive) {
|
|
351
|
+
dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
|
|
352
|
+
delete dashboardConfig.sharedFilters[index].queuedActive
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
dispatch({ type: 'SET_CONFIG', payload: { ...state.config, dashboard: dashboardConfig } })
|
|
358
|
+
updateDataFilters()
|
|
359
|
+
reloadURLData()
|
|
360
|
+
} else {
|
|
361
|
+
// TODO noftify of required fields
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const changeFilterActive = (index: number, value: string) => {
|
|
366
|
+
const { config } = state
|
|
367
|
+
let dashboardConfig = { ...config.dashboard }
|
|
368
|
+
|
|
369
|
+
if (config.filterBehavior !== FilterBehavior.Apply) {
|
|
370
|
+
dashboardConfig.sharedFilters[index].active = value
|
|
371
|
+
} else {
|
|
372
|
+
dashboardConfig.sharedFilters[index].queuedActive = value
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
dispatch({ type: 'SET_CONFIG', payload: { ...config, dashboard: dashboardConfig } })
|
|
376
|
+
if (config.filterBehavior !== FilterBehavior.Apply) {
|
|
377
|
+
updateDataFilters()
|
|
378
|
+
reloadURLData()
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const updateDataFilters = () => {
|
|
383
|
+
const { config } = state
|
|
384
|
+
let dashboardConfig = { ...config.dashboard }
|
|
385
|
+
|
|
386
|
+
let newFilteredData = {}
|
|
387
|
+
getVizKeys(config).forEach(key => {
|
|
388
|
+
let applicableFilters = dashboardConfig.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(key) !== -1)
|
|
389
|
+
if (applicableFilters.length > 0) {
|
|
390
|
+
const visualization = config.visualizations[key]
|
|
391
|
+
const _data = state.data[visualization.dataKey] || visualization.data
|
|
392
|
+
const formattedData = visualization.dataDescription ? getFormattedData(_data, visualization.dataDescription) : _data
|
|
393
|
+
|
|
394
|
+
newFilteredData[key] = filterData(applicableFilters, formattedData, config.filterBehavior)
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const handleOnChange = (index: number, value: string) => {
|
|
402
|
+
const { config } = state
|
|
403
|
+
changeFilterActive(index, value)
|
|
404
|
+
if (config.filterBehavior === FilterBehavior.Apply) {
|
|
405
|
+
const autoLoadViz = getAutoLoadVisualization()
|
|
406
|
+
if (!autoLoadViz) return // nothing left to do for regular filter behavior.
|
|
407
|
+
const isAutoSelectFilter = !autoLoadViz.hide.includes(index)
|
|
408
|
+
const missingFilterSelections = config.dashboard.sharedFilters.some(f => !f.active)
|
|
409
|
+
if (isAutoSelectFilter && !missingFilterSelections) {
|
|
410
|
+
// a dropdown has been selected that doesn't
|
|
411
|
+
// require the Go Button
|
|
412
|
+
reloadURLData()
|
|
413
|
+
} else {
|
|
414
|
+
// A parent filter was selected, reset filters by:
|
|
415
|
+
// set auto select filter dropdowns to null
|
|
416
|
+
const autoSelectFilters = config.dashboard.sharedFilters.filter((_, _index) => !autoLoadViz?.hide.includes(_index))
|
|
417
|
+
const dropdownFilterKeys = autoSelectFilters.map(filter => getApiFilterKey(filter.apiFilter!))
|
|
418
|
+
const newApiDropdowns = { ...apiFilterDropdowns }
|
|
419
|
+
dropdownFilterKeys.forEach(key => (newApiDropdowns[key] = null))
|
|
420
|
+
setAPIFilterDropdowns(newApiDropdowns)
|
|
421
|
+
// remove active from sharedFilters that are autoLoading
|
|
422
|
+
const dashboardConfig = { ...config.dashboard }
|
|
423
|
+
if (config.filterBehavior !== FilterBehavior.Apply) {
|
|
424
|
+
dashboardConfig.sharedFilters[index].active = value
|
|
425
|
+
} else {
|
|
426
|
+
dashboardConfig.sharedFilters[index].queuedActive = value
|
|
427
|
+
}
|
|
428
|
+
const newSharedFilters = config.dashboard.sharedFilters.map((filter, _index) => {
|
|
429
|
+
const _isAutoSelectFilter = !autoLoadViz?.hide.includes(_index)
|
|
430
|
+
if (_isAutoSelectFilter) filter.active = ''
|
|
431
|
+
return filter
|
|
432
|
+
})
|
|
433
|
+
const _newConfig = { ...config, dashboard: { ...config.dashboard, sharedFilters: newSharedFilters } }
|
|
434
|
+
dispatch({ type: 'SET_CONFIG', payload: _newConfig })
|
|
435
|
+
// setData to empty object because we no longer have a data state.
|
|
436
|
+
dispatch({ type: 'SET_DATA', payload: {} })
|
|
437
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const Filters = ({ hide, autoLoad }: { hide?: number[]; autoLoad?: boolean }) => {
|
|
443
|
+
const { config } = state
|
|
444
|
+
const isLegacyFilter = !config.filterBehavior
|
|
445
|
+
return (
|
|
446
|
+
<>
|
|
447
|
+
{config.dashboard.sharedFilters.map((singleFilter, filterIndex) => {
|
|
448
|
+
if ((singleFilter.type !== 'urlfilter' && !singleFilter.showDropdown) || (hide && hide.indexOf(filterIndex) !== -1)) return <></>
|
|
449
|
+
const values: JSX.Element[] = []
|
|
450
|
+
if (singleFilter.resetLabel) {
|
|
451
|
+
values.push(
|
|
452
|
+
<option key={`${singleFilter.resetLabel}-option`} value={singleFilter.resetLabel}>
|
|
453
|
+
{singleFilter.resetLabel}
|
|
454
|
+
</option>
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
const _key = singleFilter.apiFilter ? getApiFilterKey(singleFilter.apiFilter) : undefined
|
|
458
|
+
if (_key && apiFilterDropdowns[_key]) {
|
|
459
|
+
// URL Filter
|
|
460
|
+
apiFilterDropdowns[_key]!.forEach(({ text, value }, index) => {
|
|
461
|
+
values.push(
|
|
462
|
+
<option key={`${value}-option-${index}`} value={value}>
|
|
463
|
+
{text}
|
|
464
|
+
</option>
|
|
465
|
+
)
|
|
466
|
+
})
|
|
467
|
+
} else {
|
|
468
|
+
// Data Filter
|
|
469
|
+
singleFilter.values?.forEach((filterOption, index) => {
|
|
470
|
+
const labeledOpt = singleFilter.labels && singleFilter.labels[filterOption]
|
|
471
|
+
values.push(
|
|
472
|
+
<option key={`${singleFilter.key}-option-${index}`} value={filterOption}>
|
|
473
|
+
{labeledOpt || filterOption}
|
|
474
|
+
</option>
|
|
475
|
+
)
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
<div className='cove-dashboard-filters' key={`${singleFilter.key}-filtersection-${filterIndex}`}>
|
|
481
|
+
<section className='dashboard-filters-section'>
|
|
482
|
+
<label htmlFor={`filter-${filterIndex}`}>{singleFilter.key}</label>
|
|
483
|
+
<select
|
|
484
|
+
id={`filter-${filterIndex}`}
|
|
485
|
+
className='filter-select'
|
|
486
|
+
data-index='0'
|
|
487
|
+
value={singleFilter.queuedActive || singleFilter.active}
|
|
488
|
+
onChange={val => {
|
|
489
|
+
handleOnChange(filterIndex, val.target.value)
|
|
490
|
+
}}
|
|
491
|
+
>
|
|
492
|
+
{values}
|
|
493
|
+
</select>
|
|
494
|
+
</section>
|
|
495
|
+
</div>
|
|
496
|
+
)
|
|
497
|
+
})}
|
|
498
|
+
|
|
499
|
+
{!isLegacyFilter && config.filterBehavior === FilterBehavior.Apply && <button onClick={applyFilters}>GO!</button>}
|
|
500
|
+
</>
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const resizeObserver = new ResizeObserver(entries => {
|
|
505
|
+
for (let entry of entries) {
|
|
506
|
+
let newViewport = getViewport(entry.contentRect.width)
|
|
507
|
+
|
|
508
|
+
setCurrentViewport(newViewport)
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
const outerContainerRef = useCallback(node => {
|
|
513
|
+
if (node !== null) {
|
|
514
|
+
resizeObserver.observe(node)
|
|
515
|
+
}
|
|
516
|
+
}, [])
|
|
517
|
+
|
|
518
|
+
const setPreview = shouldPreview => dispatch({ type: 'SET_PREVIEW', payload: shouldPreview })
|
|
519
|
+
|
|
520
|
+
// Prevent render if loading
|
|
521
|
+
if (state.loading) return <Loading />
|
|
522
|
+
|
|
523
|
+
let body: JSX.Element | null = null
|
|
524
|
+
// Editor mode
|
|
525
|
+
if (isEditor && !state.preview) {
|
|
526
|
+
let subVisualizationEditing = false
|
|
527
|
+
|
|
528
|
+
getVizKeys(state.config).forEach(visualizationKey => {
|
|
529
|
+
let visualizationConfig = { ...state.config.visualizations[visualizationKey] }
|
|
530
|
+
|
|
531
|
+
const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
|
|
532
|
+
|
|
533
|
+
if (state.filteredData && state.filteredData[visualizationKey]) {
|
|
534
|
+
visualizationConfig.data = state.filteredData[visualizationKey]
|
|
535
|
+
if (visualizationConfig.formattedData) {
|
|
536
|
+
visualizationConfig.originalFormattedData = visualizationConfig.formattedData
|
|
537
|
+
visualizationConfig.formattedData = visualizationConfig.data
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
visualizationConfig.data = state.data[dataKey]
|
|
541
|
+
if (visualizationConfig.formattedData) {
|
|
542
|
+
visualizationConfig.originalFormattedData = visualizationConfig.formattedData
|
|
543
|
+
visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const setsSharedFilter = state.config.dashboard.sharedFilters && state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey).length > 0
|
|
548
|
+
const setSharedFilterValue = setsSharedFilter ? state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey)[0].active : undefined
|
|
549
|
+
|
|
550
|
+
if (visualizationConfig.editing) {
|
|
551
|
+
subVisualizationEditing = true
|
|
552
|
+
|
|
553
|
+
const _updateConfig = newConfig => {
|
|
554
|
+
let dataCorrectedConfig = visualizationConfig.originalFormattedData ? { ...newConfig, formattedData: visualizationConfig.originalFormattedData } : newConfig
|
|
555
|
+
updateChildConfig(visualizationKey, dataCorrectedConfig)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
switch (visualizationConfig.type) {
|
|
559
|
+
case 'chart':
|
|
560
|
+
body = (
|
|
561
|
+
<>
|
|
562
|
+
<Header visualizationKey={visualizationKey} subEditor='Chart' />
|
|
563
|
+
<CdcChart
|
|
564
|
+
key={visualizationKey}
|
|
565
|
+
config={visualizationConfig}
|
|
566
|
+
isEditor={true}
|
|
567
|
+
isDebug={isDebug}
|
|
568
|
+
setConfig={_updateConfig}
|
|
569
|
+
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
570
|
+
setSharedFilterValue={setSharedFilterValue}
|
|
571
|
+
dashboardConfig={state.config}
|
|
572
|
+
isDashboard={true}
|
|
573
|
+
configUrl={undefined}
|
|
574
|
+
setEditing={undefined}
|
|
575
|
+
hostname={undefined}
|
|
576
|
+
link={undefined}
|
|
577
|
+
/>
|
|
578
|
+
</>
|
|
579
|
+
)
|
|
580
|
+
break
|
|
581
|
+
case 'map':
|
|
582
|
+
body = (
|
|
583
|
+
<>
|
|
584
|
+
<Header visualizationKey={visualizationKey} subEditor='Map' />
|
|
585
|
+
<CdcMap
|
|
586
|
+
key={visualizationKey}
|
|
587
|
+
config={visualizationConfig}
|
|
588
|
+
isEditor={true}
|
|
589
|
+
isDebug={isDebug}
|
|
590
|
+
setConfig={_updateConfig}
|
|
591
|
+
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
592
|
+
setSharedFilterValue={setSharedFilterValue}
|
|
593
|
+
isDashboard={true}
|
|
594
|
+
showLoader={false}
|
|
595
|
+
dashboardConfig={state.config}
|
|
596
|
+
/>
|
|
597
|
+
</>
|
|
598
|
+
)
|
|
599
|
+
break
|
|
600
|
+
case 'data-bite':
|
|
601
|
+
visualizationConfig = { ...visualizationConfig, newViz: true }
|
|
602
|
+
body = (
|
|
603
|
+
<>
|
|
604
|
+
<Header visualizationKey={visualizationKey} subEditor='Data Bite' />
|
|
605
|
+
<CdcDataBite key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} />
|
|
606
|
+
</>
|
|
607
|
+
)
|
|
608
|
+
break
|
|
609
|
+
case 'waffle-chart':
|
|
610
|
+
body = (
|
|
611
|
+
<>
|
|
612
|
+
<Header visualizationKey={visualizationKey} subEditor='Waffle Chart' />
|
|
613
|
+
<CdcWaffleChart key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
|
|
614
|
+
</>
|
|
615
|
+
)
|
|
616
|
+
break
|
|
617
|
+
case 'markup-include':
|
|
618
|
+
body = (
|
|
619
|
+
<>
|
|
620
|
+
<Header visualizationKey={visualizationKey} subEditor='Markup Include' />
|
|
621
|
+
<CdcMarkupInclude key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
|
|
622
|
+
</>
|
|
623
|
+
)
|
|
624
|
+
break
|
|
625
|
+
case 'filtered-text':
|
|
626
|
+
body = (
|
|
627
|
+
<>
|
|
628
|
+
<Header visualizationKey={visualizationKey} subEditor='Filtered Text' />
|
|
629
|
+
<CdcFilteredText key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
|
|
630
|
+
</>
|
|
631
|
+
)
|
|
632
|
+
break
|
|
633
|
+
case 'filter-dropdowns':
|
|
634
|
+
const hideFilter = visualizationConfig.autoLoad && inNoDataState
|
|
635
|
+
body = !hideFilter ? (
|
|
636
|
+
<>
|
|
637
|
+
<Header visualizationKey={visualizationKey} subEditor='Filter Dropdowns' />
|
|
638
|
+
<Filters hide={visualizationConfig.hide} autoLoad={visualizationConfig.autoLoad} />
|
|
639
|
+
</>
|
|
640
|
+
) : (
|
|
641
|
+
<></>
|
|
642
|
+
)
|
|
643
|
+
break
|
|
644
|
+
default:
|
|
645
|
+
body = <></>
|
|
646
|
+
break
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
if (!subVisualizationEditing) {
|
|
652
|
+
body = (
|
|
653
|
+
<DndProvider backend={HTML5Backend}>
|
|
654
|
+
<div className='header-container'>
|
|
655
|
+
<Header setPreview={setPreview} />
|
|
656
|
+
<VisualizationsPanel loadConfig={newConfig => dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })} config={state.config} />
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
<div className='layout-container'>
|
|
660
|
+
<Grid />
|
|
661
|
+
</div>
|
|
662
|
+
</DndProvider>
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
} else {
|
|
666
|
+
const { config } = state
|
|
667
|
+
const { title, description } = config.dashboard || {}
|
|
668
|
+
body = (
|
|
669
|
+
<>
|
|
670
|
+
{isEditor && <Header setPreview={setPreview} />}
|
|
671
|
+
<MultiTabs isEditor={isEditor && !state.preview} />
|
|
672
|
+
<div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''}`}>
|
|
673
|
+
<Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
|
|
674
|
+
{/* Description */}
|
|
675
|
+
{description && <div className='subtext'>{parse(description)}</div>}
|
|
676
|
+
|
|
677
|
+
{/* Filters */}
|
|
678
|
+
{config.dashboard.sharedFilters && Object.values(config.visualizations || {}).filter(viz => viz.visualizationType === 'filter-dropdowns').length === 0 && <Filters hide={undefined} autoLoad={undefined} />}
|
|
679
|
+
|
|
680
|
+
{/* Visualizations */}
|
|
681
|
+
{config.rows &&
|
|
682
|
+
config.rows
|
|
683
|
+
.filter(row => row.filter(col => col.widget).length !== 0)
|
|
684
|
+
.map((row, index) => {
|
|
685
|
+
return (
|
|
686
|
+
<div className={`dashboard-row ${row.equalHeight ? 'equal-height' : ''}`} key={`row__${index}`}>
|
|
687
|
+
{row.map((col, colIndex) => {
|
|
688
|
+
if (col.width) {
|
|
689
|
+
if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`dashboard-col dashboard-col-${col.width}`}></div>
|
|
690
|
+
|
|
691
|
+
let visualizationConfig = { ...config.visualizations[col.widget] }
|
|
692
|
+
|
|
693
|
+
const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
|
|
694
|
+
|
|
695
|
+
if (state.filteredData && state.filteredData[col.widget]) {
|
|
696
|
+
visualizationConfig.data = state.filteredData[col.widget]
|
|
697
|
+
if (visualizationConfig.formattedData) {
|
|
698
|
+
visualizationConfig.originalFormattedData = visualizationConfig.formattedData
|
|
699
|
+
visualizationConfig.formattedData = visualizationConfig.data
|
|
700
|
+
}
|
|
701
|
+
} else {
|
|
702
|
+
visualizationConfig.data = state.data[dataKey]
|
|
703
|
+
if (visualizationConfig.formattedData) {
|
|
704
|
+
visualizationConfig.originalFormattedData = visualizationConfig.formattedData
|
|
705
|
+
visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const setsSharedFilter = config.dashboard.sharedFilters && config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget).length > 0
|
|
710
|
+
const setSharedFilterValue = setsSharedFilter ? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget)[0].active : undefined
|
|
711
|
+
const tableLink = (
|
|
712
|
+
<a href={`#data-table-${visualizationConfig.dataKey}`} className='margin-left-href'>
|
|
713
|
+
{visualizationConfig.dataKey} (Go to Table)
|
|
714
|
+
</a>
|
|
715
|
+
)
|
|
716
|
+
const hideFilter = visualizationConfig.autoLoad && inNoDataState
|
|
717
|
+
return (
|
|
718
|
+
<React.Fragment key={`vis__${index}__${colIndex}`}>
|
|
719
|
+
<div className={`dashboard-col dashboard-col-${col.width}`}>
|
|
720
|
+
{visualizationConfig.type === 'chart' && (
|
|
721
|
+
<CdcChart
|
|
722
|
+
key={col.widget}
|
|
723
|
+
config={visualizationConfig}
|
|
724
|
+
dashboardConfig={config}
|
|
725
|
+
isEditor={false}
|
|
726
|
+
setConfig={newConfig => {
|
|
727
|
+
updateChildConfig(col.widget, newConfig)
|
|
728
|
+
}}
|
|
729
|
+
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
730
|
+
isDashboard={true}
|
|
731
|
+
link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
|
|
732
|
+
configUrl={undefined}
|
|
733
|
+
setEditing={undefined}
|
|
734
|
+
hostname={undefined}
|
|
735
|
+
setSharedFilterValue={undefined}
|
|
736
|
+
/>
|
|
737
|
+
)}
|
|
738
|
+
{visualizationConfig.type === 'map' && (
|
|
739
|
+
<CdcMap
|
|
740
|
+
key={col.widget}
|
|
741
|
+
config={visualizationConfig}
|
|
742
|
+
isEditor={false}
|
|
743
|
+
setConfig={newConfig => {
|
|
744
|
+
updateChildConfig(col.widget, newConfig)
|
|
745
|
+
}}
|
|
746
|
+
showLoader={false}
|
|
747
|
+
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
748
|
+
setSharedFilterValue={setSharedFilterValue}
|
|
749
|
+
isDashboard={true}
|
|
750
|
+
link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
|
|
751
|
+
/>
|
|
752
|
+
)}
|
|
753
|
+
{visualizationConfig.type === 'data-bite' && (
|
|
754
|
+
<CdcDataBite
|
|
755
|
+
key={col.widget}
|
|
756
|
+
config={visualizationConfig}
|
|
757
|
+
isEditor={false}
|
|
758
|
+
setConfig={newConfig => {
|
|
759
|
+
updateChildConfig(col.widget, newConfig)
|
|
760
|
+
}}
|
|
761
|
+
isDashboard={true}
|
|
762
|
+
/>
|
|
763
|
+
)}
|
|
764
|
+
{visualizationConfig.type === 'waffle-chart' && (
|
|
765
|
+
<CdcWaffleChart
|
|
766
|
+
key={col.widget}
|
|
767
|
+
config={visualizationConfig}
|
|
768
|
+
isEditor={false}
|
|
769
|
+
setConfig={newConfig => {
|
|
770
|
+
updateChildConfig(col.widget, newConfig)
|
|
771
|
+
}}
|
|
772
|
+
isDashboard={true}
|
|
773
|
+
configUrl={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
|
|
774
|
+
/>
|
|
775
|
+
)}
|
|
776
|
+
{visualizationConfig.type === 'markup-include' && (
|
|
777
|
+
<CdcMarkupInclude
|
|
778
|
+
key={col.widget}
|
|
779
|
+
config={visualizationConfig}
|
|
780
|
+
isEditor={false}
|
|
781
|
+
setConfig={newConfig => {
|
|
782
|
+
updateChildConfig(col.widget, newConfig)
|
|
783
|
+
}}
|
|
784
|
+
isDashboard={true}
|
|
785
|
+
configUrl={undefined}
|
|
786
|
+
/>
|
|
787
|
+
)}
|
|
788
|
+
{visualizationConfig.type === 'filtered-text' && (
|
|
789
|
+
<CdcFilteredText
|
|
790
|
+
key={col.widget}
|
|
791
|
+
config={visualizationConfig}
|
|
792
|
+
isEditor={false}
|
|
793
|
+
setConfig={newConfig => {
|
|
794
|
+
updateChildConfig(col.widget, newConfig)
|
|
795
|
+
}}
|
|
796
|
+
isDashboard={true}
|
|
797
|
+
configUrl={undefined}
|
|
798
|
+
/>
|
|
799
|
+
)}
|
|
800
|
+
{visualizationConfig.type === 'filter-dropdowns' && !hideFilter && <Filters hide={visualizationConfig.hide} autoLoad={visualizationConfig.autoLoad} />}
|
|
801
|
+
</div>
|
|
802
|
+
</React.Fragment>
|
|
803
|
+
)
|
|
804
|
+
}
|
|
805
|
+
return <React.Fragment key={`vis__${index}__${colIndex}`}></React.Fragment>
|
|
806
|
+
})}
|
|
807
|
+
</div>
|
|
808
|
+
)
|
|
809
|
+
})}
|
|
810
|
+
|
|
811
|
+
{/* Image or PDF Inserts */}
|
|
812
|
+
<section className='download-buttons'>
|
|
813
|
+
{config.table?.downloadImageButton && <MediaControls.Button title='Download Dashboard as Image' type='image' state={config} text='Download Dashboard Image' elementToCapture={imageId} />}
|
|
814
|
+
{config.table?.downloadPdfButton && <MediaControls.Button title='Download Dashboard as PDF' type='pdf' state={config} text='Download Dashboard PDF' elementToCapture={imageId} />}
|
|
815
|
+
</section>
|
|
816
|
+
|
|
817
|
+
{/* Data Table */}
|
|
818
|
+
{config.table?.show && config.data && (
|
|
819
|
+
<DataTable
|
|
820
|
+
config={config}
|
|
821
|
+
rawData={config.data}
|
|
822
|
+
runtimeData={config.data || []}
|
|
823
|
+
expandDataTable={config.table.expanded}
|
|
824
|
+
showDownloadButton={config.table.download}
|
|
825
|
+
tableTitle={config.dashboard.title || ''}
|
|
826
|
+
viewport={currentViewport}
|
|
827
|
+
tabbingId={config.dashboard.title || ''}
|
|
828
|
+
outerContainerRef={outerContainerRef}
|
|
829
|
+
imageRef={imageId}
|
|
830
|
+
isDebug={isDebug}
|
|
831
|
+
isEditor={isEditor}
|
|
832
|
+
/>
|
|
833
|
+
)}
|
|
834
|
+
{config.table?.show &&
|
|
835
|
+
config.datasets &&
|
|
836
|
+
Object.keys(config.datasets).map(datasetKey => {
|
|
837
|
+
//For each dataset, find any shared filters that apply to all visualizations using the dataset
|
|
838
|
+
//Apply these filters to the table
|
|
839
|
+
let filteredTableData
|
|
840
|
+
if (config.dashboard.sharedFilters && config.dashboard.sharedFilters.length > 0) {
|
|
841
|
+
//Gets list of visuailzations using the dataset
|
|
842
|
+
let vizKeysUsingDataset: string[] = []
|
|
843
|
+
getVizKeys(config).forEach(visualizationKey => {
|
|
844
|
+
if (config.visualizations[visualizationKey].dataKey === datasetKey) {
|
|
845
|
+
vizKeysUsingDataset.push(visualizationKey)
|
|
846
|
+
}
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
//Checks shared filters against list to see if all visualizations are represented
|
|
850
|
+
let applicableFilters: SharedFilter[] = []
|
|
851
|
+
config.dashboard.sharedFilters.forEach(sharedFilter => {
|
|
852
|
+
let allMatch = true
|
|
853
|
+
vizKeysUsingDataset.forEach(visualizationKey => {
|
|
854
|
+
if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) === -1) {
|
|
855
|
+
allMatch = false
|
|
856
|
+
}
|
|
857
|
+
})
|
|
858
|
+
if (allMatch) {
|
|
859
|
+
applicableFilters.push(sharedFilter)
|
|
860
|
+
}
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
//Applys any applicable filters
|
|
864
|
+
if (applicableFilters.length > 0) {
|
|
865
|
+
filteredTableData = filterData(applicableFilters, config.datasets[datasetKey].data, state.config.filterBehavior)
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return (
|
|
870
|
+
<div className='multi-table-container' id={`data-table-${datasetKey}`} key={`data-table-${datasetKey}`}>
|
|
871
|
+
<DataTable
|
|
872
|
+
config={config as TableConfig}
|
|
873
|
+
dataConfig={config.datasets[datasetKey]}
|
|
874
|
+
rawData={config.datasets[datasetKey].data}
|
|
875
|
+
runtimeData={filteredTableData || config.datasets[datasetKey].data || []}
|
|
876
|
+
expandDataTable={config.table.expanded}
|
|
877
|
+
tableTitle={datasetKey}
|
|
878
|
+
viewport={currentViewport}
|
|
879
|
+
tabbingId={datasetKey}
|
|
880
|
+
/>
|
|
881
|
+
</div>
|
|
882
|
+
)
|
|
883
|
+
})}
|
|
884
|
+
</div>
|
|
885
|
+
</>
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const dashboardContainerClasses = ['cdc-open-viz-module', 'type-dashboard', `${currentViewport}`]
|
|
890
|
+
|
|
891
|
+
return (
|
|
892
|
+
<GlobalContextProvider>
|
|
893
|
+
<DashboardContext.Provider value={{ ...state, setParentConfig: editorContext.setTempConfig, outerContainerRef, isDebug }}>
|
|
894
|
+
<DashboardDispatchContext.Provider value={dispatch}>
|
|
895
|
+
<div className={dashboardContainerClasses.join(' ')} ref={outerContainerRef} data-download-id={imageId}>
|
|
896
|
+
{body}
|
|
897
|
+
</div>
|
|
898
|
+
<OverlayFrame />
|
|
899
|
+
</DashboardDispatchContext.Provider>
|
|
900
|
+
</DashboardContext.Provider>
|
|
901
|
+
</GlobalContextProvider>
|
|
902
|
+
)
|
|
903
|
+
}
|