From 67a5484ca37f9cbdca807a3527dbbc8ecab65f5e Mon Sep 17 00:00:00 2001 From: Andrii Podriez Date: Thu, 25 Jul 2024 13:52:51 +0200 Subject: [PATCH] updated dashboard page Signed-off-by: Andrii Podriez --- .../dashboards/dashboard-button-group.js | 112 +++++ src/pages/dashboards/dashboard.js | 447 ++++++++++++++++++ src/pages/dashboards/dropzone.js | 77 +++ src/pages/dashboards/grid.js | 38 ++ src/pages/dashboards/widget-area.js | 76 +++ 5 files changed, 750 insertions(+) create mode 100644 src/pages/dashboards/dashboard-button-group.js create mode 100644 src/pages/dashboards/dashboard.js create mode 100644 src/pages/dashboards/dropzone.js create mode 100644 src/pages/dashboards/grid.js create mode 100644 src/pages/dashboards/widget-area.js diff --git a/src/pages/dashboards/dashboard-button-group.js b/src/pages/dashboards/dashboard-button-group.js new file mode 100644 index 0000000..71db45e --- /dev/null +++ b/src/pages/dashboards/dashboard-button-group.js @@ -0,0 +1,112 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import IconButton from '../../common/buttons/icon-button'; + +const buttonStyle = { + marginLeft: '12px', + height: '44px', + width: '35px', +}; + +const iconStyle = { + height: '25px', + width: '25px' +} + +let buttonkey = 0; + +class DashboardButtonGroup extends React.Component { + + getBtn(icon, tooltip, clickFn, locked = false) { + if (locked) { + return + } else { + return + } + } + + render() { + const buttons = []; + buttonkey = 0; + + if (this.props.editing) { + buttons.push(this.getBtn("save", "Save changes", this.props.onSave)); + buttons.push(this.getBtn("times", "Discard changes", this.props.onCancel)); + } else { + if (this.props.fullscreen !== true) { + buttons.push(this.getBtn("expand", "Change to fullscreen view", this.props.onFullscreen)); + } else { + buttons.push(this.getBtn("compress", "Back to normal view", this.props.onFullscreen)); + } + + if (this.props.paused) { + buttons.push(this.getBtn("play", "Continue simulation", this.props.onUnpause)); + } else { + buttons.push(this.getBtn("pause", "Pause simulation", this.props.onPause)); + } + + if (this.props.fullscreen !== true) { + let tooltip = this.props.locked ? "View files of scenario" : "Add, edit or delete files of scenario"; + buttons.push(this.getBtn("file", tooltip, this.props.onEditFiles)); + buttons.push(this.getBtn("sign-in-alt", "Add, edit or delete input signals", this.props.onEditInputSignals, this.props.locked)); + buttons.push(this.getBtn("sign-out-alt", "Add, edit or delete output signals", this.props.onEditOutputSignals, this.props.locked)); + buttons.push(this.getBtn("pen", "Add widgets and edit layout", this.props.onEdit, this.props.locked)); + } + } + + return
+ {buttons} +
; + } +} + +DashboardButtonGroup.propTypes = { + editing: PropTypes.bool, + fullscreen: PropTypes.bool, + paused: PropTypes.bool, + onEdit: PropTypes.func, + onSave: PropTypes.func, + onCancel: PropTypes.func, + onFullscreen: PropTypes.func, + onPause: PropTypes.func, + onUnpause: PropTypes.func +}; + +export default DashboardButtonGroup; diff --git a/src/pages/dashboards/dashboard.js b/src/pages/dashboards/dashboard.js new file mode 100644 index 0000000..971abb8 --- /dev/null +++ b/src/pages/dashboards/dashboard.js @@ -0,0 +1,447 @@ +// import React, { useState, useEffect, useCallback } from 'react'; +// import { useParams } from 'react-router-dom'; +// import Fullscreenable from 'react-fullscreenable'; +// import classNames from 'classnames'; +// import 'react-contexify/dist/ReactContexify.min.css'; +// import EditWidget from '../../widget/edit-widget/edit-widget'; +// import EditFilesDialog from '../../file/edit-files'; +// import EditSignalMappingDialog from '../scenarios/dialogs/edit-signal-mapping' +// import WidgetToolbox from '../../widget/widget-toolbox'; +// import WidgetArea from './widget-area'; +// import DashboardButtonGroup from './dashboard-button-group'; +// import IconToggleButton from '../../common/buttons/icon-toggle-button'; +// import WidgetContainer from '../../widget/widget-container'; +// import Widget from "../../widget/widget"; +// import { +// useGetDashboardQuery, +// useLazyGetWidgetsQuery, +// useLazyGetConfigsQuery, +// useAddWidgetMutation, +// useUpdateWidgetMutation, +// useDeleteWidgetMutation +// } from '../../store/apiSlice'; + +// const startUpdaterWidgets = new Set(['Slider', 'Button', 'NumberInput']); + +// const Dashboard = ({ isFullscreen, toggleFullscreen }) => { +// const params = useParams(); +// const { data: dashboardRes, error: dashboardError, isLoading: isDashboardLoading } = useGetDashboardQuery(params.dashboard); +// const dashboard = dashboardRes ? dashboardRes.dashboard : {}; + +// const [triggerGetWidgets] = useLazyGetWidgetsQuery(); +// const [triggerGetConfigs] = useLazyGetConfigsQuery(); +// const [addWidget] = useAddWidgetMutation(); +// const [updateWidget] = useUpdateWidgetMutation(); +// const [deleteWidgetMutation] = useDeleteWidgetMutation(); + +// const [widgets, setWidgets] = useState([]); +// const [configs, setConfigs] = useState([]); +// const [signals, setSignals] = useState([]); +// const [sessionToken, setSessionToken] = useState(localStorage.getItem("token")); +// const [files, setFiles] = useState([]); +// const [ics, setIcs] = useState([]); +// const [editing, setEditing] = useState(false); +// const [paused, setPaused] = useState(false); +// const [editModal, setEditModal] = useState(false); +// const [editOutputSignalsModal, setEditOutputSignalsModal] = useState(false); +// const [editInputSignalsModal, setEditInputSignalsModal] = useState(false); +// const [filesEditModal, setFilesEditModal] = useState(false); +// const [filesEditSaveState, setFilesEditSaveState] = useState([]); +// const [modalData, setModalData] = useState(null); +// const [modalIndex, setModalIndex] = useState(null); +// const [widgetChangeData, setWidgetChangeData] = useState([]); +// const [widgetOrigIDs, setWidgetOrigIDs] = useState([]); +// const [maxWidgetHeight, setMaxWidgetHeight] = useState(null); +// const [locked, setLocked] = useState(false); + +// useEffect(() => { +// if (dashboard.id) { +// fetchWidgets(dashboard.id); +// fetchConfigs(dashboard.scenarioID); +// } +// }, [dashboard]); + +// const fetchWidgets = async (dashboardID) => { +// try { +// const res = await triggerGetWidgets(dashboardID).unwrap(); +// if (res.widgets) { +// setWidgets(res.widgets); +// } +// } catch (err) { +// console.log('error', err); +// } +// }; + +// const fetchConfigs = async (scenarioID) => { +// try { +// const res = await triggerGetConfigs(scenarioID).unwrap(); +// if (res.configs) { +// setConfigs(res.configs); +// } +// } catch (err) { +// console.log('error', err); +// } +// }; + +// const handleKeydown = useCallback((e) => { +// switch (e.key) { +// case ' ': +// case 'p': +// setPaused(prevPaused => !prevPaused); +// break; +// case 'e': +// setEditing(prevEditing => !prevEditing); +// break; +// case 'f': +// toggleFullscreen(); +// break; +// default: +// } +// }, [toggleFullscreen]); + +// useEffect(() => { +// window.addEventListener('keydown', handleKeydown); +// return () => { +// window.removeEventListener('keydown', handleKeydown); +// }; +// }, [handleKeydown]); + +// const handleDrop = async (widget) => { +// widget.dashboardID = dashboard.id; + +// if (widget.type === 'ICstatus') { +// let allICids = ics.map(ic => ic.id); +// widget.customProperties.checkedIDs = allICids; +// } + +// try { +// const res = await addWidget(widget).unwrap(); +// if (res) { +// fetchWidgets(dashboard.id); +// } +// } catch (err) { +// console.log('error', err); +// } +// }; + +// const widgetChange = async (widget) => { +// setWidgetChangeData(prevWidgetChangeData => [...prevWidgetChangeData, widget]); + +// try { +// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap(); +// fetchWidgets(dashboard.id); +// } catch (err) { +// console.log('error', err); +// } +// }; + +// const onChange = async (widget) => { +// try { +// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap(); +// fetchWidgets(dashboard.id); +// } catch (err) { +// console.log('error', err); +// } +// }; + +// const onSimulationStarted = () => { +// widgets.forEach(async (widget) => { +// if (startUpdaterWidgets.has(widget.type)) { +// widget.customProperties.simStartedSendValue = true; +// try { +// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap(); +// } catch (err) { +// console.log('error', err); +// } +// } +// }); +// }; + +// const editWidget = (widget, index) => { +// setEditModal(true); +// setModalData(widget); +// setModalIndex(index); +// }; + +// const duplicateWidget = async (widget) => { +// let widgetCopy = { ...widget, id: undefined, x: widget.x + 50, y: widget.y + 50 }; +// try { +// const res = await addWidget({ widget: widgetCopy }).unwrap(); +// if (res) { +// fetchWidgets(dashboard.id); +// } +// } catch (err) { +// console.log('error', err); +// } +// }; + +// const startEditFiles = () => { +// let tempFiles = files.map(file => ({ id: file.id, name: file.name })); +// setFilesEditModal(true); +// setFilesEditSaveState(tempFiles); +// }; + +// const closeEditFiles = () => { +// widgets.forEach(widget => { +// if (widget.type === "Image") { +// widget.customProperties.update = true; +// } +// }); +// setFilesEditModal(false); +// }; + +// const closeEdit = async (data) => { +// if (!data) { +// setEditModal(false); +// setModalData(null); +// setModalIndex(null); +// return; +// } + +// if (data.type === "Image") { +// data.customProperties.update = true; +// } + +// try { +// await updateWidget({ widgetID: data.id, updatedWidget: { widget: data } }).unwrap(); +// fetchWidgets(dashboard.id); +// } catch (err) { +// console.log('error', err); +// } + +// setEditModal(false); +// setModalData(null); +// setModalIndex(null); +// }; + +// const deleteWidget = async (widgetID) => { +// try { +// await deleteWidgetMutation(widgetID).unwrap(); +// fetchWidgets(dashboard.id); +// } catch (err) { +// console.log('error', err); +// } +// }; + +// const startEditing = () => { +// let originalIDs = widgets.map(widget => widget.id); +// widgets.forEach(async (widget) => { +// if (widget.type === 'Slider' || widget.type === 'NumberInput' || widget.type === 'Button') { +// try { +// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap(); +// } catch (err) { +// console.log('error', err); +// } +// } else if (widget.type === 'Image') { +// widget.customProperties.update = true; +// } +// }); +// setEditing(true); +// setWidgetOrigIDs(originalIDs); +// }; + +// const saveEditing = () => { +// widgets.forEach(async (widget) => { +// if (widget.type === 'Image') { +// widget.customProperties.update = true; +// } +// try { +// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap(); +// } catch (err) { +// console.log('error', err); +// } +// }); +// setEditing(false); +// setWidgetChangeData([]); +// }; + +// const cancelEditing = () => { +// widgets.forEach(async (widget) => { +// if (widget.type === 'Image') { +// widget.customProperties.update = true; +// } +// if (!widgetOrigIDs.includes(widget.id)) { +// try { +// await deleteWidget(widget.id).unwrap(); +// } catch (err) { +// console.log('error', err); +// } +// } +// }); +// setEditing(false); +// setWidgetChangeData([]); +// }; + +// const setGrid = (value) => { +// setState(prevState => ({ ...prevState, dashboard: { ...dashboard, grid: value } })); +// }; + +// const setDashboardSize = (value) => { +// const maxHeight = Object.values(widgets).reduce((currentHeight, widget) => { +// const absolutHeight = widget.y + widget.height; +// return absolutHeight > currentHeight ? absolutHeight : currentHeight; +// }, 0); + +// if (value === -1) { +// if (dashboard.height >= 450 && dashboard.height >= (maxHeight + 80)) { +// setState(prevState => ({ ...prevState, dashboard: { ...dashboard, height: dashboard.height - 50 } })); +// } +// } else { +// setState(prevState => ({ ...prevState, dashboard: { ...dashboard, height: dashboard.height + 50 } })); +// } +// }; + +// const pauseData = () => setPaused(true); +// const unpauseData = () => setPaused(false); +// const editInputSignals = () => setEditInputSignalsModal(true); +// const editOutputSignals = () => setEditOutputSignalsModal(true); + +// const closeEditSignalsModal = (direction) => { +// if (direction === "in") { +// setEditInputSignalsModal(false); +// } else if (direction === "out") { +// setEditOutputSignalsModal(false); +// } +// }; + +// const buttonStyle = { marginLeft: '10px' }; +// const iconStyle = { height: '25px', width: '25px' }; +// const grid = dashboard.grid; +// const boxClasses = classNames('section', 'box', { 'fullscreen-padding': isFullscreen }); +// let dropZoneHeight = dashboard.height; + +// if (isDashboardLoading) { +// return
Loading...
; +// } + +// if (dashboardError) { +// return
Error. Dashboard not found
; +// } + +// return ( +//
+//
+//
+//

