@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
|
@@ -5,8 +5,8 @@ import ExampleConfig_1 from './_mock/dashboard-gallery.json'
|
|
|
5
5
|
import ExampleConfig_2 from './_mock/dashboard-2.json'
|
|
6
6
|
import ExampleConfig_3 from './_mock/dashboard_no_filter.json'
|
|
7
7
|
import Dashboard_Filter from './_mock/dashboard-filter.json'
|
|
8
|
-
import Dashboard from '../
|
|
9
|
-
import { Config } from '../types/
|
|
8
|
+
import Dashboard from '../CdcDashboardComponent'
|
|
9
|
+
import { type DashboardConfig as Config } from '../types/DashboardConfig'
|
|
10
10
|
import { userEvent, within } from '@storybook/testing-library'
|
|
11
11
|
|
|
12
12
|
const meta: Meta<typeof Dashboard> = {
|
|
@@ -7,19 +7,22 @@ import Widget from './Widget'
|
|
|
7
7
|
const Column = ({ data, rowIdx, colIdx }) => {
|
|
8
8
|
const { config } = useContext(DashboardContext)
|
|
9
9
|
|
|
10
|
-
const [{ isOver, canDrop }, drop] = useDrop(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const [{ isOver, canDrop }, drop] = useDrop(
|
|
11
|
+
() => ({
|
|
12
|
+
accept: 'vis-widget',
|
|
13
|
+
drop: () => ({
|
|
14
|
+
rowIdx,
|
|
15
|
+
colIdx,
|
|
16
|
+
canDrop
|
|
17
|
+
}),
|
|
18
|
+
canDrop: () => !data.widget,
|
|
19
|
+
collect: monitor => ({
|
|
20
|
+
isOver: monitor.isOver(),
|
|
21
|
+
canDrop: !!monitor.canDrop()
|
|
22
|
+
})
|
|
16
23
|
}),
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
isOver: monitor.isOver(),
|
|
20
|
-
canDrop: !!monitor.canDrop()
|
|
21
|
-
})
|
|
22
|
-
}))
|
|
24
|
+
[config.activeDashboard]
|
|
25
|
+
)
|
|
23
26
|
|
|
24
27
|
const widget = data.widget ? config?.visualizations[data.widget] : null
|
|
25
28
|
if (widget && !widget.uid) widget.uid = data.widget
|
|
@@ -5,7 +5,7 @@ import { DashboardContext, DashboardDispatchContext } from '../../DashboardConte
|
|
|
5
5
|
// types
|
|
6
6
|
import { type APIFilter } from '../../types/APIFilter'
|
|
7
7
|
import { type SharedFilter } from '../../types/SharedFilter'
|
|
8
|
-
import { type Config } from '../../types/
|
|
8
|
+
import { type DashboardConfig as Config } from '../../types/DashboardConfig'
|
|
9
9
|
|
|
10
10
|
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
11
11
|
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
@@ -17,6 +17,7 @@ import Select from '@cdc/core/components/ui/Select'
|
|
|
17
17
|
import Button from '@cdc/core/components/elements/Button'
|
|
18
18
|
|
|
19
19
|
import './index.scss'
|
|
20
|
+
import MultiConfigTabs from '../MultiConfigTabs'
|
|
20
21
|
|
|
21
22
|
type HeaderProps = {
|
|
22
23
|
setPreview?: any
|
|
@@ -502,13 +503,13 @@ const Header = (props: HeaderProps) => {
|
|
|
502
503
|
<select
|
|
503
504
|
value={filter.parents || []}
|
|
504
505
|
onChange={e => {
|
|
505
|
-
updateFilterProp('
|
|
506
|
+
updateFilterProp('parents', index, e.target.value)
|
|
506
507
|
}}
|
|
507
508
|
>
|
|
508
509
|
<option value=''>Select a filter</option>
|
|
509
510
|
{config.dashboard.sharedFilters &&
|
|
510
511
|
config.dashboard.sharedFilters.map(sharedFilter => {
|
|
511
|
-
if (sharedFilter.key !== filter.key
|
|
512
|
+
if (sharedFilter.key !== filter.key) {
|
|
512
513
|
return <option>{sharedFilter.key}</option>
|
|
513
514
|
}
|
|
514
515
|
})}
|
|
@@ -541,6 +542,15 @@ const Header = (props: HeaderProps) => {
|
|
|
541
542
|
)
|
|
542
543
|
}
|
|
543
544
|
|
|
545
|
+
const handleCheck = e => {
|
|
546
|
+
const { checked } = e.currentTarget
|
|
547
|
+
if (checked) {
|
|
548
|
+
dispatch({ type: 'INITIALIZE_MULTIDASHBOARDS' })
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const multiInitialized = !!config.multiDashboards
|
|
553
|
+
|
|
544
554
|
return (
|
|
545
555
|
<div aria-level={2} role='heading' className={`editor-heading${subEditor ? ' sub-dashboard-viz' : ''}`}>
|
|
546
556
|
{subEditor ? (
|
|
@@ -549,13 +559,17 @@ const Header = (props: HeaderProps) => {
|
|
|
549
559
|
</div>
|
|
550
560
|
) : (
|
|
551
561
|
<div className='heading-1'>
|
|
552
|
-
Dashboard Editor
|
|
562
|
+
Dashboard Editor{' '}
|
|
563
|
+
<span className='small'>
|
|
564
|
+
<input type='checkbox' onChange={handleCheck} checked={multiInitialized} disabled={multiInitialized} /> make multidashboard
|
|
565
|
+
</span>
|
|
553
566
|
<br />
|
|
554
567
|
{<input type='text' placeholder='Enter Dashboard Name Here' defaultValue={config.dashboard?.title} onChange={e => changeConfigValue('dashboard', 'title', e.target.value)} />}
|
|
555
568
|
</div>
|
|
556
569
|
)}
|
|
557
570
|
{!subEditor && (
|
|
558
571
|
<div className='toggle-bar__wrapper'>
|
|
572
|
+
<MultiConfigTabs isEditor />
|
|
559
573
|
<ul className='toggle-bar'>
|
|
560
574
|
<li
|
|
561
575
|
className={tabSelected === 0 ? 'active' : 'inactive'}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createRef, useContext, useMemo, useState } from 'react'
|
|
2
|
+
import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
|
|
3
|
+
import Modal from '@cdc/core/components/ui/Modal'
|
|
4
|
+
import { useGlobalContext } from '@cdc/core/components/GlobalContext'
|
|
5
|
+
import './multiconfigtabs.styles.css'
|
|
6
|
+
|
|
7
|
+
const AreYouSure = deleteCallback => {
|
|
8
|
+
return (
|
|
9
|
+
<Modal>
|
|
10
|
+
<Modal.Content>
|
|
11
|
+
<p>Are you sure you want to delete this dashboard? </p>
|
|
12
|
+
<button className='btn btn-danger' onClick={deleteCallback}>
|
|
13
|
+
DELETE
|
|
14
|
+
</button>
|
|
15
|
+
</Modal.Content>
|
|
16
|
+
</Modal>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Tab = ({ name, handleClick, tabs, index, active }) => {
|
|
21
|
+
const [editing, setEditing] = useState(false)
|
|
22
|
+
const dispatch = useContext(DashboardDispatchContext)
|
|
23
|
+
const { overlay } = useGlobalContext()
|
|
24
|
+
const inputRef = createRef<HTMLInputElement>()
|
|
25
|
+
|
|
26
|
+
const onBlur = () => {
|
|
27
|
+
const newVal = inputRef.current.value
|
|
28
|
+
const sameName = newVal === name
|
|
29
|
+
const blankName = !newVal
|
|
30
|
+
const duplicateName = tabs.includes(newVal)
|
|
31
|
+
if (!sameName && !blankName && !duplicateName) {
|
|
32
|
+
dispatch({ type: 'RENAME_DASHBOARD_TAB', payload: { current: name, new: newVal } })
|
|
33
|
+
}
|
|
34
|
+
setEditing(false)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const onClick = e => {
|
|
38
|
+
// ignore click on delete button
|
|
39
|
+
if (e.target.className === 'remove') return
|
|
40
|
+
if (active) {
|
|
41
|
+
setEditing(true)
|
|
42
|
+
} else {
|
|
43
|
+
handleClick()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const handleRemove = () => {
|
|
48
|
+
const deleteCallback = () => {
|
|
49
|
+
dispatch({ type: 'REMOVE_MULTIDASHBOARD_AT_INDEX', payload: index })
|
|
50
|
+
overlay?.actions.toggleOverlay(false)
|
|
51
|
+
}
|
|
52
|
+
overlay?.actions.openOverlay(AreYouSure(deleteCallback))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleReorder = (index: number, moveTo: -1 | 1) => {
|
|
56
|
+
const newIndex = index + moveTo
|
|
57
|
+
const inbounds = newIndex > -1 && newIndex <= tabs.length - 1
|
|
58
|
+
if (inbounds) {
|
|
59
|
+
dispatch({ type: 'REORDER_MULTIDASHBOARDS', payload: { currentIndex: index, newIndex: index + moveTo } })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const canMoveLeft = index !== 0
|
|
64
|
+
const canMoveRight = index <= tabs.length - 2
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<li className='nav-item'>
|
|
68
|
+
<a className={`edit nav-link${active ? ' active' : ''}`} aria-current={active ? 'page' : null} href='#' onClick={onClick}>
|
|
69
|
+
{canMoveLeft && <button onClick={() => handleReorder(index, -1)}>{'<'}</button>}
|
|
70
|
+
{editing ? <input type='text' defaultValue={name} onBlur={onBlur} ref={inputRef} /> : <>{name}</>}
|
|
71
|
+
{canMoveRight && <button onClick={() => handleReorder(index, 1)}>{'>'}</button>}
|
|
72
|
+
<button className='remove' onClick={handleRemove}>
|
|
73
|
+
X
|
|
74
|
+
</button>
|
|
75
|
+
</a>
|
|
76
|
+
</li>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const MultiConfigTabs = () => {
|
|
81
|
+
const { config } = useContext(DashboardContext)
|
|
82
|
+
const dispatch = useContext(DashboardDispatchContext)
|
|
83
|
+
const tabs = useMemo<string[]>(() => (config.multiDashboards || []).map(({ label }) => label), [config.multiDashboards])
|
|
84
|
+
const activeTab = useMemo<number>(() => config.activeDashboard, [config.activeDashboard])
|
|
85
|
+
|
|
86
|
+
const saveAndLoad = (indexToSwitchTo: number) => {
|
|
87
|
+
dispatch({ type: 'SAVE_CURRENT_CHANGES' })
|
|
88
|
+
dispatch({ type: 'SWITCH_CONFIG', payload: indexToSwitchTo })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!config.multiDashboards) return null
|
|
92
|
+
return (
|
|
93
|
+
<ul className='nav nav-tabs multi-config-tabs'>
|
|
94
|
+
{tabs.map((tab, index) => (
|
|
95
|
+
<Tab key={tab + index} name={tab} tabs={tabs} index={index} handleClick={() => saveAndLoad(index)} active={index === activeTab} />
|
|
96
|
+
))}
|
|
97
|
+
<li className='nav-item'>
|
|
98
|
+
<a className='nav-link add' href='#' onClick={() => dispatch({ type: 'ADD_NEW_DASHBOARD' })}>
|
|
99
|
+
+
|
|
100
|
+
</a>
|
|
101
|
+
</li>
|
|
102
|
+
</ul>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default MultiConfigTabs
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useContext, useMemo } from 'react'
|
|
2
|
+
import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
|
|
3
|
+
|
|
4
|
+
import './multiconfigtabs.styles.css'
|
|
5
|
+
|
|
6
|
+
const MultiTabs = () => {
|
|
7
|
+
const { config } = useContext(DashboardContext)
|
|
8
|
+
const dispatch = useContext(DashboardDispatchContext)
|
|
9
|
+
const tabs = useMemo<string[]>(() => (config.multiDashboards || []).map(({ label }) => label), [config.multiDashboards])
|
|
10
|
+
const activeTab = useMemo<number>(() => config.activeDashboard, [config.activeDashboard])
|
|
11
|
+
|
|
12
|
+
const load = (indexToSwitchTo: number) => {
|
|
13
|
+
dispatch({ type: 'SWITCH_CONFIG', payload: indexToSwitchTo })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!config.multiDashboards) return null
|
|
17
|
+
return (
|
|
18
|
+
<ul className='nav nav-tabs multi-config-tabs'>
|
|
19
|
+
{tabs.map((tab, index) => (
|
|
20
|
+
<li className='nav-item'>
|
|
21
|
+
<a className={`nav-link${activeTab === index ? ' active' : ''}`} aria-current={activeTab === index ? 'page' : null} href='#' onClick={() => load(index)}>
|
|
22
|
+
{tab}
|
|
23
|
+
</a>
|
|
24
|
+
</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default MultiTabs
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
.multi-config-tabs {
|
|
2
|
+
.nav-link {
|
|
3
|
+
font-weight: 400;
|
|
4
|
+
display: block;
|
|
5
|
+
padding: 0.5rem 1rem;
|
|
6
|
+
:is(button) {
|
|
7
|
+
display: none;
|
|
8
|
+
background: none;
|
|
9
|
+
}
|
|
10
|
+
&:hover {
|
|
11
|
+
:is(button) {
|
|
12
|
+
display: inline-block;
|
|
13
|
+
color: var(--gray);
|
|
14
|
+
&:hover {
|
|
15
|
+
color: var(--black);
|
|
16
|
+
font-weight: bold;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
.remove {
|
|
22
|
+
color: var(--gray);
|
|
23
|
+
&:hover {
|
|
24
|
+
color: var(--black);
|
|
25
|
+
font-weight: bold;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
.add {
|
|
29
|
+
font-weight: 800;
|
|
30
|
+
padding: 0.5rem 1rem;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -10,7 +10,7 @@ import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
|
10
10
|
import DataDesigner from '@cdc/core/components/managers/DataDesigner'
|
|
11
11
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
12
12
|
import Modal from '@cdc/core/components/ui/Modal'
|
|
13
|
-
import { Visualization } from '
|
|
13
|
+
import { Visualization } from '@cdc/core/types/Visualization'
|
|
14
14
|
|
|
15
15
|
const iconHash = {
|
|
16
16
|
'data-bite': <Icon display='databite' base />,
|
|
@@ -87,13 +87,16 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
87
87
|
updateConfig({ ...config, rows, visualizations })
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
const [{ isDragging, ...collected }, drag] = useDrag(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
const [{ isDragging, ...collected }, drag] = useDrag(
|
|
91
|
+
{
|
|
92
|
+
type: 'vis-widget',
|
|
93
|
+
end: handleWidgetMove,
|
|
94
|
+
collect: monitor => ({
|
|
95
|
+
isDragging: monitor.isDragging()
|
|
96
|
+
})
|
|
97
|
+
},
|
|
98
|
+
[config.activeDashboard, config.rows]
|
|
99
|
+
)
|
|
97
100
|
|
|
98
101
|
const deleteWidget = () => {
|
|
99
102
|
if (!data) return
|
|
@@ -266,6 +269,19 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
266
269
|
}
|
|
267
270
|
}, [data?.openModal])
|
|
268
271
|
|
|
272
|
+
let isConfigurationReady = false;
|
|
273
|
+
if(type === 'markup-include' || type === 'filter-dropdowns'){
|
|
274
|
+
isConfigurationReady = true;
|
|
275
|
+
} else if(data && data.formattedData) {
|
|
276
|
+
isConfigurationReady = true;
|
|
277
|
+
} else if(data && data.dataKey && data.dataDescription && config.datasets[data.dataKey]){
|
|
278
|
+
let formattedDataAttempt = transform.autoStandardize(config.datasets[data.dataKey].data);
|
|
279
|
+
formattedDataAttempt = transform.developerStandardize(formattedDataAttempt, data.dataDescription);
|
|
280
|
+
if(formattedDataAttempt){
|
|
281
|
+
isConfigurationReady = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
269
285
|
return (
|
|
270
286
|
<>
|
|
271
287
|
<div className='widget' ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }} {...collected}>
|
|
@@ -273,7 +289,7 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
273
289
|
<div className='widget__content'>
|
|
274
290
|
{data?.rowIdx !== undefined && (
|
|
275
291
|
<div className='widget-menu'>
|
|
276
|
-
{
|
|
292
|
+
{isConfigurationReady && (
|
|
277
293
|
<button title='Configure Visualization' className='btn btn-configure' onClick={editWidget}>
|
|
278
294
|
{iconHash['tools']}
|
|
279
295
|
</button>
|
|
@@ -13,7 +13,7 @@ export const generateValuesForFilter = (columnName, _data, filterBehavior) => {
|
|
|
13
13
|
const values: string[] = []
|
|
14
14
|
|
|
15
15
|
Object.keys(_data).forEach(key => {
|
|
16
|
-
_data[key]
|
|
16
|
+
_data[key]?.forEach(row => {
|
|
17
17
|
const value = row[columnName]
|
|
18
18
|
if (value && false === values.includes(value)) {
|
|
19
19
|
values.push(value)
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { DashboardState } from '../store/dashboard.reducer'
|
|
2
|
-
import { Config } from '../types/
|
|
2
|
+
import { DashboardConfig as Config, DashboardConfig } from '../types/DashboardConfig'
|
|
3
3
|
import { filterData } from './filterData'
|
|
4
4
|
import { generateValuesForFilter } from './generateValuesForFilter'
|
|
5
5
|
import { getFormattedData } from './getFormattedData'
|
|
6
6
|
import { getVizKeys } from './getVizKeys'
|
|
7
7
|
|
|
8
|
+
type UpdateState = Omit<DashboardState, 'config'> & {
|
|
9
|
+
config?: DashboardConfig
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
export const getUpdateConfig =
|
|
9
|
-
(state:
|
|
13
|
+
(state: UpdateState) =>
|
|
10
14
|
(newConfig, dataOverride?: Object): [Config, Object] => {
|
|
11
15
|
let newFilteredData = {}
|
|
12
16
|
let visualizationKeys = getVizKeys(newConfig)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FilterBehavior } from '../components/Header/Header'
|
|
2
|
+
import { DataSet } from '../types/DataSet'
|
|
3
|
+
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
4
|
+
import { getFormattedData } from './getFormattedData'
|
|
5
|
+
|
|
6
|
+
export const processData = async (dataSet: DataSet, filterBehavior) => {
|
|
7
|
+
if (dataSet.dataUrl && filterBehavior !== FilterBehavior.Apply) {
|
|
8
|
+
const dataset = await fetchRemoteData(`${dataSet.dataUrl}`)
|
|
9
|
+
return getFormattedData(dataset, dataSet.dataDescription)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return dataSet.formattedData || dataSet.data
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
2
|
+
import { getFormattedData } from './getFormattedData'
|
|
3
|
+
|
|
4
|
+
export const processDataLegacy = async (response: any) => {
|
|
5
|
+
let dataset = response.formattedData || response.data
|
|
6
|
+
|
|
7
|
+
if (response.dataUrl) {
|
|
8
|
+
dataset = await fetchRemoteData(`${response.dataUrl}`)
|
|
9
|
+
|
|
10
|
+
dataset = getFormattedData(dataset, response.dataDescription)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return dataset
|
|
14
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import ReactDOM from 'react-dom/client'
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import MultiDashboardWrapper from './CdcDashboard'
|
|
5
5
|
|
|
6
6
|
let isEditor = window.location.href.includes('editor=true')
|
|
7
7
|
let isDebug = window.location.href.includes('debug=true')
|
|
@@ -10,6 +10,6 @@ let domContainer = document.getElementsByClassName('react-container')[0]
|
|
|
10
10
|
|
|
11
11
|
ReactDOM.createRoot(domContainer).render(
|
|
12
12
|
<React.StrictMode>
|
|
13
|
-
<
|
|
13
|
+
<MultiDashboardWrapper configUrl={domContainer.attributes['data-config'].value} isEditor={isEditor} isDebug={isDebug} />
|
|
14
14
|
</React.StrictMode>
|
|
15
15
|
)
|
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
.editor-panel + .cdc-dashboard-inner-container,
|
|
4
4
|
.editor-heading + .cdc-open-viz-module {
|
|
5
5
|
position: relative;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
min-height: 80vh;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.header-container {
|
|
10
|
+
display: flex;
|
|
11
|
+
position: sticky;
|
|
12
|
+
top: 0;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
width: calc(350px + 1em);
|
|
15
|
+
max-height: 95vh;
|
|
16
|
+
z-index: 3;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
.editor-panel {
|
|
@@ -83,12 +87,12 @@
|
|
|
83
87
|
background: #fff;
|
|
84
88
|
width: $editorWidth;
|
|
85
89
|
overflow-y: overlay;
|
|
86
|
-
position:
|
|
90
|
+
position: absolute;
|
|
87
91
|
z-index: 8;
|
|
88
92
|
display: flex;
|
|
89
93
|
flex-direction: column;
|
|
90
94
|
left: 0;
|
|
91
|
-
top:
|
|
95
|
+
top: 0;
|
|
92
96
|
bottom: 0;
|
|
93
97
|
.accordion__heading {
|
|
94
98
|
background: $lightestGray;
|
|
@@ -587,14 +591,13 @@
|
|
|
587
591
|
color: #000;
|
|
588
592
|
font-size: 1em;
|
|
589
593
|
border: 0;
|
|
590
|
-
position:
|
|
594
|
+
position: absolute;
|
|
591
595
|
z-index: 99;
|
|
592
596
|
transition: 0.1s background;
|
|
593
597
|
cursor: pointer;
|
|
594
598
|
width: 25px;
|
|
595
599
|
height: 25px;
|
|
596
600
|
left: 307px;
|
|
597
|
-
top: calc(3em + 10px);
|
|
598
601
|
box-shadow: rgba(0, 0, 0, 0.5) 0 1px 2px;
|
|
599
602
|
&:before {
|
|
600
603
|
top: 43%;
|
package/src/scss/grid.scss
CHANGED
|
@@ -3,18 +3,17 @@ $red: #f74242;
|
|
|
3
3
|
|
|
4
4
|
.layout-container {
|
|
5
5
|
display: flex;
|
|
6
|
+
margin-top: -80vh;
|
|
7
|
+
margin-left: calc($editorWidth + 1em);
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
.visualizations-panel {
|
|
9
11
|
background-color: #fff;
|
|
10
12
|
padding: 1em;
|
|
11
|
-
width:
|
|
13
|
+
width: $editorWidth;
|
|
12
14
|
border-right: #c7c7c7 1px solid;
|
|
13
|
-
position: fixed;
|
|
14
|
-
top: 6em;
|
|
15
|
-
bottom: 0;
|
|
16
15
|
z-index: 1;
|
|
17
|
-
overflow-y:
|
|
16
|
+
overflow-y: scroll;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
.hidden.editor-panel + .builder-grid {
|
|
@@ -29,7 +28,6 @@ $red: #f74242;
|
|
|
29
28
|
.builder-grid {
|
|
30
29
|
flex-grow: 1;
|
|
31
30
|
padding: 5em 3em 3em;
|
|
32
|
-
margin-left: calc(350px + 1em);
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
.column-container {
|
package/src/scss/main.scss
CHANGED
|
@@ -15,11 +15,8 @@
|
|
|
15
15
|
border-bottom: #c7c7c7 1px solid;
|
|
16
16
|
padding: 0 1em;
|
|
17
17
|
display: flex;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
width: 100%;
|
|
21
|
-
z-index: 19;
|
|
22
|
-
top: 0;
|
|
18
|
+
width: 100vw;
|
|
19
|
+
z-index: 5;
|
|
23
20
|
&.sub-dashboard-viz {
|
|
24
21
|
height: 3em;
|
|
25
22
|
}
|
|
@@ -285,9 +282,6 @@
|
|
|
285
282
|
.editor-panel {
|
|
286
283
|
top: 40px;
|
|
287
284
|
}
|
|
288
|
-
.editor-toggle {
|
|
289
|
-
top: 3em;
|
|
290
|
-
}
|
|
291
285
|
}
|
|
292
286
|
}
|
|
293
287
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { Config } from '../types/
|
|
2
|
-
|
|
3
|
-
type Action<T, P> = { type: T; payload: P }
|
|
1
|
+
import type { DashboardConfig as Config } from '../types/DashboardConfig'
|
|
2
|
+
import { type Action } from '@cdc/core/types/Action'
|
|
4
3
|
|
|
5
4
|
type SET_CONFIG = Action<'SET_CONFIG', Config>
|
|
6
5
|
type UPDATE_CONFIG = Action<'UPDATE_CONFIG', [Config, Object?]>
|
|
@@ -9,6 +8,13 @@ type SET_LOADING = Action<'SET_LOADING', boolean>
|
|
|
9
8
|
type SET_PREVIEW = Action<'SET_PREVIEW', boolean>
|
|
10
9
|
type SET_FILTERED_DATA = Action<'SET_FILTERED_DATA', Object>
|
|
11
10
|
type SET_TAB_SELECTED = Action<'SET_TAB_SELECTED', number>
|
|
11
|
+
type RENAME_DASHBOARD_TAB = Action<'RENAME_DASHBOARD_TAB', { current: string; new: string }>
|
|
12
|
+
type INITIALIZE_MULTIDASHBOARDS = Action<'INITIALIZE_MULTIDASHBOARDS', undefined>
|
|
13
|
+
type REMOVE_MULTIDASHBOARD_AT_INDEX = Action<'REMOVE_MULTIDASHBOARD_AT_INDEX', number>
|
|
14
|
+
type REORDER_MULTIDASHBOARDS = Action<'REORDER_MULTIDASHBOARDS', { currentIndex: number; newIndex: number }>
|
|
15
|
+
type ADD_NEW_DASHBOARD = Action<'ADD_NEW_DASHBOARD', undefined>
|
|
16
|
+
type SAVE_CURRENT_CHANGES = Action<'SAVE_CURRENT_CHANGES', undefined>
|
|
17
|
+
type SWITCH_CONFIG = Action<'SWITCH_CONFIG', number>
|
|
12
18
|
|
|
13
|
-
type DashboardActions = SET_CONFIG | UPDATE_CONFIG | SET_DATA | SET_LOADING | SET_PREVIEW | SET_FILTERED_DATA | SET_TAB_SELECTED
|
|
19
|
+
type DashboardActions = ADD_NEW_DASHBOARD | SET_CONFIG | UPDATE_CONFIG | REMOVE_MULTIDASHBOARD_AT_INDEX | RENAME_DASHBOARD_TAB | REORDER_MULTIDASHBOARDS | SAVE_CURRENT_CHANGES | SET_DATA | SET_LOADING | SET_PREVIEW | SET_FILTERED_DATA | SET_TAB_SELECTED | SWITCH_CONFIG | INITIALIZE_MULTIDASHBOARDS
|
|
14
20
|
export default DashboardActions
|
|
@@ -1,17 +1,40 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
1
2
|
import { getUpdateConfig } from '../helpers/getUpdateConfig'
|
|
2
|
-
import {
|
|
3
|
+
import { MultiDashboardConfig } from '../types/MultiDashboard'
|
|
3
4
|
import DashboardActions from './dashboard.actions'
|
|
5
|
+
import { devToolsWrapper } from '@cdc/core/helpers/withDevTools'
|
|
6
|
+
|
|
7
|
+
const createBlankDashboard = () => ({
|
|
8
|
+
dashboard: {
|
|
9
|
+
theme: 'theme-blue'
|
|
10
|
+
},
|
|
11
|
+
rows: [[{ width: 12 }, {}, {}]],
|
|
12
|
+
visualizations: {},
|
|
13
|
+
table: {
|
|
14
|
+
label: 'Data Table',
|
|
15
|
+
show: true,
|
|
16
|
+
showDownloadUrl: false,
|
|
17
|
+
showVertical: true
|
|
18
|
+
}
|
|
19
|
+
})
|
|
4
20
|
|
|
5
21
|
export type DashboardState = {
|
|
6
|
-
config
|
|
22
|
+
config: MultiDashboardConfig
|
|
7
23
|
data: Object
|
|
8
24
|
filteredData: Object
|
|
9
25
|
loading: boolean
|
|
10
26
|
preview: boolean
|
|
11
27
|
tabSelected: number
|
|
12
28
|
}
|
|
29
|
+
|
|
13
30
|
const reducer = (state: DashboardState, action: DashboardActions): DashboardState => {
|
|
14
31
|
switch (action.type) {
|
|
32
|
+
case 'ADD_NEW_DASHBOARD': {
|
|
33
|
+
const currentMultiDashboards = state.config.multiDashboards
|
|
34
|
+
const label = 'New Dashboard ' + (currentMultiDashboards.length + 1)
|
|
35
|
+
const newMultiDashboards = [...currentMultiDashboards, { ...createBlankDashboard(), label }]
|
|
36
|
+
return applyMultiDashboards(state, newMultiDashboards)
|
|
37
|
+
}
|
|
15
38
|
case 'UPDATE_CONFIG': {
|
|
16
39
|
const [config, filteredData] = getUpdateConfig(state)(...action.payload)
|
|
17
40
|
return { ...state, config, filteredData }
|
|
@@ -34,9 +57,57 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
|
|
|
34
57
|
case 'SET_TAB_SELECTED': {
|
|
35
58
|
return { ...state, tabSelected: action.payload }
|
|
36
59
|
}
|
|
60
|
+
case 'REMOVE_MULTIDASHBOARD_AT_INDEX': {
|
|
61
|
+
const newMultiDashboards = [...state.config.multiDashboards]
|
|
62
|
+
_.remove(newMultiDashboards, (_, index) => {
|
|
63
|
+
return index === action.payload
|
|
64
|
+
})
|
|
65
|
+
if (newMultiDashboards.length === 0) return { ...state, config: _.omit(state.config, 'multiDashboards') }
|
|
66
|
+
return applyMultiDashboards(state, newMultiDashboards)
|
|
67
|
+
}
|
|
68
|
+
case 'RENAME_DASHBOARD_TAB': {
|
|
69
|
+
const newMultiDashboards = state.config.multiDashboards.map(dashboard => {
|
|
70
|
+
if (dashboard.label === action.payload.current) {
|
|
71
|
+
dashboard.label = action.payload.new
|
|
72
|
+
}
|
|
73
|
+
return dashboard
|
|
74
|
+
})
|
|
75
|
+
const newConfig = { ...state.config, label: action.payload.new } // make sure active label is updated
|
|
76
|
+
return applyMultiDashboards({ ...state, newConfig }, newMultiDashboards)
|
|
77
|
+
}
|
|
78
|
+
case 'REORDER_MULTIDASHBOARDS': {
|
|
79
|
+
const { newIndex, currentIndex } = action.payload
|
|
80
|
+
const newMultiDashboards = [...state.config.multiDashboards]
|
|
81
|
+
newMultiDashboards.splice(newIndex, 0, newMultiDashboards.splice(currentIndex, 1)[0])
|
|
82
|
+
// set activeDashboard to newIndex
|
|
83
|
+
const config = { ...state.config, activeDashboard: newIndex }
|
|
84
|
+
return applyMultiDashboards({ ...state, config }, newMultiDashboards)
|
|
85
|
+
}
|
|
86
|
+
case 'SAVE_CURRENT_CHANGES': {
|
|
87
|
+
const saveSlot = state.config.activeDashboard
|
|
88
|
+
const newMultiDashboards = [...state.config.multiDashboards]
|
|
89
|
+
const label = newMultiDashboards[saveSlot].label
|
|
90
|
+
const toSave = _.pick(state.config, ['dashboard', 'visualizations', 'rows'])
|
|
91
|
+
newMultiDashboards[saveSlot] = { ...toSave, label }
|
|
92
|
+
return applyMultiDashboards(state, newMultiDashboards)
|
|
93
|
+
}
|
|
94
|
+
case 'INITIALIZE_MULTIDASHBOARDS': {
|
|
95
|
+
const label = 'New Dashboard 1'
|
|
96
|
+
const toSave = _.pick(state.config, ['dashboard', 'visualizations', 'rows'])
|
|
97
|
+
const newMultiDashboards = [{ ...toSave, label }]
|
|
98
|
+
const config = { ...state.config, activeDashboard: 0 }
|
|
99
|
+
return applyMultiDashboards({ ...state, config }, newMultiDashboards)
|
|
100
|
+
}
|
|
101
|
+
case 'SWITCH_CONFIG': {
|
|
102
|
+
const slot = action.payload
|
|
103
|
+
const newConfigFields = state.config.multiDashboards[slot]
|
|
104
|
+
return { ...state, config: { ...state.config, ...newConfigFields, activeDashboard: slot } }
|
|
105
|
+
}
|
|
37
106
|
default:
|
|
38
107
|
return state
|
|
39
108
|
}
|
|
40
109
|
}
|
|
41
110
|
|
|
42
|
-
|
|
111
|
+
const applyMultiDashboards = (state, newMultiDashboards) => ({ ...state, config: { ...state.config, multiDashboards: newMultiDashboards } })
|
|
112
|
+
|
|
113
|
+
export default devToolsWrapper<DashboardState, DashboardActions>(reducer)
|