@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.
Files changed (53) hide show
  1. package/dist/cdcdashboard.js +117195 -108041
  2. package/examples/{private/DEV-6574.json → DEV-6574.json} +8 -8
  3. package/examples/filters/Alabama.json +72 -0
  4. package/examples/zika.json +2274 -0
  5. package/index.html +5 -3
  6. package/package.json +9 -9
  7. package/src/CdcDashboard.tsx +124 -991
  8. package/src/CdcDashboardComponent.tsx +903 -0
  9. package/src/_stories/Dashboard.stories.tsx +2 -2
  10. package/src/components/Column.tsx +15 -12
  11. package/src/components/Header/Header.tsx +18 -4
  12. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +106 -0
  13. package/src/components/MultiConfigTabs/MultiTabs.tsx +30 -0
  14. package/src/components/MultiConfigTabs/index.tsx +8 -0
  15. package/src/components/MultiConfigTabs/multiconfigtabs.styles.css +32 -0
  16. package/src/components/Widget.tsx +25 -9
  17. package/src/helpers/generateValuesForFilter.ts +1 -1
  18. package/src/helpers/getUpdateConfig.ts +6 -2
  19. package/src/helpers/processData.ts +13 -0
  20. package/src/helpers/processDataLegacy.ts +14 -0
  21. package/src/{index.jsx → index.tsx} +2 -2
  22. package/src/scss/editor-panel.scss +14 -11
  23. package/src/scss/grid.scss +4 -6
  24. package/src/scss/main.scss +2 -8
  25. package/src/store/dashboard.actions.ts +10 -4
  26. package/src/store/dashboard.reducer.ts +74 -3
  27. package/src/types/ConfigRow.ts +6 -0
  28. package/src/types/Dashboard.ts +11 -0
  29. package/src/types/{Config.ts → DashboardConfig.ts} +8 -14
  30. package/src/types/InitialState.ts +10 -0
  31. package/src/types/MultiDashboard.ts +11 -0
  32. package/examples/private/DEV-5189-2.json +0 -935
  33. package/examples/private/DEV-5189.json +0 -1266
  34. package/examples/private/dash-scaling.json +0 -45325
  35. package/examples/private/epi-chart.json +0 -813
  36. package/examples/private/epi-dash.json +0 -457
  37. package/examples/private/epi-new.json +0 -994
  38. package/examples/private/filters/Alabama.json +0 -4217
  39. package/examples/private/new-epi-csv.json +0 -358
  40. package/examples/private/new-epi.json +0 -358
  41. package/examples/private/no-lines.json +0 -8432
  42. package/examples/private/tick-bold-data.json +0 -82325
  43. package/examples/private/tick-bold.json +0 -164678
  44. package/examples/private/visits.json +0 -9985
  45. /package/examples/{private/filters → filters}/Alaska.json +0 -0
  46. /package/examples/{private/filters → filters}/Arkansas.json +0 -0
  47. /package/examples/{private/filters → filters}/California.json +0 -0
  48. /package/examples/{private/filters → filters}/Colorado.json +0 -0
  49. /package/examples/{private/filters → filters}/Connecticut.json +0 -0
  50. /package/examples/{private/filters → filters}/Delaware.json +0 -0
  51. /package/examples/{private/filters → filters}/DistrictofColumbia.json +0 -0
  52. /package/examples/{private/filters → filters}/Florida.json +0 -0
  53. /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 '../CdcDashboard'
9
- import { Config } from '../types/Config'
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
- accept: 'vis-widget',
12
- drop: () => ({
13
- rowIdx,
14
- colIdx,
15
- canDrop
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
- canDrop: () => !data.widget,
18
- collect: monitor => ({
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/Config'
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('parent', index, e.target.value)
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 && sharedFilter.type !== 'urlfilter') {
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,8 @@
1
+ import MultiConfigTabs from './MultiConfigTabs'
2
+ import MultiTabs from './MultiTabs'
3
+
4
+ const Tabs: React.FC<{ isEditor: boolean }> = ({ isEditor = false }) => {
5
+ return isEditor ? <MultiConfigTabs /> : <MultiTabs />
6
+ }
7
+
8
+ export default Tabs
@@ -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 '../types/Config'
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
- type: 'vis-widget',
92
- end: handleWidgetMove,
93
- collect: monitor => ({
94
- isDragging: monitor.isDragging()
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
- {((data.dataKey && data.dataDescription && (data.formattedData || (config.datasets[data.dataKey] && transform.autoStandardize(config.datasets[data.dataKey].data)))) || type === 'markup-include') && type !== 'filter-dropdowns' && (
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].forEach(row => {
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/Config'
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: DashboardState) =>
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 CdcDashboard from './CdcDashboard'
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
- <CdcDashboard configUrl={domContainer.attributes['data-config'].value} isEditor={isEditor} isDebug={isDebug} />
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
- top: 3em;
7
- .editor-panel {
8
- top: 3em;
9
- }
10
- .editor-toggle {
11
- top: 3.5em;
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: fixed;
90
+ position: absolute;
87
91
  z-index: 8;
88
92
  display: flex;
89
93
  flex-direction: column;
90
94
  left: 0;
91
- top: 48px;
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: fixed;
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%;
@@ -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: 350px;
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: auto;
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 {
@@ -15,11 +15,8 @@
15
15
  border-bottom: #c7c7c7 1px solid;
16
16
  padding: 0 1em;
17
17
  display: flex;
18
- height: 6em;
19
- position: fixed;
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/Config'
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 { Config } from '../types/Config'
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?: 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
- export default reducer
111
+ const applyMultiDashboards = (state, newMultiDashboards) => ({ ...state, config: { ...state.config, multiDashboards: newMultiDashboards } })
112
+
113
+ export default devToolsWrapper<DashboardState, DashboardActions>(reducer)