+// {dashboard.name} +// +// +// +//

+//
+ +// +//
+ +//
e.preventDefault()}> +// {editing && +// +// } + +// +// {widgets != null && Object.keys(widgets).map(widgetKey => ( +//
+// deleteWidget(widget.id)} +// onChange={editing ? widgetChange : onChange} +// > +// +// +//
+// ))} +//
+ +// + +// +//
+//
+// ); +// }; + +// export default Fullscreenable()(Dashboard); + +const Dashboard = (props) => { + return
+} + +export default Dashboard; diff --git a/src/pages/dashboards/dropzone.js b/src/pages/dashboards/dropzone.js new file mode 100644 index 0000000..530ec1e --- /dev/null +++ b/src/pages/dashboards/dropzone.js @@ -0,0 +1,77 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import { DropTarget } from 'react-dnd'; +import classNames from 'classnames'; + +const dropzoneTarget = { + drop(props, monitor, component) { + // get drop position + var position = monitor.getSourceClientOffset(); + var dropzoneRect = component.wrapper.getBoundingClientRect(); + position.x -= dropzoneRect.left; + position.y -= dropzoneRect.top; + + // Z-Index is one more the top most children + let foundZ = props.widgets.reduce( (maxZ, currentWidget) => { + if (currentWidget != null) { + // Is there a simpler way? Is not easy to expose a getter in a Container.create(Component) + if (currentWidget.z > maxZ) { + return currentWidget.z; + } + } + + return maxZ; + }, 0) + position.z = foundZ >= 100? foundZ : foundZ += 10; + if(monitor.getItem().name === "Box"){ + position.z = 0; + } + + props.onDrop(monitor.getItem(), position); + } +}; + +function collect(connect, monitor) { + + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + canDrop: monitor.canDrop() + }; +} + +class Dropzone extends React.Component { + render() { + + var toolboxClass = classNames({ + 'box-content': true, + 'toolbox-dropzone': true, + 'toolbox-dropzone-active': this.props.isOver && this.props.canDrop && this.props.editing, + 'toolbox-dropzone-editing': this.props.editing + }); + + return this.props.connectDropTarget( +
this.wrapper = wrapper}> + {this.props.children} +
+ ); + } +} + +export default DropTarget('widget', dropzoneTarget, collect)(Dropzone); diff --git a/src/pages/dashboards/grid.js b/src/pages/dashboards/grid.js new file mode 100644 index 0000000..3c3a180 --- /dev/null +++ b/src/pages/dashboards/grid.js @@ -0,0 +1,38 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; + +class Grid extends React.Component { + render() { + if (this.props.disabled) return false; + + return ( + + + + + + + + + + ); + } +} + +export default Grid; diff --git a/src/pages/dashboards/widget-area.js b/src/pages/dashboards/widget-area.js new file mode 100644 index 0000000..1e55641 --- /dev/null +++ b/src/pages/dashboards/widget-area.js @@ -0,0 +1,76 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import Dropzone from './dropzone'; +import Grid from './grid'; + +import WidgetFactory from '../../widget/widget-factory'; + +class WidgetArea extends React.Component { + snapToGrid(value) { + if (this.props.grid === 1) { + return value; + } + + return Math.round(value / this.props.grid) * this.props.grid; + } + + handleDrop = (item, position) => { + position.x = this.snapToGrid(position.x); + position.y = this.snapToGrid(position.y); + + const widget = WidgetFactory.createWidgetOfType(item.name, position); + + if (this.props.onWidgetAdded != null) { + this.props.onWidgetAdded(widget); + } + } + + render() { + + return + {this.props.children} + + + ; + } +} + +WidgetArea.propTypes = { + children: PropTypes.node, //TODO is .node correct here? Was .children before leading to compile error + editing: PropTypes.bool, + grid: PropTypes.number, + //widgets: PropTypes.array, + onWidgetAdded: PropTypes.func +}; + +WidgetArea.defaultProps = { + widgets: {} +}; + +export default WidgetArea;