@cdc/dashboard 1.1.1 → 1.1.4

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.
@@ -0,0 +1,354 @@
1
+ import React, { useState, useEffect, memo, useContext } from 'react'
2
+ import ReactTooltip from 'react-tooltip'
3
+
4
+ import {
5
+ Accordion,
6
+ AccordionItem,
7
+ AccordionItemHeading,
8
+ AccordionItemPanel,
9
+ AccordionItemButton,
10
+ } from 'react-accessible-accordion';
11
+ import { useDebounce } from 'use-debounce';
12
+
13
+ import Context from '../context';
14
+
15
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary';
16
+ import QuestionIcon from '@cdc/core/assets/question-circle.svg';
17
+ import Tooltip from '@cdc/core/components/ui/Tooltip'
18
+ import Icon from '@cdc/core/components/ui/Icon'
19
+
20
+ const Helper = ({text}) => {
21
+ return (
22
+ <span className='tooltip helper' data-tip={text}>
23
+ <QuestionIcon />
24
+ </span>
25
+ )
26
+ }
27
+
28
+ // IE11 Custom Event polyfill
29
+ (function () {
30
+
31
+ if ( typeof window.CustomEvent === "function" ) return false;
32
+
33
+ function CustomEvent ( event, params ) {
34
+ params = params || { bubbles: false, cancelable: false, detail: null };
35
+ var evt = document.createEvent( 'CustomEvent' );
36
+ evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
37
+ return evt;
38
+ }
39
+
40
+ window.CustomEvent = CustomEvent;
41
+ })();
42
+
43
+ const TextField = memo(({label, section = null, subsection = null, fieldName, updateField, value: stateValue, tooltip, type = "input", i = null, min = null, ...attributes}) => {
44
+ const [ value, setValue ] = useState(stateValue);
45
+
46
+ const [ debouncedValue ] = useDebounce(value, 500);
47
+
48
+ useEffect(() => {
49
+ if('string' === typeof debouncedValue && stateValue !== debouncedValue ) {
50
+ updateField(section, subsection, fieldName, debouncedValue, i)
51
+ }
52
+ }, [debouncedValue])
53
+
54
+ let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`;
55
+
56
+ const onChange = (e) => {
57
+ if('number' !== type || min === null){
58
+ setValue(e.target.value);
59
+ } else {
60
+ if(!e.target.value || min <= parseFloat(e.target.value)){
61
+ setValue(e.target.value);
62
+ } else {
63
+ setValue(min.toString());
64
+ }
65
+ }
66
+ };
67
+
68
+ let formElement = <input type="text" name={name} onChange={onChange} {...attributes} value={value} />
69
+
70
+ if('textarea' === type) {
71
+ formElement = (
72
+ <textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>
73
+ )
74
+ }
75
+
76
+ if('number' === type) {
77
+ formElement = <input type="number" name={name} onChange={onChange} {...attributes} value={value} />
78
+ }
79
+
80
+ return (
81
+ <label>
82
+ <span className="edit-label column-heading">{label}{tooltip}</span>
83
+ {formElement}
84
+ </label>
85
+ )
86
+ })
87
+
88
+ const CheckBox = memo(({label, value, fieldName, section = null, subsection = null, updateField, ...attributes}) => (
89
+ <label className="checkbox">
90
+ <input type="checkbox" name={fieldName} checked={ value } onChange={() => { updateField(section, subsection, fieldName, !value) }} {...attributes}/>
91
+ <span className="edit-label">{label}</span>
92
+ {section === 'table' && fieldName === 'show' && <Helper text=" Hiding the data table may affect accessibility. An alternate form of accessing visualization data is a 508 requirement." />}
93
+ </label>
94
+ ))
95
+
96
+ const Select = memo(({label, value, options, fieldName, section = null, subsection = null, required = false, updateField, initial: initialValue, ...attributes}) => {
97
+ let optionsJsx = options.map(optionName => <option value={optionName} key={optionName}>{optionName}</option>)
98
+
99
+ if(initialValue) {
100
+ optionsJsx.unshift(<option value="" key="initial">{initialValue}</option>)
101
+ }
102
+
103
+ return (
104
+ <label>
105
+ <span className="edit-label">{label}</span>
106
+ <select className={required && !value ? 'warning' : ''} name={fieldName} value={value} onChange={(event) => { updateField(section, subsection, fieldName, event.target.value) }} {...attributes}>
107
+ {optionsJsx}
108
+ </select>
109
+ </label>
110
+ )
111
+ })
112
+
113
+ const EditorPanel = memo(() => {
114
+ const {
115
+ config,
116
+ updateConfig,
117
+ loading,
118
+ rawData,
119
+ setParentConfig,
120
+ setEditing
121
+ } = useContext(Context);
122
+
123
+ const enforceRestrictions = (updatedConfig) => {
124
+ // TODO
125
+ };
126
+
127
+ const updateField = (section, subsection, fieldName, newValue) => {
128
+ // Top level
129
+ if( null === section && null === subsection) {
130
+ let dashboardConfig = config.dashboard;
131
+
132
+ dashboardConfig[fieldName] = newValue;
133
+
134
+ let updatedConfig = {...config, dashboard: dashboardConfig};
135
+
136
+ enforceRestrictions(updatedConfig);
137
+
138
+ updateConfig(updatedConfig);
139
+ return
140
+ }
141
+
142
+ const isArray = Array.isArray(config[section]);
143
+
144
+ let sectionValue = isArray ? [...config[section], newValue] : {...config[section], [fieldName]: newValue};
145
+
146
+ if(null !== subsection) {
147
+ if(isArray) {
148
+ sectionValue = [...config[section]]
149
+ sectionValue[subsection] = {...sectionValue[subsection], [fieldName]: newValue}
150
+ } else if(typeof newValue === "string") {
151
+ sectionValue[subsection] = newValue
152
+ } else {
153
+ sectionValue = {...config[section], [subsection]: { ...config[section][subsection], [fieldName]: newValue}}
154
+ }
155
+ }
156
+
157
+ let updatedConfig = {...config, [section]: sectionValue};
158
+
159
+ enforceRestrictions(updatedConfig);
160
+
161
+ updateConfig(updatedConfig)
162
+ }
163
+
164
+ const missingRequiredSections = () => {
165
+ //TODO
166
+
167
+ return false;
168
+ };
169
+
170
+ const [ displayPanel, setDisplayPanel ] = useState(true);
171
+
172
+ // Used to pipe a JSON version of the config you are creating out
173
+ const [ configData, setConfigData ] = useState({})
174
+
175
+ if(loading) {
176
+ return null
177
+ }
178
+
179
+ const getColumns = (filter = true) => {
180
+ let columns = {}
181
+
182
+ rawData.map(row => {
183
+ Object.keys(row).forEach(columnName => columns[columnName] = true)
184
+ })
185
+
186
+ if(filter) {
187
+ Object.keys(columns).forEach(key => {
188
+ if((config.series && config.series.filter(series => series.dataKey === key).length > 0) || (config.confidenceKeys && Object.keys(config.confidenceKeys).includes(key)) ) {
189
+ delete columns[key]
190
+ }
191
+ })
192
+ }
193
+
194
+ return Object.keys(columns)
195
+ }
196
+
197
+ const Error = () => {
198
+ return (
199
+ <section className="waiting">
200
+ <section className="waiting-container">
201
+ <h3>Error With Configuration</h3>
202
+ <p>{config.runtime.editorErrorMessage}</p>
203
+ </section>
204
+ </section>
205
+ );
206
+ }
207
+
208
+ const convertStateToConfig = (type = "JSON") => {
209
+ let strippedState = JSON.parse(JSON.stringify(config))
210
+ if(false === missingRequiredSections()) {
211
+ delete strippedState.newViz
212
+ }
213
+ delete strippedState.runtime
214
+
215
+ if(type === "JSON") {
216
+ return JSON.stringify( strippedState )
217
+ }
218
+
219
+ return strippedState
220
+ }
221
+
222
+ useEffect(() => {
223
+ const parsedData = convertStateToConfig()
224
+
225
+ const formattedData = JSON.stringify(JSON.parse(parsedData), undefined, 2);
226
+
227
+ setConfigData(formattedData)
228
+
229
+ // Emit the data in a regular JS event so it can be consumed by anything.
230
+ const event = new CustomEvent('updateVizConfig', { detail: parsedData})
231
+
232
+ window.dispatchEvent(event)
233
+
234
+ // Pass up to Editor if needed
235
+ if(setParentConfig) {
236
+ const newConfig = convertStateToConfig("object")
237
+ setParentConfig(newConfig)
238
+ }
239
+
240
+ // eslint-disable-next-line react-hooks/exhaustive-deps
241
+ }, [config]);
242
+
243
+ const removeFilter = (index) => {
244
+ let dashboardConfig = config.dashboard;
245
+
246
+ dashboardConfig.filters.splice(index, 1);
247
+
248
+ updateConfig({...config, dashboard: dashboardConfig});
249
+ }
250
+
251
+ const updateFilterProp = (name, index, value) => {
252
+ let dashboardConfig = config.dashboard;
253
+
254
+ dashboardConfig.filters[index][name] = value;
255
+
256
+ updateConfig({...config, dashboard: dashboardConfig});
257
+ }
258
+
259
+ const addNewFilter = () => {
260
+ let dashboardConfig = config.dashboard;
261
+
262
+ dashboardConfig.filters = dashboardConfig.filters || [];
263
+
264
+ dashboardConfig.filters.push({values: []});
265
+
266
+ updateConfig({...config, dashboard: dashboardConfig});
267
+ }
268
+
269
+ return (
270
+ <ErrorBoundary component="EditorPanel">
271
+ {config.runtime && config.runtime.editorErrorMessage && <Error /> }
272
+ <button className={displayPanel ? `editor-toggle` : `editor-toggle collapsed`} title={displayPanel ? `Collapse Editor` : `Expand Editor`} onClick={() => setDisplayPanel(!displayPanel) }></button>
273
+ <section className={displayPanel ? 'editor-panel cove' : 'hidden editor-panel cove'}>
274
+ <div className="heading-2">Configure</div>
275
+ <section className="form-container">
276
+ <form>
277
+ <Accordion allowZeroExpanded={true}>
278
+ <AccordionItem> {/* General */}
279
+ <AccordionItemHeading>
280
+ <AccordionItemButton>
281
+ General
282
+ </AccordionItemButton>
283
+ </AccordionItemHeading>
284
+ <AccordionItemPanel>
285
+ <TextField value={config.dashboard.title} section="dashboard" fieldName="title" label="Title" updateField={updateField} />
286
+ <TextField type="textarea" value={config.dashboard.description} section="dashboard" fieldName="description" label="Description" updateField={updateField} tooltip={
287
+ <Tooltip style={{textTransform: 'none'}}>
288
+ <Tooltip.Target><Icon display="question" style={{marginLeft: '0.5rem'}}/></Tooltip.Target>
289
+ <Tooltip.Content>
290
+ <p>Enter supporting text to display below the data visualization, if applicable. The following HTML tags are supported: strong, em, sup, and sub.</p>
291
+ </Tooltip.Content>
292
+ </Tooltip>
293
+ }/>
294
+ </AccordionItemPanel>
295
+ </AccordionItem>
296
+ <AccordionItem>
297
+ <AccordionItemHeading>
298
+ <AccordionItemButton>
299
+ Filters
300
+ </AccordionItemButton>
301
+ </AccordionItemHeading>
302
+ <AccordionItemPanel>
303
+ <ul className="filters-list">
304
+ {config.dashboard.filters && config.dashboard.filters.map((filter, index) => (
305
+ <fieldset className="edit-block" key={filter.columnName + index}>
306
+ <button type="button" className="remove-column" onClick={() => {removeFilter(index)}}>Remove</button>
307
+ <label>
308
+ <span className="edit-label column-heading">Filter</span>
309
+ <select value={filter.columnName} onChange={(e) => {updateFilterProp('columnName', index, e.target.value)}}>
310
+ <option value="">- Select Option -</option>
311
+ {getColumns().map((dataKey) => (
312
+ <option value={dataKey} key={dataKey}>{dataKey}</option>
313
+ ))}
314
+ </select>
315
+ </label>
316
+ <label>
317
+ <span className="edit-label column-heading">Label</span>
318
+ <input type="text" value={filter.label} onChange={(e) => {updateFilterProp('label', index, e.target.value)}}/>
319
+ </label>
320
+ </fieldset>
321
+ )
322
+ )}
323
+ </ul>
324
+
325
+ <button type="button" onClick={addNewFilter} className="btn btn-primary">Add Filter</button>
326
+ </AccordionItemPanel>
327
+ </AccordionItem>
328
+ <AccordionItem>
329
+ <AccordionItemHeading>
330
+ <AccordionItemButton>
331
+ Data Table
332
+ </AccordionItemButton>
333
+ </AccordionItemHeading>
334
+ <AccordionItemPanel>
335
+ <CheckBox value={config.table.show} section="table" fieldName="show" label="Show Table" updateField={updateField} />
336
+ <CheckBox value={config.table.expanded} section="table" fieldName="expanded" label="Expanded by Default" updateField={updateField} />
337
+ <CheckBox value={config.table.download} section="table" fieldName="download" label="Display Download Button" updateField={updateField} />
338
+ <TextField value={config.table.label} section="table" fieldName="label" label="Label" updateField={updateField} />
339
+ </AccordionItemPanel>
340
+ </AccordionItem>
341
+ </Accordion>
342
+ </form>
343
+ </section>
344
+ <ReactTooltip
345
+ html={true}
346
+ multiline={true}
347
+ className="helper-tooltip"
348
+ />
349
+ </section>
350
+ </ErrorBoundary>
351
+ )
352
+ })
353
+
354
+ export default EditorPanel;
@@ -0,0 +1,28 @@
1
+ import React, { useContext } from 'react'
2
+ import Row from './Row'
3
+
4
+ import Context from '../context'
5
+
6
+ const Grid = () => {
7
+ const { rows, config, updateConfig } = useContext(Context)
8
+
9
+ const addRow = () => {
10
+ updateConfig({
11
+ ...config,
12
+ rows: [
13
+ ...rows,
14
+ [{width: 12}, {}, {}]
15
+ ],
16
+ uuid: Date.now()
17
+ })
18
+ }
19
+
20
+ return (
21
+ <div className="builder-grid">
22
+ {rows.map((row, idx) => <Row row={row} idx={idx} uuid={row.uuid} key={idx}/>)}
23
+ <button className="btn add-row" onClick={addRow}>Add Row</button>
24
+ </div>
25
+ )
26
+ }
27
+
28
+ export default Grid
@@ -0,0 +1,15 @@
1
+ import React, { useContext, memo } from 'react'
2
+
3
+ const Header = ({preview, setPreview, back, subEditor = null}) => {
4
+ return (
5
+ <div aria-level="2" role="heading" className="editor-heading">
6
+ {subEditor ? <div className="heading-1 back-to" onClick={back} style={{cursor: 'pointer'}}><span>&#8592;</span> Back to Dashboard</div> : <div className="heading-1">Dashboard Editor</div>}
7
+ {!subEditor && <ul className="toggle-bar">
8
+ <li className={preview ? 'inactive' : 'active'} onClick={() => {setPreview(false)}}>Build Layout</li>
9
+ <li className={preview ? 'active' : 'inactive'} onClick={() => {setPreview(true)}}>Preview &amp; Configure</li>
10
+ </ul>}
11
+ </div>
12
+ )
13
+ }
14
+
15
+ export default Header
@@ -0,0 +1,137 @@
1
+ import React, { useContext, useState } from 'react'
2
+ import Column from './Column'
3
+ import Context from '../context'
4
+ import CloseIcon from '../images/icon-close.svg'
5
+ import RowUp from '../images/icon-up.svg'
6
+ import RowDown from '../images/icon-down.svg'
7
+ import OneColIcon from '../images/icon-col-12.svg'
8
+ import TwoColIcon from '../images/icon-col-6.svg'
9
+ import ThreeColIcon from '../images/icon-col-4.svg'
10
+ import FourEightColIcon from '../images/icon-col-4-8.svg'
11
+ import EightFourColIcon from '../images/icon-col-8-4.svg'
12
+
13
+ const RowMenu = ({ rowIdx, row }) => {
14
+ const { rows, config, updateConfig } = useContext(Context)
15
+
16
+ const getCurr = () => {
17
+ let res = []
18
+
19
+ for (let i = 0; i < row.length; i++) {
20
+ if(row[i].width) res.push(row[i].width)
21
+ }
22
+
23
+ return res.join('')
24
+ }
25
+
26
+ const [curr, setCurr] = useState(getCurr())
27
+
28
+ const setRowLayout = (layout) => {
29
+ const newRows = [...rows]
30
+ const r = newRows[rowIdx]
31
+
32
+ for (let i = 0; i < r.length; i++) {
33
+ r[i].width = layout[i] ?? null
34
+ }
35
+
36
+ updateConfig({ ...config, rows: newRows})
37
+ setCurr(layout.join(''))
38
+ }
39
+
40
+ const moveRow = (dir = 'down') => {
41
+ if (rowIdx === rows.length - 1 && dir === 'down') return
42
+
43
+ let newIdx = dir === 'down' ? rowIdx + 1 : rowIdx - 1
44
+
45
+ // Swap
46
+ const temp = rows[newIdx]
47
+
48
+ rows[newIdx] = row
49
+ rows[rowIdx] = temp
50
+
51
+ rows[newIdx].uuid = Date.now();
52
+ rows[rowIdx].uuid = Date.now();
53
+
54
+ updateConfig({...config, rows})
55
+
56
+ // TODO: Migrate this animation to a React animation library once one is selected for COVE. This is pretty minor so can stay for now.
57
+ let calcRowMove = dir === 'down' ? 202 : -202
58
+ let calcRowMove2 = dir === 'down' ? -202 : 202
59
+
60
+ let rowEle = document.querySelector('[data-row-id=\'' + rowIdx + '\']')
61
+ let rowNewEle = document.querySelector('[data-row-id=\'' + newIdx + '\']')
62
+
63
+ rowEle.style.pointerEvents = 'none'
64
+ rowNewEle.style.pointerEvents = 'none'
65
+ rowEle.style.top = calcRowMove + 'px'
66
+ rowNewEle.style.top = calcRowMove2 + 'px'
67
+
68
+ setTimeout(() => {
69
+ rowEle.style.transition = 'top 500ms cubic-bezier(0.16, 1, 0.3, 1)'
70
+ rowNewEle.style.transition = 'top 500ms cubic-bezier(0.16, 1, 0.3, 1)'
71
+ rowEle.style.top = '0'
72
+ rowNewEle.style.top = '0'
73
+ }, 0)
74
+
75
+ setTimeout(() => {
76
+ rowEle.style = null
77
+ rowNewEle.style = null
78
+ }, 500)
79
+ }
80
+
81
+ const deleteRow = () => {
82
+ rows.splice(rowIdx, 1) // Just delete the row. Don't delete the instantiated widgets for now.
83
+
84
+ updateConfig({...config, rows})
85
+ }
86
+
87
+ const layoutList = [
88
+ <li className={curr === '12' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([ 12 ])} key="12" title="1 Column">
89
+ <OneColIcon />
90
+ </li>,
91
+ <li className={curr === '66' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([ 6, 6 ])} key="66" title="2 Columns">
92
+ <TwoColIcon />
93
+ </li>,
94
+ <li className={curr === '444' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([ 4, 4, 4 ])} key="444" title="3 Columns">
95
+ <ThreeColIcon />
96
+ </li>,
97
+ <li className={curr === '48' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([ 4, 8 ])} key="48" title="2 Columns">
98
+ <FourEightColIcon />
99
+ </li>,
100
+ <li className={curr === '84' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([ 8, 4 ])} key="84" title="2 Columns">
101
+ <EightFourColIcon />
102
+ </li>
103
+ ]
104
+
105
+ return (
106
+ <nav className="row-menu">
107
+ <div className="row-menu__btn">
108
+ <ul className="row-menu__flyout">
109
+ {layoutList}
110
+ </ul>
111
+ </div>
112
+ <div className="spacer"></div>
113
+ <button className={rowIdx === 0 ? 'row-menu__btn row-menu__btn-disabled' : 'row-menu__btn'} title="Move Row Up" onClick={() => moveRow('up')}>
114
+ <RowUp />
115
+ </button>
116
+ <button className={rowIdx + 1 === rows.length ? 'row-menu__btn row-menu__btn-disabled' : 'row-menu__btn'} title="Move Row Down" onClick={() => moveRow('down')}>
117
+ <RowDown />
118
+ </button>
119
+ <button className={rowIdx === 0 && rows.length === 1 ? 'row-menu__btn row-menu__btn--remove row-menu__btn-disabled' : 'row-menu__btn row-menu__btn--remove'} title="Delete Row" onClick={deleteRow}>
120
+ <CloseIcon />
121
+ </button>
122
+ </nav>
123
+ )
124
+ }
125
+
126
+ const Row = ({ row, idx: rowIdx, uuid}) => {
127
+ return (
128
+ <div className="builder-row" data-row-id={rowIdx}>
129
+ <RowMenu rowIdx={rowIdx} row={row} />
130
+ <div className="column-container">
131
+ {row.filter(column => column.width).map((column, colIdx) => <Column data={column} key={`row-${uuid}-col-${colIdx}`} rowIdx={rowIdx} colIdx={colIdx} />)}
132
+ </div>
133
+ </div>
134
+ )
135
+ }
136
+
137
+ export default Row
@@ -0,0 +1,112 @@
1
+ import React, { useContext } from 'react';
2
+ import { useDrag } from 'react-dnd';
3
+ import CloseIcon from '../images/icon-close.svg';
4
+ import GridIcon from '../images/icon-grid.svg';
5
+ import CodeIcon from '../images/icon-code.svg';
6
+ import EditIcon from '../images/icon-edit.svg';
7
+ import MoveIcon from '../images/icon-move.svg';
8
+ import BiteIcon from '@cdc/core/assets/data-bite-graphic.svg';
9
+ import BarIcon from '@cdc/core/assets/chart-bar-solid.svg';
10
+ import LineIcon from '@cdc/core/assets/chart-line-solid.svg';
11
+ import PieIcon from '@cdc/core/assets/chart-pie-solid.svg';
12
+ import UsaIcon from '@cdc/core/assets/usa-graphic.svg';
13
+ import WorldIcon from '@cdc/core/assets/world-graphic.svg';
14
+ import AlabamaIcon from '@cdc/core/assets/alabama-graphic.svg';
15
+
16
+ import Context from '../context';
17
+
18
+ const iconHash = {
19
+ 'data-bite' : <BiteIcon />,
20
+ 'Bar': <BarIcon />,
21
+ 'Spark Line': <LineIcon />,
22
+ 'waffle-chart' : <GridIcon />,
23
+ 'markup-include' : <CodeIcon />,
24
+ 'Line' : <LineIcon />,
25
+ 'Pie' : <PieIcon />,
26
+ 'us' : <UsaIcon />,
27
+ 'us-county': <UsaIcon />,
28
+ 'world' : <WorldIcon />,
29
+ 'single-state': <AlabamaIcon />
30
+ }
31
+
32
+ const labelHash = {
33
+ 'data-bite': 'Data Bite',
34
+ 'waffle-chart' : 'Waffle Chart',
35
+ 'markup-include' : 'Markup Include',
36
+ 'Bar' : 'Bar',
37
+ 'Line' : 'Line',
38
+ 'Pie' : 'Pie',
39
+ 'Spark Line' : 'Spark Line',
40
+ 'us': 'United States (State- or County-Level)',
41
+ 'us-county': 'United States (State- or County-Level)',
42
+ 'world' : 'World',
43
+ 'single-state': 'U.S. State'
44
+ }
45
+
46
+ const Widget = ({ data = {}, addVisualization, type }) => {
47
+ const { rows, visualizations, config, updateConfig } = useContext(Context)
48
+
49
+ console.log('type', type)
50
+
51
+ const handleWidgetMove = (item, monitor) => {
52
+ let result = monitor.getDropResult()
53
+
54
+ if(!result) return null
55
+
56
+ const { rowIdx, colIdx } = result
57
+
58
+ if(undefined !== data.rowIdx) {
59
+ rows[data.rowIdx][data.colIdx].widget = null // Wipe from old position
60
+
61
+ rows[rowIdx][colIdx].widget = data.uid // Add to new row and col
62
+ } else {
63
+ // Item does not exist, instantiate a new one
64
+ const newViz = addVisualization()
65
+ visualizations[newViz.uid] = newViz // Add to widgets collection
66
+ rows[rowIdx][colIdx].widget = newViz.uid // Store reference in rows collection under the specific column
67
+ }
68
+
69
+ updateConfig({...config, rows, visualizations})
70
+ }
71
+
72
+ const [ { isDragging, ...collected }, drag ] = useDrag({
73
+ type: 'vis-widget',
74
+ end: handleWidgetMove,
75
+ collect: (monitor) => ({
76
+ isDragging: monitor.isDragging()
77
+ })
78
+ })
79
+
80
+ const deleteWidget = () => {
81
+ rows[data.rowIdx][data.colIdx].widget = null
82
+
83
+ delete visualizations[data.uid]
84
+
85
+ updateConfig({...config, rows, visualizations})
86
+ }
87
+
88
+ const editWidget = () => {
89
+ visualizations[data.uid].editing = true;
90
+
91
+ updateConfig({...config, visualizations});
92
+ }
93
+
94
+ return (
95
+ <div className="widget" ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }} {...collected}>
96
+ <MoveIcon className="drag-icon" />
97
+ <div className="widget__content">
98
+ {data.rowIdx !== undefined && (
99
+ <div className="widget-menu">
100
+ <div className="widget-menu-item" onClick={editWidget}><EditIcon /></div>
101
+ <div className="widget-menu-item" onClick={deleteWidget}><CloseIcon /></div>
102
+ </div>
103
+ )}
104
+ {iconHash[type]}
105
+ <span>{labelHash[type]}</span>
106
+ {data.newViz && <span onClick={editWidget} className="config-needed">Configuration needed</span>}
107
+ </div>
108
+ </div>
109
+ )
110
+ }
111
+
112
+ export default Widget
@@ -0,0 +1,5 @@
1
+ import { createContext } from 'react';
2
+
3
+ const ConfigContext = createContext({});
4
+
5
+ export default ConfigContext;
@@ -0,0 +1,17 @@
1
+ export default {
2
+ dashboard: {
3
+ theme: 'theme-blue'
4
+ },
5
+ rows: [
6
+ [
7
+ {width: 12},
8
+ {},
9
+ {}
10
+ ]
11
+ ],
12
+ visualizations: {},
13
+ table: {
14
+ label: 'Data Table',
15
+ show: true
16
+ }
17
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
2
+ <path fill="currentColor" d="M414.8 40.79L286.8 488.8C281.9 505.8 264.2 515.6 247.2 510.8C230.2 505.9 220.4 488.2 225.2 471.2L353.2 23.21C358.1 6.216 375.8-3.624 392.8 1.232C409.8 6.087 419.6 23.8 414.8 40.79H414.8zM518.6 121.4L630.6 233.4C643.1 245.9 643.1 266.1 630.6 278.6L518.6 390.6C506.1 403.1 485.9 403.1 473.4 390.6C460.9 378.1 460.9 357.9 473.4 345.4L562.7 256L473.4 166.6C460.9 154.1 460.9 133.9 473.4 121.4C485.9 108.9 506.1 108.9 518.6 121.4V121.4zM166.6 166.6L77.25 256L166.6 345.4C179.1 357.9 179.1 378.1 166.6 390.6C154.1 403.1 133.9 403.1 121.4 390.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4L121.4 121.4C133.9 108.9 154.1 108.9 166.6 121.4C179.1 133.9 179.1 154.1 166.6 166.6V166.6z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
2
+ <path d="M2,16 L22,16 L22,14 L2,14 L2,16 Z M2,11 L22,11 L22,9 L2,9 L2,11 Z M2,4 L2,6 L22,6 L22,4 L2,4 Z"/>
3
+ </svg>