From 10efea11ce592b51d554723ba4f8225a8d7c7f6e Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Wed, 23 Oct 2024 12:14:59 +0200 Subject: [PATCH 01/12] Refactor PlayerWidget into a functional component Signed-off-by: Andrii Podriez Signed-off-by: SystemsPurge --- src/pages/dashboards/widget/widget.js | 2 +- .../dashboards/widget/widgets/player.jsx | 299 ++++++++++++++++++ .../infrastructure/dialogs/new-ic-dialog.js | 8 +- src/pages/infrastructure/ic-action-board.js | 1 - src/pages/infrastructure/infrastructure.js | 20 +- .../scenarios/tables/config-action-board.js | 32 +- 6 files changed, 351 insertions(+), 11 deletions(-) create mode 100644 src/pages/dashboards/widget/widgets/player.jsx diff --git a/src/pages/dashboards/widget/widget.js b/src/pages/dashboards/widget/widget.js index 6f86f547..9f0e0da1 100644 --- a/src/pages/dashboards/widget/widget.js +++ b/src/pages/dashboards/widget/widget.js @@ -35,7 +35,7 @@ import WidgetButton from './widgets/button'; import WidgetInput from './widgets/input'; import WidgetSlider from './widgets/slider'; // import WidgetTopology from './widgets/topology'; -import WidgetPlayer from './widgets/player'; +import WidgetPlayer from './widgets/player.jsx'; //import WidgetHTML from './widgets/html'; import '../../../styles/widgets.css'; import { useGetICSQuery, useGetSignalsQuery, useGetConfigsQuery } from '../../../store/apiSlice'; diff --git a/src/pages/dashboards/widget/widgets/player.jsx b/src/pages/dashboards/widget/widgets/player.jsx new file mode 100644 index 00000000..b50b3e3b --- /dev/null +++ b/src/pages/dashboards/widget/widgets/player.jsx @@ -0,0 +1,299 @@ +/** + * 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, { useState, useEffect } from 'react'; +import { Container, Row, Col } from 'react-bootstrap'; +import JSZip from 'jszip'; +import IconButton from '../../../../common/buttons/icon-button'; +import IconTextButton from '../../../../common/buttons/icon-text-button'; +import ParametersEditor from '../../../../common/parameters-editor'; +import ResultPythonDialog from '../../../scenarios/dialogs/result-python-dialog'; +import { playerMachine } from '../widget-player/player-machine'; +import { interpret } from 'xstate'; +import { useSendActionMutation, useLazyDownloadFileQuery, useGetResultsQuery, useGetFilesQuery } from '../../../../store/apiSlice'; +import notificationsDataManager from '../../../../common/data-managers/notifications-data-manager'; +import NotificationsFactory from '../../../../common/data-managers/notifications-factory'; +import { start } from 'xstate/lib/actions'; + +const WidgetPlayer = ( + {widget, editing, configs, onStarted, ics, results, files, scenarioID}) => { + + const [sendAction] = useSendActionMutation(); + const [triggerDownloadFile] = useLazyDownloadFileQuery(); + const {refetch: refetchResults} = useGetResultsQuery(scenarioID); + const {refetch: refetchFiles} = useGetFilesQuery(scenarioID); + + + const [playerState, setPlayerState] = useState(playerMachine.initialState); + const [configID, setConfigID] = useState(-1); + const [config, setConfig] = useState({}); + const [ic, setIC] = useState(null); + const [icState, setICState] = useState("unknown"); + const [startParameters, setStartParameters] = useState({}); + const [playerIC, setPlayerIC] = useState({name: ""}); + const [showPythonModal, setShowPythonModal] = useState(false); + const [showConfig, setShowConfig] = useState(false); + const [isUploadResultsChecked, setIsUploadResultsChecked] = useState(false); + const [resultArrayId, setResultArrayId] = useState(0); + const [filesToDownload, setFilesToDownload] = useState([]); + + const [showWarning, setShowWarning] = useState(false); + const [warningText, setWarningText] = useState(""); + const [configBtnText, setConfigBtnText] = useState("Component Configuration"); + + const playerService = interpret(playerMachine); + playerService.start(); + + useEffect(() => { + if (typeof widget.customProperties.configID !== "undefined" + && configID !== widget.customProperties.configID) { + let configID = widget.customProperties.configID; + let config = configs.find(cfg => cfg.id === parseInt(configID, 10)); + if (config) { + let playeric = ics.find(ic => ic.id === parseInt(config.icID, 10)); + if (playeric) { + var afterCreateState = ''; + if (playeric.state === 'idle') { + afterCreateState = transitionState(playerState, 'ICIDLE'); + } else { + afterCreateState = transitionState(playerState, 'ICBUSY'); + } + + setPlayerIC(playeric); + setConfigID(configID); + setPlayerState(afterCreateState); + setConfig(config); + setStartParameters(config.startParameters); + } + } + } + }, [configs]); + + useEffect(() => { + if (results && results.length != resultArrayId) { + setResultArrayId(results.length - 1); + } + }, [results]); + + useEffect(() => { + if (ic && ic?.state != icState){ + var newState = ""; + switch (ic.state) { + case 'stopping': // if configured, show results + if (isUploadResultsChecked) { + refetchResults(); + refetchFiles(); + } + newState = transitionState(playerState, 'FINISH') + return { playerState: newState, icState: ic.state } + case 'idle': + newState = transitionState(playerState, 'ICIDLE') + return { playerState: newState, icState: ic.state } + default: + if (ic.state === 'running') { + onStarted() + } + newState = transitionState(playerState, 'ICBUSY') + return { playerState: newState, icState: ic.state } + } + } + }, [icState]); + + const transitionState = (currentState, playerEvent) => { + return playerMachine.transition(currentState, { type: playerEvent }) + } + + const clickStart = async () => { + const startConfig = { ...config }; + startConfig.startParameters = startParameters; + + try { + sendAction({ icid: startConfig.icID, action: "start", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); + } catch(error) { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(error?.data?.message)); + } + + setPlayerState(transitionState(playerState, 'START')); + } + + const clickReset = async () => { + try { + sendAction({ icid: ic.id, action: "reset", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); + } catch(error) { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(error?.data?.message)); + console.log(error); + } + } + + const downloadResultFiles = () => { + if (results.length <= resultArrayId) { + setShowWarning(true); + setWarningText('no new result'); + return; + } + + const result = results[resultArrayId]; + const toDownload = result.resultFileIDs; + + if(toDownload.length <= 0){ + setShowWarning(true); + setWarningText('no result files'); + } else { + toDownload.forEach(fileID => handleDownloadFile(fileID)) + } + + setFilesToDownload(toDownload); + + } + + const handleDownloadFile = async (fileID) => { + try { + const res = await triggerDownloadFile(fileID); + const file = files.find(f => f.id === fileID); + const blob = new Blob([res], { type: 'application/octet-stream' }); + zip.file(file.name, blob); + } catch (error) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(error?.data?.message)); + } + } + + const openPythonDialog = () => { + if (results.length <= resultArrayId) { + setShowWarning(true); + setWarningText('no new result'); + return; + } + + setShowPythonModal(true); + } + + const iconStyle = { + height: '20px', + width: '20px' + } + + let configButton = { + height: '70px', + width: '120px', + fontSize: '13px' + } + + return ( +
+
+ + + + + clickStart()} + icon='play' + disabled={!(playerState && playerState.matches('startable'))} + iconStyle={iconStyle} + tooltip='Start Component' + /> + + + + + clickReset()} + icon='undo' + iconStyle={iconStyle} + tooltip='Reset Component' + /> + + + + + + + + setShowConfig(prevState => (!prevState))} + icon='cogs' + text={configBtnText + ' '} + buttonStyle={configButton} + disabled={false} + tooltip='Open/Close Component Configuration' + /> + + + + + {isUploadResultsChecked ? +
+

Results

+ + + + openPythonDialog()} + icon={['fab', 'python']} + disabled={(playerState && playerState.matches('finished')) ? false : true} + iconStyle={iconStyle} + /> + + + + + downloadResultFiles()} + icon='file-download' + disabled={(playerState && playerState.matches('finished')) ? false : true} + iconStyle={iconStyle} + /> + + + +
+ : <> + } +
+ + +
+ {showConfig && config ? +
+ setStartParameters(data)} + />
+ : <> + } + {showWarning ? +

{warningText}

: <> + } + {isUploadResultsChecked ? + setShowPythonModal(false)} + /> : <> + } +
+ ); +} + +export default WidgetPlayer; diff --git a/src/pages/infrastructure/dialogs/new-ic-dialog.js b/src/pages/infrastructure/dialogs/new-ic-dialog.js index a4380e74..c12a01f1 100644 --- a/src/pages/infrastructure/dialogs/new-ic-dialog.js +++ b/src/pages/infrastructure/dialogs/new-ic-dialog.js @@ -27,7 +27,6 @@ class NewICDialog extends React.Component { constructor(props) { super(props); - this.state = { name: '', websocketurl: '', @@ -103,9 +102,9 @@ class NewICDialog extends React.Component { setManager(e) { this.setState({ [e.target.id]: e.target.value }); - if (this.props.managers) { - let schema = this.props.managers.find(m => m.uuid === e.target.value).createparameterschema + let manager = this.props.managers.find(m => m.uuid === e.target.value) + let schema = manager ? manager.createparameterschema : false if (schema) { $RefParser.dereference(schema, (err, deref) => { if (err) { @@ -116,6 +115,9 @@ class NewICDialog extends React.Component { } }) } + else{ + this.setState({schema:{}}) + } } } diff --git a/src/pages/infrastructure/ic-action-board.js b/src/pages/infrastructure/ic-action-board.js index 6e904645..1516aea6 100644 --- a/src/pages/infrastructure/ic-action-board.js +++ b/src/pages/infrastructure/ic-action-board.js @@ -90,7 +90,6 @@ const ICActionBoard = (props) => { onReset()} - onShutdown={() => onShutdown()} onDelete={() => onDelete()} onRecreate={() => onRecreate()} paused={false} diff --git a/src/pages/infrastructure/infrastructure.js b/src/pages/infrastructure/infrastructure.js index 0c221681..e992c27f 100644 --- a/src/pages/infrastructure/infrastructure.js +++ b/src/pages/infrastructure/infrastructure.js @@ -77,14 +77,30 @@ const Infrastructure = () => { newAction["action"] = "create"; newAction["parameters"] = data.parameters; newAction["when"] = new Date(); - // find the manager IC const managerIC = ics.find(ic => ic.uuid === data.manager) if (managerIC === null || managerIC === undefined) { NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Could not find manager IC with UUID " + data.manager)); return; } - + switch (managerIC.type){ + case "kubernetes","kubernetes-simple": + newAction["parameters"]["type"] = "kubernetes" + newAction["parameters"]["category"] = "simulator" + delete newAction.parameters.location + delete newAction.parameters.description + if (newAction.parameters.uuid === undefined){ + delete newAction.parameters.uuid + } + break; + case "generic": + // should check that the form contains following VALID MANDATORY fields: + // name, type , owner,realm,ws_url,api_url,category and location <= generic create action schema + break; + default: + NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Creation not supported for manager type " + managerIC.type)); + return; + } dispatch(sendActionToIC({token: sessionToken, id: managerIC.id, actions: newAction})) } } diff --git a/src/pages/scenarios/tables/config-action-board.js b/src/pages/scenarios/tables/config-action-board.js index 60379a80..854e772a 100644 --- a/src/pages/scenarios/tables/config-action-board.js +++ b/src/pages/scenarios/tables/config-action-board.js @@ -33,7 +33,7 @@ const ConfigActionBoard = ({selectedConfigs, scenarioID}) => { const [triggerGetSignals] = useLazyGetSignalsQuery(); const [sendAction] = useSendActionMutation(); - const [addResult] = useAddResultMutation(); + const [addResult, {isError: isErrorAddingResult}] = useAddResultMutation(); //we only need to update results table in case new result being added const { refetch: refetchResults } = useGetResultsQuery(scenarioID); @@ -43,8 +43,14 @@ const ConfigActionBoard = ({selectedConfigs, scenarioID}) => { const handleConfigStart = async () => { for(const config of selectedConfigs){ try { - if(isResultRequested){ + const action = { + icid: config.icID, + action: "start", + when: Math.round(new Date(time).getTime() / 1000), + parameters: {...config.startParameters} + } + if(isResultRequested){ const signalsInRes = await triggerGetSignals({configID: config.id, direction: "in"}, ).unwrap(); const signalsOutRes = await triggerGetSignals({configID: config.id, direction: "out"}, ).unwrap(); @@ -73,10 +79,28 @@ const ConfigActionBoard = ({selectedConfigs, scenarioID}) => { } } - await addResult({result: newResult}) + //get result id (if successfull) before sending an action + const res = await addResult({result: newResult}).unwrap(); + + if(!isErrorAddingResult){ + console.log("result", res) + const url = window.location.origin; + action.results = { + url: `slew.k8s.eonerc.rwth-aachen.de/results/${res.result.id}/file`, + type: "url", + token: sessionToken + } + await sendAction(action).unwrap(); + } else { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR("Error adding result")); + } + refetchResults(); + } else { + await sendAction(action).unwrap(); } - await sendAction({ icid: config.icID, action: "start", when: Math.round(new Date(time).getTime() / 1000), parameters: {} }).unwrap(); + + notificationsDataManager.addNotification(NotificationsFactory.ACTION_INFO()); } catch (err) { if(err.data){ notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(err.data.message)); From a384147e22a23467739cc583e70cbc171f05871f Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Thu, 24 Oct 2024 16:59:57 +0200 Subject: [PATCH 02/12] fix: status widget and player widget add: missing branding Signed-off-by: SystemsPurge --- .gitlab-ci.yml | 24 ++ src/branding/branding.js | 27 ++ src/branding/enershare/enershare-functions.js | 57 +++ src/branding/enershare/enershare-values.js | 45 +++ src/branding/enershare/img/logo_Enershare.svg | 33 ++ .../enershare/img/logo_Enershare_Icon.svg | 20 ++ src/branding/kopernikus/img/Logo_BMBF.jpg | Bin 0 -> 25769 bytes .../kopernikus/img/kopernikus_logo.jpg | Bin 0 -> 32825 bytes .../kopernikus/kopernikus-functions.js | 71 ++++ src/branding/kopernikus/kopernikus-values.js | 43 +++ src/common/api/websocket-api.js | 1 - src/pages/dashboards/dashboard.js | 9 +- .../widget/edit-widget/edit-widget.js | 3 - src/pages/dashboards/widget/widget.js | 14 +- .../dashboards/widget/widgets/button.jsx | 28 +- .../dashboards/widget/widgets/icstatus.jsx | 38 +- src/pages/dashboards/widget/widgets/input.jsx | 27 +- src/pages/dashboards/widget/widgets/player.js | 325 ------------------ .../dashboards/widget/widgets/player.jsx | 165 ++++++--- .../dashboards/widget/widgets/slider.jsx | 24 +- src/pages/infrastructure/ic-action-board.js | 27 +- src/pages/infrastructure/infrastructure.js | 7 +- .../scenarios/dialogs/edit-signal-mapping.js | 1 - .../scenarios/dialogs/result-python-dialog.js | 2 +- src/pages/scenarios/scenarios.js | 1 - .../scenarios/tables/config-action-board.js | 4 +- src/store/configSlice.js | 2 - src/store/icSlice.js | 3 - src/store/websocketSlice.js | 24 +- 29 files changed, 530 insertions(+), 495 deletions(-) create mode 100644 src/branding/enershare/enershare-functions.js create mode 100644 src/branding/enershare/enershare-values.js create mode 100644 src/branding/enershare/img/logo_Enershare.svg create mode 100644 src/branding/enershare/img/logo_Enershare_Icon.svg create mode 100644 src/branding/kopernikus/img/Logo_BMBF.jpg create mode 100644 src/branding/kopernikus/img/kopernikus_logo.jpg create mode 100644 src/branding/kopernikus/kopernikus-functions.js create mode 100644 src/branding/kopernikus/kopernikus-values.js delete mode 100644 src/pages/dashboards/widget/widgets/player.js diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d619f5c2..52d37378 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,6 +30,18 @@ build.opalrt: DOCKER_TAG: ${CI_COMMIT_BRANCH}-${BRANDING} extends: build +build.kopernikus: + variables: + BRANDING: kopernikus + DOCKER_TAG: ${CI_COMMIT_BRANCH}-${BRANDING} + extends: build + +build.enershare: + variables: + BRANDING: enershare + DOCKER_TAG: ${CI_COMMIT_BRANCH}-${BRANDING} + extends: build + deploy: stage: deploy variables: @@ -56,6 +68,18 @@ deploy.opalrt: BRANDING: opalrt DOCKER_TAG: ${CI_COMMIT_BRANCH}-${BRANDING} +deploy.kopernikus: + extends: deploy + variables: + BRANDING: kopernikus + DOCKER_TAG: ${CI_COMMIT_BRANCH}-${BRANDING} + +deploy.enershare: + variables: + BRANDING: enershare + DOCKER_TAG: ${CI_COMMIT_BRANCH}-${BRANDING} + extends: deploy + deploy.latest: extends: deploy variables: diff --git a/src/branding/branding.js b/src/branding/branding.js index 2d3bf535..6eea7425 100644 --- a/src/branding/branding.js +++ b/src/branding/branding.js @@ -21,12 +21,18 @@ import villasweb_values from './villasweb/villasweb-values'; import { slew_home, slew_welcome } from './slew/slew-functions'; import slew_values from './slew/slew-values'; +import { enershare_footer, enershare_home, enershare_welcome } from './enershare/enershare-functions'; +import enershare_values from './enershare/enershare-values'; + import { opalrt_footer, opalrt_home, opalrt_welcome } from './opalrt/opalrt-functions'; import opalrt_values from './opalrt/opalrt-values'; import { template_welcome, template_home, template_footer } from './template/template-functions'; import template_values from './template/template-values'; +import {kopernikus_home,kopernikus_welcome} from "./kopernikus/kopernikus-functions"; +import kopernikus_values from "./kopernikus/kopernikus-values"; + class Branding { constructor(brand) { this.brand = brand; @@ -49,9 +55,15 @@ class Branding { case 'opalrt': this.values = opalrt_values; break; + case 'enershare': + this.values = enershare_values; + break; case 'template': this.values = template_values; break; + case 'kopernikus': + this.values = kopernikus_values + break; default: console.error("Branding '" + this.brand + "' not available, will use 'villasweb' branding"); this.brand = 'villasweb'; @@ -72,9 +84,15 @@ class Branding { case 'opalrt': homepage = opalrt_home(this.getTitle(), username, userid, role); break; + case 'enershare': + homepage = enershare_home(this.getTitle(), username, userid, role); + break; case 'template': homepage = template_home(); break; + case "kopernikus": + homepage = kopernikus_home(); + break; default: homepage = villasweb_home(this.getTitle(), username, userid, role); break; @@ -91,6 +109,9 @@ class Branding { case 'opalrt': footer = opalrt_footer(); break; + case 'enershare': + footer = enershare_footer(); + break; default: footer = villasweb_footer(); break; @@ -110,9 +131,15 @@ class Branding { case 'opalrt': welcome = opalrt_welcome(); break; + case 'enershare': + welcome = enershare_welcome(); + break; case 'template': welcome = template_welcome(); break; + case "kopernikus": + welcome = kopernikus_welcome(); + break; default: welcome = this.defaultWelcome(); break; diff --git a/src/branding/enershare/enershare-functions.js b/src/branding/enershare/enershare-functions.js new file mode 100644 index 00000000..aee263d8 --- /dev/null +++ b/src/branding/enershare/enershare-functions.js @@ -0,0 +1,57 @@ +/** + * 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'; + + +export function enershare_home() { + return ( +
+ EnerShare Logo +

Home

+

+ Welcome to DT-FPEN! +

+

The DT concept for electrical networks is based on the simulation tool DPSim, and allows the interconnection + to data acquisition devices (measurement devices and status indicators) and the interaction with other systems.

+ +

It is possibile to interconnect with other systems, like the service for Data-driven management of surplus RES generation + where forecasts for the consumers in low voltage are calculated, or that can be used for predicting the consumption + of the water pumps in the circuit of the pilot. This will allow the pilot to improve their usage of the flexibility capabilities.

+
) +} + +export function enershare_welcome() { + return ( +
+

Welcome!

+

This is the Digital Twin for flexible energy networks, a system designed to facilitate the integration and flexibility on electrical networks with renewable energy sources.

+
) +} + +export function enershare_footer() { + return ( + + ); +} diff --git a/src/branding/enershare/enershare-values.js b/src/branding/enershare/enershare-values.js new file mode 100644 index 00000000..3df93bd8 --- /dev/null +++ b/src/branding/enershare/enershare-values.js @@ -0,0 +1,45 @@ +/** + * 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 . + ******************************************************************************/ + +const enershare_values = { + title: 'Digital Twin for flexible energy networks', + subtitle: '', + icon: "logo_Enershare_Icon.svg", + logo: "logo_Enershare.svg", + pages: { + home: true, + scenarios: true, + infrastructure: true, + users: true, + account: true, + api: true + }, + links: { + "AppStore": "https://store.haslab-dataspace.pt/gui/", + "The Project": "https://enershare.eu/" + }, + style: { + background: 'rgba(24,229,176, 0.6)', + highlights: 'rgba(153,102,255, 0.75)', + main: 'rgba(0,0,0, 1)', + secondaryText: 'rgba(0,0,100, 0.8)', + fontFamily: "Manrope, sans-serif", + borderRadius: "60px" + } +} + +export default enershare_values; diff --git a/src/branding/enershare/img/logo_Enershare.svg b/src/branding/enershare/img/logo_Enershare.svg new file mode 100644 index 00000000..439ca4b0 --- /dev/null +++ b/src/branding/enershare/img/logo_Enershare.svg @@ -0,0 +1,33 @@ + + \ No newline at end of file diff --git a/src/branding/enershare/img/logo_Enershare_Icon.svg b/src/branding/enershare/img/logo_Enershare_Icon.svg new file mode 100644 index 00000000..05622f43 --- /dev/null +++ b/src/branding/enershare/img/logo_Enershare_Icon.svg @@ -0,0 +1,20 @@ + + \ No newline at end of file diff --git a/src/branding/kopernikus/img/Logo_BMBF.jpg b/src/branding/kopernikus/img/Logo_BMBF.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e164f3a3aaa3c3b21c8c69a23d22b417145be30a GIT binary patch literal 25769 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 F|1a+cGUxyR literal 0 HcmV?d00001 diff --git a/src/branding/kopernikus/img/kopernikus_logo.jpg b/src/branding/kopernikus/img/kopernikus_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9739ae46e6852b22bc96fc67b09f374e4194bfa5 GIT binary patch literal 32825 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 DQyp+% literal 0 HcmV?d00001 diff --git a/src/branding/kopernikus/kopernikus-functions.js b/src/branding/kopernikus/kopernikus-functions.js new file mode 100644 index 00000000..7ae97d0c --- /dev/null +++ b/src/branding/kopernikus/kopernikus-functions.js @@ -0,0 +1,71 @@ +/** + * 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"; + +export function kopernikus_home() { + return ( +
+

Home

+

+ Welcome to the Kopernikus Projects - ENSURE! +

+

+ Rethinking the energy system for Germany and finding answers to the + questions of the energy transition: This is the great challenge that the + Kopernikus projects are facing. Find out more about the four main + pillars on these pages. The Kopernikus projects are making a significant + contribution to ensuring that Germany can achieve its climate targets by + 2045. +

+ +

Contacts

+ + +

Credits

+
+ Logo BMBF +
+
+ ); +} + +export function kopernikus_welcome() { + return ( +
+

Welcome!

+

+ SLEW is a learning platform for running experiments in a virtual power + engineering world. The platform enables to interact with the experiments + in real time and perform analyses on the experimental results. +

+
+ ); +} \ No newline at end of file diff --git a/src/branding/kopernikus/kopernikus-values.js b/src/branding/kopernikus/kopernikus-values.js new file mode 100644 index 00000000..d24dab7b --- /dev/null +++ b/src/branding/kopernikus/kopernikus-values.js @@ -0,0 +1,43 @@ +/** + * 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 . + ******************************************************************************/ + +const kopernikus_values = { + title: "Kopernikus Projekte", + subtitle: "ENSURE", + logo: "kopernikus_logo.jpg", + pages: { + home: true, + scenarios: true, + infrastructure: false, + account: false, + api: false, + }, + links: { + "DPsim Simulator": "https://dpsim.fein-aachen.org", + VILLASframework: "https://villas.fein-aachen.org/doc", + }, + style: { + background: "rgba(189, 195, 199 , 1)", + highlights: "rgba(2,36,97, 0.75)", + main: "rgba(80,80,80, 1)", + secondaryText: "rgba(80,80,80, 0.9)", + fontFamily: "Roboto, sans-serif", + borderRadius: "10px", + }, + }; + + export default kopernikus_values; \ No newline at end of file diff --git a/src/common/api/websocket-api.js b/src/common/api/websocket-api.js index 210181af..26abe767 100644 --- a/src/common/api/websocket-api.js +++ b/src/common/api/websocket-api.js @@ -58,7 +58,6 @@ class WebSocketManager { if (socket == null) { return false; } - console.log("Sending to IC", id, "message: ", message); const data = this.messageToBuffer(message); socket.socket.send(data); diff --git a/src/pages/dashboards/dashboard.js b/src/pages/dashboards/dashboard.js index 1cb7efe6..49d262cc 100644 --- a/src/pages/dashboards/dashboard.js +++ b/src/pages/dashboards/dashboard.js @@ -30,7 +30,7 @@ import IconToggleButton from '../../common/buttons/icon-toggle-button'; import WidgetContainer from "./widget/widget-container"; import Widget from "./widget/widget"; -import { connectWebSocket, disconnect } from '../../store/websocketSlice'; +import { connectWebSocket, disconnect,reportLength } from '../../store/websocketSlice'; import { useGetDashboardQuery, @@ -127,7 +127,6 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { setHeight(dashboard.height); setGrid(dashboard.grid); - console.log('widgets', widgets); } }, [dashboard]); @@ -157,6 +156,7 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { for(const config of configsRes.configs){ const signalsInRes = await triggerGetSignals({configID: config.id, direction: "in"}).unwrap(); const signalsOutRes = await triggerGetSignals({configID: config.id, direction: "out"}).unwrap(); + dispatch(reportLength(signalsInRes.signals.length)) setSignals(prevState => ([...signalsInRes.signals, ...signalsOutRes.signals, ...prevState])); } } @@ -232,9 +232,10 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { const onSimulationStarted = () => { widgets.forEach(async (widget) => { if (startUpdaterWidgets.has(widget.type)) { - widget.customProperties.simStartedSendValue = true; + let updatedWidget = structuredClone(widget) + updatedWidget.customProperties.simStartedSendValue = true; try { - await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap(); + await updateWidget({ widgetID: widget.id, updatedWidget: {widget:updatedWidget} }).unwrap(); } catch (err) { console.log('error', err); } diff --git a/src/pages/dashboards/widget/edit-widget/edit-widget.js b/src/pages/dashboards/widget/edit-widget/edit-widget.js index 5d5a0ea1..c0635821 100644 --- a/src/pages/dashboards/widget/edit-widget/edit-widget.js +++ b/src/pages/dashboards/widget/edit-widget/edit-widget.js @@ -148,9 +148,6 @@ class EditWidgetDialog extends React.Component { } else { customProperty ? changeObject[parts[0]][parts[1]] = e.target.value : changeObject[e.target.id] = e.target.value ; } - - console.log(changeObject) - this.setState({ temporal: changeObject}); } diff --git a/src/pages/dashboards/widget/widget.js b/src/pages/dashboards/widget/widget.js index 9f0e0da1..39017c01 100644 --- a/src/pages/dashboards/widget/widget.js +++ b/src/pages/dashboards/widget/widget.js @@ -38,10 +38,8 @@ import WidgetSlider from './widgets/slider'; import WidgetPlayer from './widgets/player.jsx'; //import WidgetHTML from './widgets/html'; import '../../../styles/widgets.css'; -import { useGetICSQuery, useGetSignalsQuery, useGetConfigsQuery } from '../../../store/apiSlice'; -import { sessionToken } from '../../../localStorage'; import { useUpdateWidgetMutation } from '../../../store/apiSlice'; -import { sendMessageToWebSocket } from '../../../store/websocketSlice'; +import { initValue, sendMessageToWebSocket } from '../../../store/websocketSlice'; import { useGetResultsQuery } from '../../../store/apiSlice'; const Widget = ({widget, editing, files, configs, signals, paused, ics, scenarioID, onSimulationStarted}) => { @@ -87,7 +85,6 @@ const Widget = ({widget, editing, files, configs, signals, paused, ics, scenario if (controlID !== '' && isFinalChange) { let updatedWidget = JSON.parse(JSON.stringify(widget)); updatedWidget.customProperties[controlID] = controlValue; - updateWidget(updatedWidget); } @@ -97,6 +94,10 @@ const Widget = ({widget, editing, files, configs, signals, paused, ics, scenario console.warn("Unable to send signal for signal ID", signalID, ". Signal not found."); return; } + if(!isFinalChange){ + dispatch(initValue({idx:signal[0].index,initVal:data})) + return; + } // determine ID of infrastructure component related to signal[0] // Remark: there is only one selected signal for an input type widget let icID = icIDs[signal[0].id]; @@ -143,7 +144,6 @@ const Widget = ({widget, editing, files, configs, signals, paused, ics, scenario inputDataChanged(widget, value, controlID, controlValue, isFinalChange) } signals={signals} - token={sessionToken} /> ); } else if (widget.type === 'NumberInput') { @@ -155,7 +155,6 @@ const Widget = ({widget, editing, files, configs, signals, paused, ics, scenario inputDataChanged(widget, value, controlID, controlValue, isFinalChange) } signals={signals} - token={sessionToken} /> ); } else if (widget.type === 'Slider') { @@ -167,7 +166,6 @@ const Widget = ({widget, editing, files, configs, signals, paused, ics, scenario inputDataChanged(widget, value, controlID, controlValue, isFinalChange) } signals={signals} - token={sessionToken} /> ); } else if (widget.type === 'Player') { @@ -184,7 +182,7 @@ const Widget = ({widget, editing, files, configs, signals, paused, ics, scenario /> ); } else { - console.log('Unknown widget type', widget.type); + console.log('Unknown widget type', widget); return
Error: Widget not found!
; } } diff --git a/src/pages/dashboards/widget/widgets/button.jsx b/src/pages/dashboards/widget/widgets/button.jsx index 2c285f4d..926a9276 100644 --- a/src/pages/dashboards/widget/widgets/button.jsx +++ b/src/pages/dashboards/widget/widgets/button.jsx @@ -22,33 +22,19 @@ const WidgetButton = (props) => { const [pressed, setPressed] = useState(props.widget.customProperties.pressed); useEffect(() => { - let widget = props.widget; - widget.customProperties.simStartedSendValue = false; - widget.customProperties.pressed = false; - - // AppDispatcher.dispatch({ - // type: 'widgets/start-edit', - // token: props.token, - // data: widget - // }); - - // Effect cleanup - return () => { - // Clean up if needed - }; - }, [props.token, props.widget]); + let widget = { ...props.widget }; + widget.customProperties.simStartedSendValue = false; + widget.customProperties.pressed = false; + if(props.onInputChanged && props.signals && props.signals.length > 0){ + props.onInputChanged(widget.customProperties.value, "", "", false); + } + }, [props.widget]); useEffect(() => { if (props.widget.customProperties.simStartedSendValue) { let widget = props.widget; widget.customProperties.simStartedSendValue = false; widget.customProperties.pressed = false; - AppDispatcher.dispatch({ - type: 'widgets/start-edit', - token: props.token, - data: widget - }); - props.onInputChanged(widget.customProperties.off_value, '', false, false); } }, [props, setPressed]); diff --git a/src/pages/dashboards/widget/widgets/icstatus.jsx b/src/pages/dashboards/widget/widgets/icstatus.jsx index 943c4187..94af01a6 100644 --- a/src/pages/dashboards/widget/widgets/icstatus.jsx +++ b/src/pages/dashboards/widget/widgets/icstatus.jsx @@ -18,36 +18,40 @@ import React, { useState, useEffect } from "react"; import { Badge } from "react-bootstrap"; import {stateLabelStyle} from "../../../infrastructure/styles"; +import { loadICbyId } from "../../../../store/icSlice"; +import { sessionToken } from "../../../../localStorage"; +import { useDispatch } from "react-redux"; +let timer = null const WidgetICstatus = (props) => { - const [sessionToken, setSessionToken] = useState( - localStorage.getItem("token") - ); - + const dispatch = useDispatch() + const [ics,setIcs] = useState(props.ics) + const refresh = async() => { + if (props.ics) { + let iccs = []; + for(let ic of props.ics){ + const res = await dispatch(loadICbyId({id: ic.id, token:sessionToken})); + iccs.push(res.payload) + } + setIcs(iccs) + } + }; useEffect(() => { + window.clearInterval(timer) + timer = window.setInterval(refresh,3000) // Function to refresh data - const refresh = () => { - if (props.ics) { - props.ics.forEach((ic) => { - let icID = parseInt(ic.id, 10); - }); - } - }; - - // Start timer for periodic refresh - const timer = window.setInterval(() => refresh(), 3000); - + refresh() // Cleanup function equivalent to componentWillUnmount return () => { window.clearInterval(timer); }; - }, [props.ics, sessionToken]); + }, [props.ics]); let badges = []; let checkedICs = props.widget ? props.widget.customProperties.checkedIDs : []; if (props.ics && checkedICs) { - badges = props.ics + badges = ics .filter((ic) => checkedICs.includes(ic.id)) .map((ic) => { let badgeStyle = stateLabelStyle(ic.state, ic); diff --git a/src/pages/dashboards/widget/widgets/input.jsx b/src/pages/dashboards/widget/widgets/input.jsx index 938e2d75..056ce75c 100644 --- a/src/pages/dashboards/widget/widgets/input.jsx +++ b/src/pages/dashboards/widget/widgets/input.jsx @@ -22,30 +22,13 @@ function WidgetInput(props) { const [unit, setUnit] = useState(""); useEffect(() => { - const widget = { ...props.widget }; - widget.customProperties.simStartedSendValue = false; - - // AppDispatcher.dispatch({ - // type: "widgets/start-edit", - // token: props.token, - // data: widget, - // }); - }, [props.token, props.widget]); - - useEffect(() => { - if (props.widget.customProperties.simStartedSendValue) { - const widget = { ...props.widget }; + let widget = { ...props.widget }; widget.customProperties.simStartedSendValue = false; + if(props.onInputChanged && props.signals && props.signals.length > 0){ + props.onInputChanged(widget.customProperties.value, "", "", false); + } + }, [props.widget]); - AppDispatcher.dispatch({ - type: "widgets/start-edit", - token: props.token, - data: widget, - }); - - props.onInputChanged(Number(value), "", "", false); - } - }, [props, value]); useEffect(() => { let newValue = ""; diff --git a/src/pages/dashboards/widget/widgets/player.js b/src/pages/dashboards/widget/widgets/player.js deleted file mode 100644 index 100bf1a8..00000000 --- a/src/pages/dashboards/widget/widgets/player.js +++ /dev/null @@ -1,325 +0,0 @@ -/** - * 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, { Component } from 'react'; -import { Container, Row, Col } from 'react-bootstrap'; -import JSZip from 'jszip'; -import IconButton from '../../../../common/buttons/icon-button'; -import IconTextButton from '../../../../common/buttons/icon-text-button'; -import ParametersEditor from '../../../../common/parameters-editor'; -import ResultPythonDialog from '../../../scenarios/dialogs/result-python-dialog'; -import { playerMachine } from '../widget-player/player-machine'; -import { interpret } from 'xstate'; - -const playerService = interpret(playerMachine); - -function transitionState(currentState, playerEvent) { - return playerMachine.transition(currentState, { type: playerEvent }) -} - -class WidgetPlayer extends Component { - constructor(props) { - super(props); - - playerService.start(); - - this.state = { - showDialog: false, - configID: '', - config: null, - playerState: playerMachine.initialState, - showConfig: false, - startParameters: {}, - icState: 'unknown', - ic: null, - configBtnText: 'Component Configuration', - uploadResults: false, - sessionToken: localStorage.getItem("token"), - pythonResultsModal: false, - resultArrayId: 0, - filesToDownload: [], - showWarning: true, - warningText: 'no config selected' - }; - } - - componentDidUpdate(prevProps) { - if (this.props.results && this.props.results.length - 1 !== this.state.resultArrayId) { - this.setState({ resultArrayId: this.props.results.length - 1 }); - } - // zip download files - if (this.state.filesToDownload && this.state.filesToDownload.length > 0) { - if (this.props.files !== prevProps.files) { - let filesToDownload = this.props.files.filter(file => this.state.filesToDownload.includes(file.id) && file.data); - - if (filesToDownload.length === this.state.filesToDownload.length) { // all requested files have been loaded - var zip = new JSZip(); - filesToDownload.forEach(file => { - zip.file(file.name, file.data); - }); - let zipname = "results_" + (new Date()).toISOString(); - zip.generateAsync({ type: "blob" }).then(function (content) { - saveAs(content, zipname); - }); - this.setState({ filesToDownload: [] }); - } - } - } - } - - componentWillUnmount() { - playerService.stop(); - } - - static getDerivedStateFromProps(props, state) { - - // configID was changed via edit control - if (typeof props.widget.customProperties.configID !== "undefined" - && state.configID !== props.widget.customProperties.configID) { - let configID = props.widget.customProperties.configID - let config = props.configs.find(cfg => cfg.id === parseInt(configID, 10)); - if (config) { - let playeric = props.ics.find(ic => ic.id === parseInt(config.icID, 10)) - if (playeric) { - var afterCreateState = '' - if (playeric.state === 'idle') { - afterCreateState = transitionState(state.playerState, 'ICIDLE') - } else { - afterCreateState = transitionState(state.playerState, 'ICBUSY') - } - return { - configID: configID, - config: config, - startParameters: config.startParameters, - ic: playeric, - icState: playeric.state, - configBtnText: playeric.name, - uploadResults: props.widget.customProperties.uploadResults, - playerState: afterCreateState, - showWarning: false, - warningText: '' - }; - } - } - } - - // upload results was un-/checked via edit control - if (props.widget.customProperties.uploadResults !== state.uploadResults) { - return { uploadResults: props.widget.customProperties.uploadResults } - } - - // IC state changed - if (state.ic && state.ic.state != state.icState) { - var newState; - switch (state.ic.state) { - case 'stopping': // if configured, show results - if (state.uploadResults) { - // AppDispatcher.dispatch({ - // type: 'results/start-load', - // param: '?scenarioID=' + props.scenarioID, - // token: state.sessionToken, - // }) - - // AppDispatcher.dispatch({ - // type: 'files/start-load', - // token: state.sessionToken, - // param: '?scenarioID=' + props.scenarioID, - // }); - } - newState = transitionState(state.playerState, 'FINISH') - return { playerState: newState, icState: state.ic.state } - case 'idle': - newState = transitionState(state.playerState, 'ICIDLE') - return { playerState: newState, icState: state.ic.state } - default: - if (state.ic.state === 'running') { - props.onStarted() - } - newState = transitionState(state.playerState, 'ICBUSY') - return { playerState: newState, icState: state.ic.state } - } - } - - return {}; - } - - clickStart() { - let config = this.state.config - config.startParameters = this.state.startParameters - // dispatch(sendActionToIC({token: sessionToken, id: id, actions: newAction})); - - let newState = transitionState(this.state.playerState, 'START') - this.setState({ playerState: newState }) - } - - clickReset() { - //dispatch(sendActionToIC({token: sessionToken, id: id, actions: newAction})); - } - - openPythonDialog() { - if (this.props.results.length <= this.state.resultArrayId) { - this.setState({ showWarning: true, warningText: 'no new result' }); - return - } - - this.setState({ pythonResultsModal: true }) - } - - downloadResultFiles() { - if (this.props.results.length <= this.state.resultArrayId) { - this.setState({ showWarning: true, warningText: 'no new result' }); - return - } - - let result = this.props.results[this.state.resultArrayId] - let toDownload = result.resultFileIDs; - - if (toDownload.length <= 0) { - this.setState({ showWarning: true, warningText: 'no result files' }); - return - } - - toDownload.forEach(fileid => { - // AppDispatcher.dispatch({ - // type: 'files/start-download', - // data: fileid, - // token: this.state.sessionToken - // }); - }); - - this.setState({ filesToDownload: toDownload }); - } - - render() { - - const iconStyle = { - height: '20px', - width: '20px' - } - - let configButton = { - height: '70px', - width: '120px', - fontSize: '13px' - } - - return ( -
-
- - - - - this.clickStart()} - icon='play' - disabled={(this.state.playerState && this.state.playerState.matches('startable')) ? false : true} - iconStyle={iconStyle} - tooltip='Start Component' - /> - - - - - this.clickReset()} - icon='undo' - iconStyle={iconStyle} - tooltip='Reset Component' - /> - - - - - - - - this.setState(prevState => ({ showConfig: !prevState.showConfig }))} - icon='cogs' - text={this.state.configBtnText + ' '} - buttonStyle={configButton} - disabled={false} - tooltip='Open/Close Component Configuration' - /> - - - - - {this.state.uploadResults ? -
-

Results

- - - - this.openPythonDialog()} - icon={['fab', 'python']} - disabled={(this.state.playerState && this.state.playerState.matches('finished')) ? false : true} - iconStyle={iconStyle} - /> - - - - - this.downloadResultFiles()} - icon='file-download' - disabled={(this.state.playerState && this.state.playerState.matches('finished')) ? false : true} - iconStyle={iconStyle} - /> - - - -
- : <> - } -
- - -
- {this.state.showConfig && this.state.config ? -
- this.setState({ startParameters: data })} - />
- : <> - } - {this.state.showWarning ? -

{this.state.warningText}

: <> - } - {this.state.uploadResults ? - this.setState({ pythonResultsModal: false })} - /> : <> - } -
- ); - } -} - -export default WidgetPlayer; diff --git a/src/pages/dashboards/widget/widgets/player.jsx b/src/pages/dashboards/widget/widgets/player.jsx index b50b3e3b..14e472ce 100644 --- a/src/pages/dashboards/widget/widgets/player.jsx +++ b/src/pages/dashboards/widget/widgets/player.jsx @@ -14,66 +14,82 @@ * You should have received a copy of the GNU General Public License * along with VILLASweb. If not, see . ******************************************************************************/ - -import React, { useState, useEffect } from 'react'; -import { Container, Row, Col } from 'react-bootstrap'; import JSZip from 'jszip'; +import React, { useState, useEffect } from 'react'; +import { Container, Row, Col,Form } from 'react-bootstrap'; +import {sendActionToIC,loadICbyId} from "../../../../store/icSlice"; +import { sessionToken } from '../../../../localStorage'; import IconButton from '../../../../common/buttons/icon-button'; import IconTextButton from '../../../../common/buttons/icon-text-button'; import ParametersEditor from '../../../../common/parameters-editor'; import ResultPythonDialog from '../../../scenarios/dialogs/result-python-dialog'; import { playerMachine } from '../widget-player/player-machine'; import { interpret } from 'xstate'; -import { useSendActionMutation, useLazyDownloadFileQuery, useGetResultsQuery, useGetFilesQuery } from '../../../../store/apiSlice'; +import { useAddResultMutation, useLazyDownloadFileQuery, useGetResultsQuery, useGetFilesQuery,useUpdateComponentConfigMutation } from '../../../../store/apiSlice'; import notificationsDataManager from '../../../../common/data-managers/notifications-data-manager'; import NotificationsFactory from '../../../../common/data-managers/notifications-factory'; import { start } from 'xstate/lib/actions'; +import FileSaver from "file-saver"; +import { useDispatch } from 'react-redux'; const WidgetPlayer = ( {widget, editing, configs, onStarted, ics, results, files, scenarioID}) => { - - const [sendAction] = useSendActionMutation(); + const dispatch = useDispatch() + const zip = new JSZip() const [triggerDownloadFile] = useLazyDownloadFileQuery(); const {refetch: refetchResults} = useGetResultsQuery(scenarioID); + const [updateComponentConfig] = useUpdateComponentConfigMutation(); const {refetch: refetchFiles} = useGetFilesQuery(scenarioID); - - + const [addResult, {isError: isErrorAddingResult}] = useAddResultMutation(); const [playerState, setPlayerState] = useState(playerMachine.initialState); const [configID, setConfigID] = useState(-1); const [config, setConfig] = useState({}); - const [ic, setIC] = useState(null); const [icState, setICState] = useState("unknown"); const [startParameters, setStartParameters] = useState({}); const [playerIC, setPlayerIC] = useState({name: ""}); const [showPythonModal, setShowPythonModal] = useState(false); const [showConfig, setShowConfig] = useState(false); - const [isUploadResultsChecked, setIsUploadResultsChecked] = useState(false); + const [isUploadResultsChecked, setIsUploadResultsChecked] = useState(widget.customProperties.uploadResults); const [resultArrayId, setResultArrayId] = useState(0); const [filesToDownload, setFilesToDownload] = useState([]); - const [showWarning, setShowWarning] = useState(false); const [warningText, setWarningText] = useState(""); const [configBtnText, setConfigBtnText] = useState("Component Configuration"); const playerService = interpret(playerMachine); playerService.start(); + useEffect(()=>{ + setIsUploadResultsChecked(widget.customProperties.uploadResults) + },[widget.customProperties.uploadResults]) + useEffect(()=>{ + if(playerIC.name.length !== 0){ + const refresh = async() => { + const res = await dispatch(loadICbyId({id: playerIC.id, token:sessionToken})); + setICState(res.payload.state) + } + const timer = window.setInterval(() => refresh(), 1000); + return () => { + window.clearInterval(timer); + }; + } + },[playerIC]) + useEffect(() => { if (typeof widget.customProperties.configID !== "undefined" && configID !== widget.customProperties.configID) { let configID = widget.customProperties.configID; let config = configs.find(cfg => cfg.id === parseInt(configID, 10)); if (config) { - let playeric = ics.find(ic => ic.id === parseInt(config.icID, 10)); - if (playeric) { + let t_playeric = ics.find(ic => ic.id === parseInt(config.icID, 10)); + if (t_playeric) { var afterCreateState = ''; - if (playeric.state === 'idle') { + if (t_playeric.state === 'idle') { afterCreateState = transitionState(playerState, 'ICIDLE'); } else { afterCreateState = transitionState(playerState, 'ICBUSY'); } - - setPlayerIC(playeric); + setPlayerIC(t_playeric); setConfigID(configID); setPlayerState(afterCreateState); setConfig(config); @@ -81,7 +97,7 @@ const WidgetPlayer = ( } } } - }, [configs]); + }, [configs,ics]); useEffect(() => { if (results && results.length != resultArrayId) { @@ -90,27 +106,30 @@ const WidgetPlayer = ( }, [results]); useEffect(() => { - if (ic && ic?.state != icState){ var newState = ""; - switch (ic.state) { + switch (icState) { case 'stopping': // if configured, show results if (isUploadResultsChecked) { refetchResults(); - refetchFiles(); + refetchFiles().then(v=>{ + setFilesToDownload(v.data.files) + }); } newState = transitionState(playerState, 'FINISH') - return { playerState: newState, icState: ic.state } + setPlayerState(newState); + return { playerState: newState, icState: icState } case 'idle': newState = transitionState(playerState, 'ICIDLE') - return { playerState: newState, icState: ic.state } + setPlayerState(newState); + return { playerState: newState, icState: icState } default: - if (ic.state === 'running') { + if (icState === 'running') { onStarted() } newState = transitionState(playerState, 'ICBUSY') - return { playerState: newState, icState: ic.state } + setPlayerState(newState); + return { playerState: newState, icState: icState } } - } }, [icState]); const transitionState = (currentState, playerEvent) => { @@ -118,25 +137,61 @@ const WidgetPlayer = ( } const clickStart = async () => { - const startConfig = { ...config }; - startConfig.startParameters = startParameters; - try { - sendAction({ icid: startConfig.icID, action: "start", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); + setPlayerState(transitionState(playerState, 'ICBUSY')); + let pld = {action:"start",when:Math.round((new Date()).getTime() / 1000),parameters:{...startParameters}} + if(isUploadResultsChecked){ + addResult({result: { + scenarioID: scenarioID + }}) + .then(v=>{ + pld.results = { + url: `https://slew.k8s.eonerc.rwth-aachen.de/api/v2/results/${v.data.result.id}/file`, + type: "url", + token: sessionToken + } + dispatch(sendActionToIC({token:sessionToken,id:config.icID,actions:[pld]})) + }) + .catch(e=>{ + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(e.toString())); + }) + } + else{ + dispatch(sendActionToIC({token:sessionToken,id:config.icID,actions:[pld]})) + } + //sendAction({ icid: startConfig.icID, action: "start", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); } catch(error) { notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(error?.data?.message)); } - - setPlayerState(transitionState(playerState, 'START')); } const clickReset = async () => { try { - sendAction({ icid: ic.id, action: "reset", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); + dispatch(sendActionToIC({token:sessionToken,id:config.icID,actions:[{action:"reset",when:Math.round((new Date()).getTime() / 1000)}]})) + //sendAction({ icid: ic.id, action: "reset", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); } catch(error) { notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(error?.data?.message)); console.log(error); } + + } + + const updateStartParameters = async (data)=>{ + let copy = structuredClone(config) + copy.startParameters = data + if (copy.fileIDs === null){ + copy.fileIDs = [] + } + console.log(copy) + const newConf = {id: config.id, config: {config:copy}} + updateComponentConfig(newConf) + .then(v=>{ + setStartParameters(data) + notificationsDataManager.addNotification(NotificationsFactory.ACTION_INFO()); + }) + .catch((error)=>{ + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(error?.data?.message)); + }) } const downloadResultFiles = () => { @@ -157,18 +212,27 @@ const WidgetPlayer = ( } setFilesToDownload(toDownload); - } const handleDownloadFile = async (fileID) => { - try { - const res = await triggerDownloadFile(fileID); - const file = files.find(f => f.id === fileID); - const blob = new Blob([res], { type: 'application/octet-stream' }); + triggerDownloadFile(fileID) + .then(v=>{ + const file = filesToDownload.find(f => f.id === fileID); + const blob = new Blob([v.data], { type: 'application/octet-stream' }); zip.file(file.name, blob); - } catch (error) { - notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(error?.data?.message)); - } + zip.generateAsync({ type: 'blob' }) + .then((content) => { + FileSaver.saveAs(content, `result-${file.id}.zip`); + }) + .catch((err) => { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR('Failed to create ZIP archive')); + console.error('Failed to create ZIP archive', err); + }); + }) + .catch(e=>{ + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(e)); + }) + } const openPythonDialog = () => { @@ -237,10 +301,21 @@ const WidgetPlayer = ( + + + + setIsUploadResultsChecked(prevState => !prevState)} + /> + + + {isUploadResultsChecked ?
-

Results

@@ -248,7 +323,7 @@ const WidgetPlayer = ( childKey={0} onClick={() => openPythonDialog()} icon={['fab', 'python']} - disabled={(playerState && playerState.matches('finished')) ? false : true} + disabled={(playerState && playerState.matches('finished')&& isUploadResultsChecked) ? false : true} iconStyle={iconStyle} /> @@ -259,7 +334,7 @@ const WidgetPlayer = ( childKey={1} onClick={() => downloadResultFiles()} icon='file-download' - disabled={(playerState && playerState.matches('finished')) ? false : true} + disabled={(playerState && playerState.matches('finished') ) ? false : true} iconStyle={iconStyle} /> @@ -275,8 +350,8 @@ const WidgetPlayer = ( {showConfig && config ?
setStartParameters(data)} + content={startParameters} + onChange={(data) => updateStartParameters(data)} />
: <> } diff --git a/src/pages/dashboards/widget/widgets/slider.jsx b/src/pages/dashboards/widget/widgets/slider.jsx index 7c6a262f..f1314cfa 100644 --- a/src/pages/dashboards/widget/widgets/slider.jsx +++ b/src/pages/dashboards/widget/widgets/slider.jsx @@ -28,28 +28,10 @@ const WidgetSlider = (props) => { useEffect(() => { let widget = { ...props.widget }; widget.customProperties.simStartedSendValue = false; - // AppDispatcher.dispatch({ - // type: "widgets/start-edit", - // token: props.token, - // data: widget, - // }); - }, [props.token, props.widget]); - - useEffect(() => { - // A simulation was started, make an update - if (props.widget.customProperties.simStartedSendValue) { - let widget = { ...props.widget }; - widget.customProperties.simStartedSendValue = false; - // AppDispatcher.dispatch({ - // type: "widgets/start-edit", - // token: props.token, - // data: widget, - // }); - - // Send value without changing widget + if(props.onInputChanged && props.signals && props.signals.length > 0){ props.onInputChanged(widget.customProperties.value, "", "", false); } - }, [props.token, props.widget, props.onInputChanged]); + }, [props.widget]); useEffect(() => { let newValue = ""; @@ -109,7 +91,7 @@ const WidgetSlider = (props) => { disabled={props.editing} vertical={isVertical} onChange={valueIsChanging} - onAfterChange={(v) => valueChanged(v, true)} + onChangeComplete={(v) => valueChanged(v, true)} /> ), value: ( diff --git a/src/pages/infrastructure/ic-action-board.js b/src/pages/infrastructure/ic-action-board.js index 1516aea6..446d2479 100644 --- a/src/pages/infrastructure/ic-action-board.js +++ b/src/pages/infrastructure/ic-action-board.js @@ -19,13 +19,13 @@ import { Form, Row, Col } from 'react-bootstrap'; import DateTimePicker from 'react-datetime-picker'; import ActionBoardButtonGroup from '../../common/buttons/action-board-button-group'; import classNames from 'classnames'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { sessionToken } from '../../localStorage'; -import { clearCheckedICs, deleteIC, sendActionToIC } from '../../store/icSlice'; +import { clearCheckedICs, deleteIC, loadICbyId, sendActionToIC } from '../../store/icSlice'; import { useGetICSQuery } from '../../store/apiSlice'; -const ICActionBoard = (props) => { +const ICActionBoard = ({externalICs}) => { const dispatch = useDispatch(); const {refetch: refetchICs} = useGetICSQuery(); const checkedICsIds = useSelector(state => state.infrastructure.checkedICsIds); @@ -45,14 +45,25 @@ const ICActionBoard = (props) => { }); } - const onRecreate = () => { + const onRecreate =() => { let newAction = {}; newAction['action'] = 'create'; newAction['when'] = time; - - checkedICsIds.forEach((id) => { - dispatch(sendActionToIC({token: sessionToken, id: id, actions: newAction})); - }); + for (let checkedIC of checkedICsIds){ + let IC = externalICs.find(e=>{ + return e.id == checkedIC + }) + if(IC){ + let manager = externalICs.find(e=>{ + return e.uuid == IC.manager + }) + if(manager){ + newAction['parameters'] = IC.properties ? IC.properties : IC.statusupdateraw.properties + dispatch(sendActionToIC({token:sessionToken,id:manager.id,actions:newAction})) + } + + } + } } const onDelete = () => { diff --git a/src/pages/infrastructure/infrastructure.js b/src/pages/infrastructure/infrastructure.js index e992c27f..73916c03 100644 --- a/src/pages/infrastructure/infrastructure.js +++ b/src/pages/infrastructure/infrastructure.js @@ -65,8 +65,6 @@ const Infrastructure = () => { const onNewModalClose = (data) => { setIsNewModalOpened(false); - console.log("Adding ic. External: ", !data.managedexternally) - if(data){ if(!data.managedexternally){ dispatch(addIC({token: sessionToken, ic: data})) @@ -84,7 +82,8 @@ const Infrastructure = () => { return; } switch (managerIC.type){ - case "kubernetes","kubernetes-simple": + case "kubernetes": + case "kubernetes-simple": newAction["parameters"]["type"] = "kubernetes" newAction["parameters"]["category"] = "simulator" delete newAction.parameters.location @@ -181,7 +180,7 @@ const Infrastructure = () => { category={"equipment"} /> - {currentUser.role === "Admin" ? : null} + {currentUser.role === "Admin" ? : null}
diff --git a/src/pages/scenarios/dialogs/edit-signal-mapping.js b/src/pages/scenarios/dialogs/edit-signal-mapping.js index 7ccaaee7..20b0720e 100644 --- a/src/pages/scenarios/dialogs/edit-signal-mapping.js +++ b/src/pages/scenarios/dialogs/edit-signal-mapping.js @@ -79,7 +79,6 @@ const ExportSignalMappingDialog = ({isShown, direction, onClose, configID}) => { } refetchSignals(); - console.log(signals) } const handleDelete = async (signalID) => { diff --git a/src/pages/scenarios/dialogs/result-python-dialog.js b/src/pages/scenarios/dialogs/result-python-dialog.js index 3042ffd0..f86b53a0 100644 --- a/src/pages/scenarios/dialogs/result-python-dialog.js +++ b/src/pages/scenarios/dialogs/result-python-dialog.js @@ -105,7 +105,7 @@ class ResultPythonDialog extends React.Component { code_snippets.push(code_imports) /* Result object */ - code_snippets.push(`r = Result(${result.id}, '${token}')`); + code_snippets.push(`r = Result(${result.id}, '${token}', endpoint='https://slew.k8s.eonerc.rwth-aachen.de')`); /* Examples */ code_snippets.push(`# Get result metadata diff --git a/src/pages/scenarios/scenarios.js b/src/pages/scenarios/scenarios.js index 42e96fb7..fa8b4585 100644 --- a/src/pages/scenarios/scenarios.js +++ b/src/pages/scenarios/scenarios.js @@ -85,7 +85,6 @@ const Scenarios = (props) => { } const onEditScenario = async (data) => { - console.log("data: ", {scenario: data}); if(data){ try{ await updateScenario({id: modalScenario.id, ...{scenario: data}}).unwrap(); diff --git a/src/pages/scenarios/tables/config-action-board.js b/src/pages/scenarios/tables/config-action-board.js index 854e772a..38170a24 100644 --- a/src/pages/scenarios/tables/config-action-board.js +++ b/src/pages/scenarios/tables/config-action-board.js @@ -20,7 +20,6 @@ import DateTimePicker from 'react-datetime-picker'; import ActionBoardButtonGroup from '../../../common/buttons/action-board-button-group'; import classNames from 'classnames'; import { useState } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; import { sessionToken } from '../../../localStorage'; import { useSendActionMutation, useAddResultMutation, useLazyGetSignalsQuery, useGetResultsQuery } from '../../../store/apiSlice'; import NotificationsFactory from "../../../common/data-managers/notifications-factory"; @@ -83,10 +82,9 @@ const ConfigActionBoard = ({selectedConfigs, scenarioID}) => { const res = await addResult({result: newResult}).unwrap(); if(!isErrorAddingResult){ - console.log("result", res) const url = window.location.origin; action.results = { - url: `slew.k8s.eonerc.rwth-aachen.de/results/${res.result.id}/file`, + url: `https://slew.k8s.eonerc.rwth-aachen.de/api/v2/results/${res.result.id}/file`, type: "url", token: sessionToken } diff --git a/src/store/configSlice.js b/src/store/configSlice.js index 744fcd60..9f35b19c 100644 --- a/src/store/configSlice.js +++ b/src/store/configSlice.js @@ -32,7 +32,6 @@ const configSlice = createSlice({ .addCase(loadConfig.fulfilled, (state, action) => { state.isLoading = false state.config = action.payload; - console.log("fetched config", action.payload) }) } }); @@ -43,7 +42,6 @@ export const loadConfig = createAsyncThunk( async (data) => { try { const res = await RestAPI.get("/api/v2/config", null); - console.log("response:", res) return res; } catch (error) { console.log("Error loading config", error); diff --git a/src/store/icSlice.js b/src/store/icSlice.js index 175b4852..3e34d745 100644 --- a/src/store/icSlice.js +++ b/src/store/icSlice.js @@ -88,7 +88,6 @@ const icSlice = createSlice({ .addCase(loadICbyId.fulfilled, (state, action) => { state.isCurrentICLoading = false state.currentIC = action.payload; - console.log("fetched IC", state.currentIC.name) }) .addCase(addIC.rejected, (state, action) => { NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Error while adding infrastructural component: " + action.error.message)); @@ -152,8 +151,6 @@ export const sendActionToIC = createAsyncThunk( const id = data.id; let actions = data.actions; - console.log("actions: ", actions) - if (!Array.isArray(actions)) actions = [ actions ] diff --git a/src/store/websocketSlice.js b/src/store/websocketSlice.js index 240ddcbc..d052311c 100644 --- a/src/store/websocketSlice.js +++ b/src/store/websocketSlice.js @@ -111,7 +111,8 @@ const websocketSlice = createSlice({ name: 'websocket', initialState: { icdata: {}, - activeSocketURLs: [] + activeSocketURLs: [], + values:[] }, reducers: { addActiveSocket: (state, action) => { @@ -127,8 +128,20 @@ const websocketSlice = createSlice({ values: new Array(length).fill(0) }, output: {}}; }, + reportLength:(state,action)=>{ + return { + ...state, + values:new Array(action.payload).fill(0) + } + }, + initValue:(state,action)=>{ + let {idx,initVal} = action.payload + state.values.splice(idx,1,initVal) + }, disconnect: (state, action) => { - wsManager.disconnect(action.payload.id); // Ensure the WebSocket is disconnected + if(action.payload){ + wsManager.disconnect(action.payload.id); // Ensure the WebSocket is disconnected + } }, updateIcData: (state, action) => { const { id, newIcData } = action.payload; @@ -155,13 +168,14 @@ const websocketSlice = createSlice({ sendMessageToWebSocket: (state, action) => { const { ic, signalID, signalIndex, data} = action.payload.message; const currentICdata = current(state.icdata); - + state.values[signalIndex] = data + const values = current(state.values); if (!(ic == null || currentICdata[ic].input == null)) { const inputAction = JSON.parse(JSON.stringify(currentICdata[ic].input)); // update message properties inputAction.timestamp = Date.now(); inputAction.sequence++; - inputAction.values[signalIndex] = data; + inputAction.values = values; inputAction.length = inputAction.values.length; inputAction.source_index = signalID; // The previous line sets the source_index field of the message to the ID of the signal @@ -183,5 +197,5 @@ const websocketSlice = createSlice({ }, }); -export const { disconnect, updateIcData, addActiveSocket, sendMessageToWebSocket } = websocketSlice.actions; +export const { disconnect, updateIcData, addActiveSocket, sendMessageToWebSocket,reportLength,initValue } = websocketSlice.actions; export default websocketSlice.reducer; From 9c0d5c25b0e3bbf160a6b427ea7e76110d3bdeb8 Mon Sep 17 00:00:00 2001 From: Andrii Podriez Date: Wed, 13 Nov 2024 12:34:23 +0100 Subject: [PATCH 03/12] Update modal for the ICs creation Signed-off-by: Andrii Podriez Signed-off-by: SystemsPurge --- package.json | 2 +- .../infrastructure/dialogs/new-ic-dialog.js | 744 ++++++++++-------- .../dialogs/new-ic-form-builder.js | 146 ++++ src/pages/infrastructure/infrastructure.js | 315 ++++---- src/store/icSlice.js | 404 +++++----- src/utils/timeAgo.js | 43 + src/utils/uuidv4.js | 27 + 7 files changed, 1017 insertions(+), 664 deletions(-) create mode 100644 src/pages/infrastructure/dialogs/new-ic-form-builder.js create mode 100644 src/utils/timeAgo.js create mode 100644 src/utils/uuidv4.js diff --git a/package.json b/package.json index 7429f334..641961bb 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, - "proxy": "https://villas.k8s.eonerc.rwth-aachen.de", + "proxy": "https://slew.k8s.eonerc.rwth-aachen.de/", "browserslist": { "production": [ ">0.2%", diff --git a/src/pages/infrastructure/dialogs/new-ic-dialog.js b/src/pages/infrastructure/dialogs/new-ic-dialog.js index c12a01f1..fb9a5ebd 100644 --- a/src/pages/infrastructure/dialogs/new-ic-dialog.js +++ b/src/pages/infrastructure/dialogs/new-ic-dialog.js @@ -14,343 +14,461 @@ * You should have received a copy of the GNU General Public License * along with VILLASweb. If not, see . ******************************************************************************/ - -import React from 'react'; -import { Form as BForm, OverlayTrigger, Tooltip} from 'react-bootstrap'; -import Dialog from '../../../common/dialogs/dialog'; -import ParametersEditor from '../../../common/parameters-editor'; -import Form from "@rjsf/core"; -import $RefParser from '@apidevtools/json-schema-ref-parser'; - -class NewICDialog extends React.Component { - valid = false; - - constructor(props) { - super(props); - this.state = { - name: '', - websocketurl: '', - apiurl: '', - uuid: '', - type: '', - category: '', - managedexternally: false, - description: '', - location: '', - manager: '', - properties: {}, - schema: {}, - formData: {}, - }; - } - - onClose(canceled) { - if (canceled === false) { - if (this.valid) { - const parameters = { - name: this.state.name, - type: this.state.type, - category: this.state.category, - uuid: this.state.uuid, - location: this.state.location, - description: this.state.description, - } - - const data = { - managedexternally: this.state.managedexternally, - manager: this.state.manager, - name: this.state.name, - type: this.state.type, - category: this.state.category, - uuid: this.state.uuid, - description: this.state.description, - location: this.state.location, - parameters: parameters - }; - - // Add custom properties - if (this.state.managedexternally) - Object.assign(parameters, this.state.properties); - - if (this.state.websocketurl != null && this.state.websocketurl !== "") { - parameters.websocketurl = this.state.websocketurl; - data.websocketurl = this.state.websocketurl; - } - - if (this.state.apiurl != null && this.state.apiurl !== "") { - parameters.apiurl = this.state.apiurl; - data.apiurl = this.state.apiurl; - } - - this.props.onClose(data); - this.setState({managedexternally: false}); - } +import { useEffect, useState, useRef } from "react"; +import Dialog from "../../../common/dialogs/dialog"; +import { Form, OverlayTrigger, Dropdown, Tooltip } from "react-bootstrap"; +import timeAgo from "../../../utils/timeAgo"; +import uuidv4 from "../../../utils/uuidv4"; +import FormFromParameterSchema from "./new-ic-form-builder"; + +/* + If user chooses to create new IC using manager, we a building a form from parameter schema + provided by said manager. Otherwise, use the generic form. +*/ + +const NewICDiallog = ({ show, managers, onClose }) => { + const [isManagedExternally, setIsManagedExternally] = useState(false); + const [selectedManager, setSelectedManager] = useState(null); + //form data that is used by the non-manager form + const initialFormData = { + name: "", + location: "", + description: "", + uuid: uuidv4(), + location: "", + category: "", + type: "", + websocketURL: "", + apiURL: "", + }; + const [formData, setFormData] = useState(initialFormData); + const [formErrors, setFormErrors] = useState({}); + + //each time use manager check is toggled we want to set a proper initial values + useEffect(() => { + if (isManagedExternally) { + setFormData((prevState) => ({ + category: prevState.category, + type: prevState.type, + })); + setFormErrors({}); } else { - this.props.onClose(); - this.setState({managedexternally: false}); - } - } - - handleChange(e) { - if(e.target.id === "managedexternally"){ - this.setState({ managedexternally : !this.state.managedexternally}); + setFormData(initialFormData); + setSelectedManager(null); + setFormErrors({}); } - else{ - this.setState({ [e.target.id]: e.target.value }); - } - } + }, [isManagedExternally]); + + const handleChange = ({ target }) => { + setFormData((prevState) => ({ + ...prevState, + [target.id]: target.value, + })); + //clear errors when added new input + setFormErrors((prevErrors) => ({ + ...prevErrors, + [target.id]: "", + })); + }; - setManager(e) { - this.setState({ [e.target.id]: e.target.value }); - if (this.props.managers) { - let manager = this.props.managers.find(m => m.uuid === e.target.value) - let schema = manager ? manager.createparameterschema : false - if (schema) { - $RefParser.dereference(schema, (err, deref) => { - if (err) { - console.error(err) - } - else { - this.setState({schema: schema}) - } - }) - } - else{ - this.setState({schema:{}}) + const handleClose = (c) => { + if (c) { + onClose(false); + } else { + if (validateForm()) { + const icData = { + data: { ...formData, ManagedExternally: isManagedExternally }, + manager: selectedManager, + }; + onClose(icData); } } - } - - handlePropertiesChange = properties => { - this.setState({ - properties: properties - }); }; - handleFormChange({formData}) { - this.setState({properties: formData, formData: formData}) - } - - resetState() { - this.setState({ - name: '', - websocketurl: '', - apiurl: '', - uuid: this.uuidv4(), - type: '', - category: '', - managedexternally: false, - description: '', - location: '', - properties: {}, - }); - } - - validateForm(target) { - - if (this.state.managedexternally) { - this.valid = this.state.manager !== ''; - return this.state.manager !== '' ? "success" : "error"; - } - - // check all controls - let name = true; - let uuid = true; - let type = true; - let category = true; + const onReset = () => { + setFormData(initialFormData); + setSelectedManager(null); + }; - if (this.state.name === '') { - name = false; - } + //ref for the form built by the manager's schema used to get its valdiation status + const managerFormRef = useRef(); - if (this.state.uuid === '') { - uuid = false; - } + const validateForm = () => { + //we are validating type and category in both cases, but other validations will differ for generic and manager form + const errors = {}; - if (this.state.type === '') { - type = false; + if (!formData.category.trim() || formData.category == "Select category") { + errors.category = "Category is required"; } - - if (this.state.category === '') { - category = false; + if (!formData.type.trim() || formData.category == "Select type") { + errors.type = "Type is required"; } - this.valid = name && uuid && type && category; - - // return state to control - if (target === 'name') return name ? "success" : "error"; - if (target === 'uuid') return uuid ? "success" : "error"; - if (target === 'type') return type ? "success" : "error"; - if (target === 'category') return category ? "success" : "error"; - - return this.valid; - } - - uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - // eslint-disable-next-line - var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - } + if (isManagedExternally) { + //when no parameter schema is provided, there is nothing to verify + if (selectedManager.createparameterschema == null) { + return true; + } + const isManagerFormValid = managerFormRef.current.validateForm(); + setFormErrors((prevState) => ({ ...errors, ...prevState })); + return Object.keys(errors).length === 0 && isManagerFormValid; + } else { + if (!formData.name.trim()) { + errors.name = "Name is required"; + } - render() { - let typeOptions = []; - switch(this.state.category){ - case "simulator": - typeOptions = ["dummy","generic","dpsim","rtlab","rscad","rtlab","kubernetes"]; - break; - case "manager": - typeOptions = ["villas-node","villas-relay","generic","kubernetes"]; - break; - case "gateway": - typeOptions = ["villas-node","villas-relay"]; - break; - case "service": - typeOptions = ["ems","custom"]; - break; - case "equipment": - typeOptions = ["chroma-emulator","chroma-loads","sma-sunnyboy","fleps","sonnenbatterie"]; - break; - default: - typeOptions =[]; - } + setFormErrors(errors); - let managerOptions = []; - managerOptions.push(); - for (let m of this.props.managers) { - managerOptions.push ( - - ); + return Object.keys(errors).length === 0; } + }; - return ( - this.onClose(c)} - onReset={() => this.resetState()} - valid={this.validateForm()} - > - {this.props.managers.length > 0 ? - - - An externally managed component is created and - managed by an IC manager via AMQP}> - this.handleChange(e)}> - - - - : <> - } - {this.state.managedexternally === true ? - <> - - - - Required field } > - Manager to create new IC * - - this.setManager(e)}> - {managerOptions} - - - - - {this.state.schema ? -
this.handleFormChange({formData})} - children={true} // hides submit button - /> - : - - - Create Properties - this.handlePropertiesChange(data)} + return ( + handleClose(c)} + onReset={() => onReset()} + valid={ + !Object.values(formErrors).some( + (error) => error && error.trim() !== "" + ) || + (selectedManager && selectedManager.createparameterschema == null) + } + > + + setIsManagedExternally((prevState) => !prevState)} + > + + + { + //if its managed externally, then select a manager and build a form according to the schema of the manager, + //use the generic form otherwise + isManagedExternally ? ( +
+ + + setSelectedManager(managers.find((m) => m.id == eventKey)) + } + > + + {selectedManager == null + ? "Select a manager" + : selectedManager.name + " " + selectedManager.id} + + + + {managers.map((manager) => ( + + + {manager.name + " " + manager.id} + + + {"created " + timeAgo(manager.createdAt)} + + + ))} + + + {selectedManager == null || + selectedManager?.createparameterschema == null ? ( + <> + ) : ( + <> + - - - } - - : - - - Required field } > - Name * + + + {" "} + Required field{" "} + + } + > + Category of component * + + handleChange(e)} + > + { + /* + this is done for the case, when createparameterschema, like the one from generic manager (which this check is created for), has its own type and category property + otherwise it would lead to a crash, since selects have predifined properties for the category and type, while manager's form utilizes textfileds which allows any input, which + can be different from the pre-defined options + */ + formData.category == "" || + typeOptionsMap[formData.category] ? ( + <> + + + + + + + + ) : ( + + ) + } + + + {formErrors.category} + + + + + + {" "} + Required field{" "} + + } + > + Type of component * + + handleChange(e)} + > + + { + /* + this is done for the case, when createparameterschema, like the one from generic manager (which this check is created for), has its own type and category property + otherwise it would lead to a crash, since selects have predifined properties for the category and type, while manager's form utilizes textfileds which allows any input, which + can be different from the pre-defined options + */ + formData.category == "" ? ( + <> + ) : formData.category == "" || + typeOptionsMap[formData.category] ? ( + typeOptionsMap[formData.category].map( + (name, index) => + ) + ) : ( + + ) + } + + + {formErrors.type} + + + + )} + +
+ ) : ( + //the generic form +
+ + + {" "} + Required field{" "} + + } + > + Name * - this.handleChange(e)} /> - - - - - UUID - this.handleChange(e)}/> - - - - - Location - this.handleChange(e)} /> - - - - - Description - this.handleChange(e)} /> - - - - - Required field } > - Category of component * + handleChange(e)} + /> + + + {formErrors.name} + + + + + UUID + handleChange(e)} + /> + + + + + Location + handleChange(e)} + /> + + + + + Description + handleChange(e)} + /> + + + + + + {" "} + Required field{" "} + + } + > + Category of component * - this.handleChange(e)}> + handleChange(e)} + isInvalid={!!formErrors.category} + > - - - - - Required field } > - Type of component * - - this.handleChange(e)}> + + + {formErrors.category} + + + + + + {" "} + Required field{" "} + + } + > + Type of component * + + handleChange(e)} + isInvalid={!!formErrors.type} + > - {typeOptions.map((name,index) => ( - - ))} - - - - - Websocket URL - this.handleChange(e)} /> - - - - - API URL - this.handleChange(e)} /> - - - - } -
- ); - } -} - -export default NewICDialog; + {formData.category == "" ? ( + <> + ) : ( + typeOptionsMap[formData.category].map((name, index) => ( + + )) + )} + + + {formErrors.type} + + + + + Websocket URL + handleChange(e)} + /> + + + + + API URL + handleChange(e)} + /> + + + + ) + } +
+ ); +}; + +//a map of available parameter types accoding to the picked category +const typeOptionsMap = { + simulator: [ + "dummy", + "generic", + "dpsim", + "rtlab", + "rscad", + "rtlab", + "kubernetes", + ], + manager: ["villas-node", "villas-relay", "generic", "kubernetes"], + gateway: ["villas-node", "villas-relay"], + service: ["ems", "custom"], + equipment: [ + "chroma-emulator", + "chroma-loads", + "sma-sunnyboy", + "fleps", + "sonnenbatterie", + ], +}; + +export default NewICDiallog; diff --git a/src/pages/infrastructure/dialogs/new-ic-form-builder.js b/src/pages/infrastructure/dialogs/new-ic-form-builder.js new file mode 100644 index 00000000..576a0ca6 --- /dev/null +++ b/src/pages/infrastructure/dialogs/new-ic-form-builder.js @@ -0,0 +1,146 @@ +/** + * 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, { + useEffect, + useState, + useImperativeHandle, + forwardRef, +} from "react"; +import { Form, OverlayTrigger, Dropdown, Tooltip } from "react-bootstrap"; + +//Builds a form using a schema from IC Manager +//ref is for the new ic modal to validate the whole together +const FormFromParameterSchema = forwardRef( + ({ createparameterschema, setParentFormData, setParentFormErrors }, ref) => { + if (!createparameterschema) return null; + + const [formData, setFormData] = useState({}); + const [formErrors, setFormErrors] = useState({}); + + //After initialization we want to set the initial values for the generated form + useEffect(() => { + if (createparameterschema !== null && createparameterschema.properties) { + Object.entries(createparameterschema.properties).forEach( + ([key, property]) => { + setFormData((prevState) => ({ + ...prevState, + [key]: property?.default || "", + })); + setParentFormData((prevState) => ({ + ...prevState, + [key]: property?.default || "", + })); + } + ); + } + }, [createparameterschema]); + + const handleChange = ({ target }) => { + const { id, value, type, checked } = target; + setFormData((prevState) => ({ + ...prevState, + [id]: type === "checkbox" ? checked : value, + })); + setFormErrors((prevState) => ({ + ...prevState, + [id]: "", + })); + setParentFormData((prevState) => ({ + ...prevState, + [id]: type === "checkbox" ? checked : value, + })); + setParentFormErrors((prevState) => ({ + ...prevState, + [id]: "", + })); + }; + + //schema has an array of required fields + const validateForm = () => { + const errors = {}; + createparameterschema.required?.forEach((key) => { + if (!formData[key]) { + errors[key] = "This field is required"; + } + }); + + setFormErrors(errors); + setParentFormErrors(errors); + return Object.keys(errors).length === 0; + }; + + //this is for new ic modal to call when handling submition + useImperativeHandle(ref, () => ({ + validateForm, + })); + + return ( +
+

{createparameterschema.title || ""}

+ {Object.entries(createparameterschema.properties).map( + ([key, property]) => { + const isRequired = createparameterschema.required?.includes(key); + //filter the type and category fields as they are always in the parent form + + if (key != "type" || key != "category") { + //right now only text field and checkbox are supported. Text field is default + return property.type != "boolean" ? ( + + {isRequired ? ( + Required field} + > + {property.title} * + + ) : ( + {property.title} + )} + handleChange(e)} + /> + {isRequired ? ( + + {formErrors[key]} + + ) : ( + <> + )} + + ) : ( + handleChange(e)} + > + ); + } + } + )} +
+ ); + } +); + +export default FormFromParameterSchema; diff --git a/src/pages/infrastructure/infrastructure.js b/src/pages/infrastructure/infrastructure.js index 73916c03..62080812 100644 --- a/src/pages/infrastructure/infrastructure.js +++ b/src/pages/infrastructure/infrastructure.js @@ -15,190 +15,181 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import { useEffect, useState } from "react" +import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { addIC, sendActionToIC, closeDeleteModal, closeEditModal, editIC, deleteIC } from "../../store/icSlice"; +import { + addIC, + sendActionToIC, + closeDeleteModal, + closeEditModal, + editIC, + deleteIC, +} from "../../store/icSlice"; import IconButton from "../../common/buttons/icon-button"; import ICCategoryTable from "./ic-category-table"; import ICActionBoard from "./ic-action-board"; import { buttonStyle, iconStyle } from "./styles"; -import NewICDialog from "./dialogs/new-ic-dialog"; import ImportICDialog from "./dialogs/import-ic-dialog"; import EditICDialog from "./dialogs/edit-ic-dialog"; import DeleteDialog from "../../common/dialogs/delete-dialog"; -import NotificationsDataManager from "../../common/data-managers/notifications-data-manager"; -import NotificationsFactory from "../../common/data-managers/notifications-factory"; -import {useGetICSQuery} from '../../store/apiSlice'; +import { useGetICSQuery } from "../../store/apiSlice"; +import NewICDiallog from "./dialogs/new-ic-dialog"; const Infrastructure = () => { - const dispatch = useDispatch(); + const dispatch = useDispatch(); - const { user: currentUser, token: sessionToken } = useSelector((state) => state.auth); + const { user: currentUser, token: sessionToken } = useSelector( + (state) => state.auth + ); - const {data: icsRes, isLoading, refetch: refetchICs} = useGetICSQuery(); - const ics = icsRes ? icsRes.ics : []; + const { data: icsRes, isLoading, refetch: refetchICs } = useGetICSQuery(); + const ics = icsRes ? icsRes.ics : []; - const externalICs = ics.filter(ic => ic.managedexternally === true); + const externalICs = ics.filter((ic) => ic.managedexternally === true); - //track status of the modals - const [isNewModalOpened, setIsNewModalOpened] = useState(false); - const [isImportModalOpened, setIsImportModalOpened] = useState(false); - const [isICModalOpened, setIsICModalOpened] = useState(false); - const [checkedICs, setCheckedICs] = useState([]); - - useEffect(() => { - //start a timer for periodic refresh - let timer = window.setInterval(() => refetchICs(), 10000); + //track status of the modals + const [isNewModalOpened, setIsNewModalOpened] = useState(false); + const [isImportModalOpened, setIsImportModalOpened] = useState(false); + const [isICModalOpened, setIsICModalOpened] = useState(false); + const [checkedICs, setCheckedICs] = useState([]); - return () => { - window.clearInterval(timer); - } - }, []); - - //modal actions and selectors - - const isEditModalOpened = useSelector(state => state.infrastructure.isEditModalOpened); - const isDeleteModalOpened = useSelector(state => state.infrastructure.isDeleteModalOpened); - const editModalIC = useSelector(state => state.infrastructure.editModalIC); - const deleteModalIC = useSelector(state => state.infrastructure.deleteModalIC); - - const onNewModalClose = (data) => { - setIsNewModalOpened(false); - - if(data){ - if(!data.managedexternally){ - dispatch(addIC({token: sessionToken, ic: data})) - }else { - // externally managed IC: dispatch create action to selected manager - let newAction = {}; - - newAction["action"] = "create"; - newAction["parameters"] = data.parameters; - newAction["when"] = new Date(); - // find the manager IC - const managerIC = ics.find(ic => ic.uuid === data.manager) - if (managerIC === null || managerIC === undefined) { - NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Could not find manager IC with UUID " + data.manager)); - return; - } - switch (managerIC.type){ - case "kubernetes": - case "kubernetes-simple": - newAction["parameters"]["type"] = "kubernetes" - newAction["parameters"]["category"] = "simulator" - delete newAction.parameters.location - delete newAction.parameters.description - if (newAction.parameters.uuid === undefined){ - delete newAction.parameters.uuid - } - break; - case "generic": - // should check that the form contains following VALID MANDATORY fields: - // name, type , owner,realm,ws_url,api_url,category and location <= generic create action schema - break; - default: - NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Creation not supported for manager type " + managerIC.type)); - return; - } - dispatch(sendActionToIC({token: sessionToken, id: managerIC.id, actions: newAction})) - } - } - } + useEffect(() => { + //start a timer for periodic refresh + let timer = window.setInterval(() => refetchICs(), 10000); - const onImportModalClose = (data) => { - setIsImportModalOpened(false); + return () => { + window.clearInterval(timer); + }; + }, []); - dispatch(addIC({token: sessionToken, ic: data})) - } + //modal actions and selectors + + const isEditModalOpened = useSelector( + (state) => state.infrastructure.isEditModalOpened + ); + const isDeleteModalOpened = useSelector( + (state) => state.infrastructure.isDeleteModalOpened + ); + const editModalIC = useSelector((state) => state.infrastructure.editModalIC); + const deleteModalIC = useSelector( + (state) => state.infrastructure.deleteModalIC + ); + + const onNewModalClose = (formInput) => { + setIsNewModalOpened(false); + + if (formInput) { + const { data, manager } = formInput; - const onEditModalClose = (data) => { - if(data){ - //some changes where done - dispatch(editIC({token: sessionToken, ic: data})) + let newAction = { action: "create", when: new Date() }; + + if (manager !== null) { + if (manager.createparameterschema) { + newAction.parameters = data; } - dispatch(closeEditModal(data)); + + dispatch( + sendActionToIC({ + token: sessionToken, + id: manager.id, + actions: newAction, + }) + ); + } else { + dispatch(addIC({ token: sessionToken, ic: data })); + } } - const onCloseDeleteModal = (isDeleteConfirmed) => { - if(isDeleteConfirmed){ - dispatch(deleteIC({token: sessionToken, id:deleteModalIC.id})) - } - dispatch(closeDeleteModal()); + refetchICs(); + }; + + const onImportModalClose = (data) => { + setIsImportModalOpened(false); + + dispatch(addIC({ token: sessionToken, ic: data })); + }; + + const onEditModalClose = (data) => { + if (data) { + //some changes where done + dispatch(editIC({ token: sessionToken, ic: data })); } + dispatch(closeEditModal(data)); + }; - //getting list of managers for the new IC modal - const managers = ics.filter(ic => ic.category === "manager"); - return ( -
-
-

Infrastructure - {currentUser.role === "Admin" ? - - setIsNewModalOpened(true)} - icon='plus' - buttonStyle={buttonStyle} - iconStyle={iconStyle} - /> - setIsImportModalOpened(true)} - icon='upload' - buttonStyle={buttonStyle} - iconStyle={iconStyle} - /> - - : - } -

- - - - - - - - - - - - {currentUser.role === "Admin" ? : null} - -
- - onNewModalClose(data)} managers={managers} /> - onImportModalClose(data)} /> - onEditModalClose(data)} - ic={editModalIC ? editModalIC : {}} - /> - onCloseDeleteModal(e)} - /> -
- ); -} + const onCloseDeleteModal = (isDeleteConfirmed) => { + if (isDeleteConfirmed) { + dispatch(deleteIC({ token: sessionToken, id: deleteModalIC.id })); + } + dispatch(closeDeleteModal()); + }; + + //getting list of managers for the new IC modal + const managers = ics.filter((ic) => ic.category === "manager"); + + return ( +
+
+

+ Infrastructure + {currentUser.role === "Admin" ? ( + + setIsNewModalOpened(true)} + icon="plus" + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> + setIsImportModalOpened(true)} + icon="upload" + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> + + ) : ( + + )} +

+ + + + + + + + + + + + {currentUser.role === "Admin" ? : null} +
+ onNewModalClose(data)} + /> + onImportModalClose(data)} + /> + onEditModalClose(data)} + ic={editModalIC ? editModalIC : {}} + /> + onCloseDeleteModal(e)} + /> +
+ ); +}; export default Infrastructure; diff --git a/src/store/icSlice.js b/src/store/icSlice.js index 3e34d745..defbc9c9 100644 --- a/src/store/icSlice.js +++ b/src/store/icSlice.js @@ -15,229 +15,257 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import {createSlice, createAsyncThunk} from '@reduxjs/toolkit' -import RestAPI from '../common/api/rest-api'; -import { sessionToken } from '../localStorage'; -import NotificationsDataManager from '../common/data-managers/notifications-data-manager'; -import NotificationsFactory from '../common/data-managers/notifications-factory'; - +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; +import RestAPI from "../common/api/rest-api"; +import { sessionToken } from "../localStorage"; +import NotificationsDataManager from "../common/data-managers/notifications-data-manager"; +import NotificationsFactory from "../common/data-managers/notifications-factory"; const icSlice = createSlice({ - name: 'infrastructure', - initialState: { - ICsArray: [], - - checkedICsIds: [], - isLoading: false, - currentIC: {}, - isCurrentICLoading: false, - //IC used for Edit and Delete Modals - editModalIC: null, - deleteModalIC: null, - isDeleteModalOpened: false, - isEditModalOpened: false - }, - reducers: { - updateCheckedICs: (state, args) => { - // each table has an object that maps IDs of all its ICs to boolean values - // which indicates wether or note user picked it in checbox column - const checkboxValues = args.payload; - let checkedICsIds = [...state.checkedICsIds]; - - for(const id in checkboxValues){ - if(checkedICsIds.includes(id)){ - if(!checkboxValues[id]){ - checkedICsIds = checkedICsIds.filter((checkedId) => checkedId != id); - } - } else { - if(checkboxValues[id]){ - checkedICsIds.push(id); - } - } - } - - state.checkedICsIds = checkedICsIds; - }, - clearCheckedICs: (state, args) => { - state.checkedICsIds = []; - }, - openEditModal: (state, args) => { - state.isEditModalOpened = true; - state.editModalIC = args.payload; - console.log(state.editModalIC) - }, - closeEditModal: (state, args) => { - state.isEditModalOpened = false; - state.editModalIC = null; - }, - openDeleteModal: (state, args) => { - state.deleteModalIC = args.payload; - state.isDeleteModalOpened = true; - }, - closeDeleteModal: (state, args) => { - state.deleteModalIC = null; - state.isDeleteModalOpened = false; + name: "infrastructure", + initialState: { + ICsArray: [], + + checkedICsIds: [], + isLoading: false, + currentIC: {}, + isCurrentICLoading: false, + //IC used for Edit and Delete Modals + editModalIC: null, + deleteModalIC: null, + isDeleteModalOpened: false, + isEditModalOpened: false, + }, + reducers: { + updateCheckedICs: (state, args) => { + // each table has an object that maps IDs of all its ICs to boolean values + // which indicates wether or note user picked it in checbox column + const checkboxValues = args.payload; + let checkedICsIds = [...state.checkedICsIds]; + + for (const id in checkboxValues) { + if (checkedICsIds.includes(id)) { + if (!checkboxValues[id]) { + checkedICsIds = checkedICsIds.filter( + (checkedId) => checkedId != id + ); + } + } else { + if (checkboxValues[id]) { + checkedICsIds.push(id); + } } + } + + state.checkedICsIds = checkedICsIds; }, - extraReducers: builder => { - builder - .addCase(loadICbyId.pending, (state, action) => { - state.isCurrentICLoading = true - }) - - .addCase(loadICbyId.fulfilled, (state, action) => { - state.isCurrentICLoading = false - state.currentIC = action.payload; - }) - .addCase(addIC.rejected, (state, action) => { - NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Error while adding infrastructural component: " + action.error.message)); - }) - .addCase(sendActionToIC.rejected, (state, action) => { - NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Error while sending action to infrastructural component: " + action.error.message)); - }) - .addCase(editIC.rejected, (state, action) => { - NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Error while trying to update an infrastructural component: " + action.error.message)); - }) - .addCase(deleteIC.rejected, (state, action) => { - NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Error while trying to delete an infrastructural component: " + action.error.message)); - }) - //TODO - // .addCase(restartIC.fullfilled, (state, action) => { - // console.log("restart fullfilled") - // //loadAllICs({token: sessionToken}) - // }) - // .addCase(shutdownIC.fullfilled, (state, action) => { - // console.log("shutdown fullfilled") - // //loadAllICs({token: sessionToken}) - // }) - } + clearCheckedICs: (state, args) => { + state.checkedICsIds = []; + }, + openEditModal: (state, args) => { + state.isEditModalOpened = true; + state.editModalIC = args.payload; + console.log(state.editModalIC); + }, + closeEditModal: (state, args) => { + state.isEditModalOpened = false; + state.editModalIC = null; + }, + openDeleteModal: (state, args) => { + state.deleteModalIC = args.payload; + state.isDeleteModalOpened = true; + }, + closeDeleteModal: (state, args) => { + state.deleteModalIC = null; + state.isDeleteModalOpened = false; + }, + }, + extraReducers: (builder) => { + builder + .addCase(loadICbyId.pending, (state, action) => { + state.isCurrentICLoading = true; + }) + + .addCase(loadICbyId.fulfilled, (state, action) => { + state.isCurrentICLoading = false; + state.currentIC = action.payload; + console.log("fetched IC", state.currentIC.name); + }) + .addCase(addIC.rejected, (state, action) => { + NotificationsDataManager.addNotification( + NotificationsFactory.ADD_ERROR( + "Error while adding infrastructural component: " + + action.error.message + ) + ); + }) + .addCase(sendActionToIC.rejected, (state, action) => { + NotificationsDataManager.addNotification( + NotificationsFactory.ADD_ERROR( + "Error while sending action to infrastructural component: " + + action.error.message + ) + ); + }) + .addCase(editIC.rejected, (state, action) => { + NotificationsDataManager.addNotification( + NotificationsFactory.ADD_ERROR( + "Error while trying to update an infrastructural component: " + + action.error.message + ) + ); + }) + .addCase(deleteIC.rejected, (state, action) => { + NotificationsDataManager.addNotification( + NotificationsFactory.ADD_ERROR( + "Error while trying to delete an infrastructural component: " + + action.error.message + ) + ); + }); + //TODO + // .addCase(restartIC.fullfilled, (state, action) => { + // console.log("restart fullfilled") + // //loadAllICs({token: sessionToken}) + // }) + // .addCase(shutdownIC.fullfilled, (state, action) => { + // console.log("shutdown fullfilled") + // //loadAllICs({token: sessionToken}) + // }) + }, }); //loads one IC by its id export const loadICbyId = createAsyncThunk( - 'infrastructure/loadICbyId', - async (data) => { - try { - const res = await RestAPI.get('/api/v2/ic/' + data.id, data.token); - return res.ic; - } catch (error) { - console.log("Error loading IC (id=" + data.id + ") : ", error); - } + "infrastructure/loadICbyId", + async (data) => { + try { + const res = await RestAPI.get("/api/v2/ic/" + data.id, data.token); + return res.ic; + } catch (error) { + console.log("Error loading IC (id=" + data.id + ") : ", error); } -) + } +); //adds a new Infrastructural component. Data object must contain token and ic fields export const addIC = createAsyncThunk( - 'infrastructure/addIC', - async (data, {rejectWithValue}) => { - try { - //post request body: ic object that is to be added - const ic = {ic: data.ic}; - const res = await RestAPI.post('/api/v2/ic/', ic, data.token); - return res; - } catch (error) { - console.log("Error adding IC: ", error); - return rejectWithValue(error.response.data); - } + "infrastructure/addIC", + async (data, { rejectWithValue }) => { + try { + //post request body: ic object that is to be added + const ic = { ic: data.ic }; + const res = await RestAPI.post("/api/v2/ic/", ic, data.token); + return res; + } catch (error) { + console.log("Error adding IC: ", error); + return rejectWithValue(error.response.data); } -) + } +); //sends an action to IC. Data object must contain a token, IC's id and actions string export const sendActionToIC = createAsyncThunk( - 'infrastructure/sendActionToIC', - async (data, {rejectWithValue}) => { - try { - const token = data.token; - const id = data.id; - let actions = data.actions; - - if (!Array.isArray(actions)) - actions = [ actions ] - - for (let action of actions) { - if (action.when) { - // Send timestamp as Unix Timestamp - action.when = Math.round(new Date(action.when).getTime() / 1000); - } - } - - const res = await RestAPI.post('/api/v2/ic/'+id+'/action', actions, token); - NotificationsDataManager.addNotification(NotificationsFactory.ACTION_INFO()); - return res; - } catch (error) { - console.log("Error sending an action to IC: ", error); - return rejectWithValue(error.response.data); - } + "infrastructure/sendActionToIC", + async (data, { rejectWithValue }) => { + try { + const token = data.token; + const id = data.id; + let actions = data.actions; + + if (!Array.isArray(actions)) actions = [actions]; + + for (let action of actions) { + if (action.when) { + // Send timestamp as Unix Timestamp + action.when = Math.round(new Date(action.when).getTime() / 1000); + } + } + + const res = await RestAPI.post( + "/api/v2/ic/" + id + "/action", + actions, + token + ); + NotificationsDataManager.addNotification( + NotificationsFactory.ACTION_INFO() + ); + return res; + } catch (error) { + console.log("Error sending an action to IC: ", error); + return rejectWithValue(error.response.data); } -) + } +); //send a request to update IC's data. Data object must contain token, and updated ic object export const editIC = createAsyncThunk( - 'infrastructure/editIC', - async (data, {rejectWithValue}) => { - try { - //post request body: ic object that is to be added - const {token, ic} = data; - const res = await RestAPI.put('/api/v2/ic/'+ic.id, {ic: ic}, token); - return res; - } catch (error) { - return rejectWithValue(error.response.data); - } + "infrastructure/editIC", + async (data, { rejectWithValue }) => { + try { + //post request body: ic object that is to be added + const { token, ic } = data; + const res = await RestAPI.put("/api/v2/ic/" + ic.id, { ic: ic }, token); + return res; + } catch (error) { + return rejectWithValue(error.response.data); } -) + } +); //send a request to delete IC. Data object must contain token, and id of the IC that is to be deleted export const deleteIC = createAsyncThunk( - 'infrastructure/deleteIC', - async (data, {rejectWithValue}) => { - try { - //post request body: ic object that is to be added - const {token, id} = data; - const res = await RestAPI.delete('/api/v2/ic/'+id, token); - return res; - } catch (error) { - console.log("Error updating IC: ", error); - return rejectWithValue(error.response.data); - } + "infrastructure/deleteIC", + async (data, { rejectWithValue }) => { + try { + //post request body: ic object that is to be added + const { token, id } = data; + const res = await RestAPI.delete("/api/v2/ic/" + id, token); + return res; + } catch (error) { + console.log("Error updating IC: ", error); + return rejectWithValue(error.response.data); } -) - + } +); //TODO //restarts ICs export const restartIC = createAsyncThunk( - 'infrastructure/restartIC', - async (data) => { - try { - const url = data.apiurl + '/restart' - const res = await RestAPI.post(data.apiurl, null); - console.log(res) - return res; - } catch (error) { - console.log("Error restarting IC: ", error); - } + "infrastructure/restartIC", + async (data) => { + try { + const url = data.apiurl + "/restart"; + const res = await RestAPI.post(data.apiurl, null); + return res; + } catch (error) { + console.log("Error restarting IC: ", error); } -) - + } +); //shut ICs down export const shutdownIC = createAsyncThunk( - 'infrastructure/shutdownIC', - async (data) => { - try { - const url = data.apiurl + '/shutdown' - const res = await RestAPI.post(data.apiurl, null); - console.log(res) - return res; - } catch (error) { - console.log("Error shutting IC down: ", error); - } + "infrastructure/shutdownIC", + async (data) => { + try { + const url = data.apiurl + "/shutdown"; + const res = await RestAPI.post(data.apiurl, null); + return res; + } catch (error) { + console.log("Error shutting IC down: ", error); } -) - + } +); -export const {updateCheckedICs, clearCheckedICs, openEditModal, openDeleteModal, closeDeleteModal, closeEditModal} = icSlice.actions; +export const { + updateCheckedICs, + clearCheckedICs, + openEditModal, + openDeleteModal, + closeDeleteModal, + closeEditModal, +} = icSlice.actions; export default icSlice.reducer; - diff --git a/src/utils/timeAgo.js b/src/utils/timeAgo.js new file mode 100644 index 00000000..1bc5599b --- /dev/null +++ b/src/utils/timeAgo.js @@ -0,0 +1,43 @@ +/** + * 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 . + ******************************************************************************/ + +//returns a text that says how many seconds, minutes, .. , years has passed since the given date +const timeAgo = (date) => { + const now = new Date(); + const past = new Date(date); + const diffSec = Math.floor((now - past) / 1000); + + const intervals = [ + { label: "year", seconds: 31536000 }, + { label: "month", seconds: 2592000 }, + { label: "day", seconds: 86400 }, + { label: "hour", seconds: 3600 }, + { label: "minute", seconds: 60 }, + { label: "second", seconds: 1 }, + ]; + + for (const interval of intervals) { + const count = Math.floor(diffSec / interval.seconds); + if (count >= 1) { + return `${count} ${interval.label}${count !== 1 ? "s" : ""} ago`; + } + } + + return "just now"; +}; + +export default timeAgo; diff --git a/src/utils/uuidv4.js b/src/utils/uuidv4.js new file mode 100644 index 00000000..ffe1a1d7 --- /dev/null +++ b/src/utils/uuidv4.js @@ -0,0 +1,27 @@ +/** + * 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 . + ******************************************************************************/ + +const uuidv4 = () => { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + // eslint-disable-next-line + var r = (Math.random() * 16) | 0, + v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +}; + +export default uuidv4; From 16de70aa436fffc136c54dc1e851f4cbd075a529 Mon Sep 17 00:00:00 2001 From: Andrii Podriez Date: Tue, 3 Dec 2024 11:11:11 +0100 Subject: [PATCH 04/12] Add error boundary to the dashboard and start refactoring Signed-off-by: Andrii Podriez Signed-off-by: SystemsPurge --- src/branding/branding.js | 145 ++-- .../dashboards/dashboard-button-group.js | 112 ---- .../dashboards/dashboard-error-boundry.js | 30 + src/pages/dashboards/dashboard-layout.js | 454 +++++++++++++ src/pages/dashboards/dashboard-old.js | 618 ++++++++++++++++++ src/pages/dashboards/dashboard.js | 551 +--------------- .../dashboards/grid/dashboard-button-group.js | 154 +++++ src/pages/dashboards/{ => grid}/dropzone.js | 0 src/pages/dashboards/{ => grid}/grid.js | 0 .../dashboards/{ => grid}/widget-area.js | 43 +- src/pages/dashboards/widget/widget-old.js | 290 ++++++++ src/pages/dashboards/widget/widget.js | 176 +---- .../dashboards/widget/widgets/button.jsx | 120 ++-- .../scenarios/dialogs/edit-signal-mapping.js | 4 +- src/store/apiSlice.js | 78 ++- src/store/dashboardSlice.js | 101 +++ 16 files changed, 1900 insertions(+), 976 deletions(-) delete mode 100644 src/pages/dashboards/dashboard-button-group.js create mode 100644 src/pages/dashboards/dashboard-error-boundry.js create mode 100644 src/pages/dashboards/dashboard-layout.js create mode 100644 src/pages/dashboards/dashboard-old.js create mode 100644 src/pages/dashboards/grid/dashboard-button-group.js rename src/pages/dashboards/{ => grid}/dropzone.js (100%) rename src/pages/dashboards/{ => grid}/grid.js (100%) rename src/pages/dashboards/{ => grid}/widget-area.js (71%) create mode 100644 src/pages/dashboards/widget/widget-old.js create mode 100644 src/store/dashboardSlice.js diff --git a/src/branding/branding.js b/src/branding/branding.js index 6eea7425..c7a61382 100644 --- a/src/branding/branding.js +++ b/src/branding/branding.js @@ -15,20 +15,29 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import { villasweb_footer, villasweb_home, villasweb_welcome } from './villasweb/villasweb-functions'; -import villasweb_values from './villasweb/villasweb-values'; +import { + villasweb_footer, + villasweb_home, + villasweb_welcome, +} from "./villasweb/villasweb-functions"; +import villasweb_values from "./villasweb/villasweb-values"; -import { slew_home, slew_welcome } from './slew/slew-functions'; -import slew_values from './slew/slew-values'; +import { slew_home, slew_welcome } from "./slew/slew-functions"; +import slew_values from "./slew/slew-values"; -import { enershare_footer, enershare_home, enershare_welcome } from './enershare/enershare-functions'; -import enershare_values from './enershare/enershare-values'; +import { + opalrt_footer, + opalrt_home, + opalrt_welcome, +} from "./opalrt/opalrt-functions"; +import opalrt_values from "./opalrt/opalrt-values"; -import { opalrt_footer, opalrt_home, opalrt_welcome } from './opalrt/opalrt-functions'; -import opalrt_values from './opalrt/opalrt-values'; - -import { template_welcome, template_home, template_footer } from './template/template-functions'; -import template_values from './template/template-values'; +import { + template_welcome, + template_home, + template_footer, +} from "./template/template-functions"; +import template_values from "./template/template-values"; import {kopernikus_home,kopernikus_welcome} from "./kopernikus/kopernikus-functions"; import kopernikus_values from "./kopernikus/kopernikus-values"; @@ -46,48 +55,46 @@ class Branding { setValues() { switch (this.brand) { - case 'villasweb': + case "villasweb": this.values = villasweb_values; break; - case 'slew': + case "slew": this.values = slew_values; break; - case 'opalrt': + case "opalrt": this.values = opalrt_values; break; - case 'enershare': - this.values = enershare_values; - break; - case 'template': + case "template": this.values = template_values; break; case 'kopernikus': this.values = kopernikus_values break; default: - console.error("Branding '" + this.brand + "' not available, will use 'villasweb' branding"); - this.brand = 'villasweb'; + console.error( + "Branding '" + + this.brand + + "' not available, will use 'villasweb' branding" + ); + this.brand = "villasweb"; this.values = villasweb_values; break; } } - getHome(username = '', userid = '', role = '') { - var homepage = ''; + getHome(username = "", userid = "", role = "") { + var homepage = ""; switch (this.brand) { - case 'villasweb': + case "villasweb": homepage = villasweb_home(this.getTitle(), username, userid, role); break; - case 'slew': + case "slew": homepage = slew_home(); break; - case 'opalrt': + case "opalrt": homepage = opalrt_home(this.getTitle(), username, userid, role); break; - case 'enershare': - homepage = enershare_home(this.getTitle(), username, userid, role); - break; - case 'template': + case "template": homepage = template_home(); break; case "kopernikus": @@ -101,12 +108,12 @@ class Branding { } getFooter() { - var footer = ''; + var footer = ""; switch (this.brand) { - case 'template': + case "template": footer = template_footer(); break; - case 'opalrt': + case "opalrt": footer = opalrt_footer(); break; case 'enershare': @@ -120,21 +127,18 @@ class Branding { } getWelcome() { - var welcome = ''; + var welcome = ""; switch (this.brand) { - case 'villasweb': + case "villasweb": welcome = villasweb_welcome(); break; - case 'slew': + case "slew": welcome = slew_welcome(); break; - case 'opalrt': + case "opalrt": welcome = opalrt_welcome(); break; - case 'enershare': - welcome = enershare_welcome(); - break; - case 'template': + case "template": welcome = template_welcome(); break; case "kopernikus": @@ -148,7 +152,12 @@ class Branding { } defaultWelcome() { - return (

Welcome!

This is the welcome page and you are very welcome here.

); + return ( +
+

Welcome!

+

This is the welcome page and you are very welcome here.

+
+ ); } // if icon cannot be found, the default favicon will be used @@ -164,12 +173,12 @@ class Branding { if (!this.values.icon) { return; } - var oldlink = document.getElementById('dynamic-favicon'); + var oldlink = document.getElementById("dynamic-favicon"); - var link = document.createElement('link'); - link.id = 'dynamic-favicon'; - link.rel = 'shortcut icon' - link.href = '/' + this.values.icon; + var link = document.createElement("link"); + link.id = "dynamic-favicon"; + link.rel = "shortcut icon"; + link.href = "/" + this.values.icon; if (oldlink) { document.head.removeChild(oldlink); @@ -178,7 +187,7 @@ class Branding { } checkValues() { - if (!this.values.hasOwnProperty('pages')) { + if (!this.values.hasOwnProperty("pages")) { let pages = {}; pages.home = true; pages.scenarios = true; @@ -189,23 +198,23 @@ class Branding { this.values.pages = pages; } else { - if (!this.values.pages.hasOwnProperty('home')) { - this.values.pages['home'] = false; + if (!this.values.pages.hasOwnProperty("home")) { + this.values.pages["home"] = false; } - if (!this.values.pages.hasOwnProperty('scenarios')) { - this.values.pages['scenarios'] = false; + if (!this.values.pages.hasOwnProperty("scenarios")) { + this.values.pages["scenarios"] = false; } - if (!this.values.pages.hasOwnProperty('infrastructure')) { - this.values.pages['infrastructure'] = false; + if (!this.values.pages.hasOwnProperty("infrastructure")) { + this.values.pages["infrastructure"] = false; } - if (!this.values.pages.hasOwnProperty('users')) { - this.values.pages['users'] = false; + if (!this.values.pages.hasOwnProperty("users")) { + this.values.pages["users"] = false; } - if (!this.values.pages.hasOwnProperty('account')) { - this.values.pages['account'] = false; + if (!this.values.pages.hasOwnProperty("account")) { + this.values.pages["account"] = false; } - if (!this.values.pages.hasOwnProperty('api')) { - this.values.pages['api'] = false; + if (!this.values.pages.hasOwnProperty("api")) { + this.values.pages["api"] = false; } } } @@ -213,10 +222,10 @@ class Branding { applyStyle() { this.changeHead(); - const rootEl = document.querySelector(':root'); + const rootEl = document.querySelector(":root"); for (const [key, value] of Object.entries(this.values.style)) { - rootEl.style.setProperty('--' + key, value); + rootEl.style.setProperty("--" + key, value); } } @@ -224,9 +233,17 @@ class Branding { let image = null; try { - image = {'Logo + image = ( + {"Logo + ); } catch (err) { - console.error("cannot find './" + this.brand + '/img/' + this.values.logo + "'"); + console.error( + "cannot find './" + this.brand + "/img/" + this.values.logo + "'" + ); } return image; @@ -239,7 +256,7 @@ class Branding { getSubtitle() { return this.values.subtitle ? this.values.subtitle : null; } -}; +} var branding = new Branding(process.env.REACT_APP_BRAND); diff --git a/src/pages/dashboards/dashboard-button-group.js b/src/pages/dashboards/dashboard-button-group.js deleted file mode 100644 index 04a1500d..00000000 --- a/src/pages/dashboards/dashboard-button-group.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * 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-error-boundry.js b/src/pages/dashboards/dashboard-error-boundry.js new file mode 100644 index 00000000..42e881ac --- /dev/null +++ b/src/pages/dashboards/dashboard-error-boundry.js @@ -0,0 +1,30 @@ +import React from "react"; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + console.error("Error caught by Error Boundary:", error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( + <> +

Something went wrong!

+ + ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/src/pages/dashboards/dashboard-layout.js b/src/pages/dashboards/dashboard-layout.js new file mode 100644 index 00000000..69e5a2c5 --- /dev/null +++ b/src/pages/dashboards/dashboard-layout.js @@ -0,0 +1,454 @@ +/** + * 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 { useCallback, useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + useGetDashboardQuery, + useGetICSQuery, + useGetWidgetsQuery, + useAddWidgetMutation, + useLazyGetSignalsQuery, + useLazyGetConfigsQuery, + useUpdateWidgetMutation, +} from "../../store/apiSlice"; +import { sessionToken } from "../../localStorage"; +import Fullscreenable from "react-fullscreenable"; +import DashboardButtonGroup from "./grid/dashboard-button-group"; +import Widget from "./widget/widget"; +import WidgetContainer from "./widget/widget-container"; +import WidgetArea from "./grid/widget-area"; +import WidgetToolbox from "./widget/widget-toolbox"; +import IconToggleButton from "../../common/buttons/icon-toggle-button"; +import EditWidgetDialog from "./widget/edit-widget/edit-widget"; +import EditSignalMappingDialog from "../scenarios/dialogs/edit-signal-mapping"; +import classNames from "classnames"; +import { Spinner } from "react-bootstrap"; + +//this component handles the UI of the dashboard +const DashboardLayout = ({ isFullscreen, toggleFullscreen }) => { + const params = useParams(); + const { + data: { dashboard } = {}, + isFetching: isFetchingDashboard, + refetch: refetchDashboard, + } = useGetDashboardQuery(params.dashboard); + const { + data: { widgets } = [], + isFetching: isFetchingWidgets, + refetch: refetchWidgets, + } = useGetWidgetsQuery(params.dashboard); + + const [updateWidget] = useUpdateWidgetMutation(); + + const [triggerGetSignals] = useLazyGetSignalsQuery(); + const [triggerGetConfigs] = useLazyGetConfigsQuery(); + + const [signals, setSignals] = useState([]); + const [configs, setConfigs] = useState([]); + + const [addWidget] = useAddWidgetMutation(); + + const { data: ics } = useGetICSQuery(); + + const [editing, setEditing] = useState(false); + const [paused, setPaused] = useState(false); + const [locked, setLocked] = useState(false); + const [editModal, setEditModal] = useState(false); + const [editModalData, setEditModalData] = useState(null); + + const [editInputSignalsModal, setEditingInputSignalsModal] = useState(false); + + //these are used to handle widget updates separately from RTK Query + const [localWidgets, setLocalWidgets] = useState([]); + const [widgetsToUpdate, setWidgetsToUpdate] = useState([]); + + useEffect(() => { + if (widgets?.length > 0) { + setLocalWidgets(widgets); + } + }, [widgets]); + + const fetchWidgetData = async (scenarioID) => { + try { + const configsRes = await triggerGetConfigs(scenarioID).unwrap(); + if (configsRes.configs) { + setConfigs(configsRes.configs); + //load signals if there are any configs + + if (configsRes.configs.length > 0) { + for (const config of configsRes.configs) { + const signalsInRes = await triggerGetSignals({ + configID: config.id, + direction: "in", + }).unwrap(); + const signalsOutRes = await triggerGetSignals({ + configID: config.id, + direction: "out", + }).unwrap(); + setSignals((prevState) => [ + ...signalsInRes.signals, + ...signalsOutRes.signals, + ...prevState, + ]); + } + } + } + } catch (err) { + console.log("error fetching data", err); + } + }; + + const [gridParameters, setGridParameters] = useState({ + height: 10, + grid: 50, + }); + + useEffect(() => { + if (!isFetchingDashboard) { + setGridParameters({ height: dashboard.height, grid: dashboard.grid }); + fetchWidgetData(dashboard.scenarioID); + } + }, [isFetchingDashboard]); + + const boxClasses = classNames("section", "box", { + "fullscreen-padding": isFullscreen, + }); + + const handleKeydown = useCallback( + (e) => { + const keyMap = { + " ": () => setPaused((prevPaused) => !prevPaused), + p: () => setPaused((prevPaused) => !prevPaused), + e: () => setEditing((prevEditing) => !prevEditing), + f: toggleFullscreen, + }; + if (keyMap[e.key]) { + keyMap[e.key](); + } + }, + [toggleFullscreen] + ); + + useEffect(() => { + window.addEventListener("keydown", handleKeydown); + return () => { + window.removeEventListener("keydown", handleKeydown); + }; + }, [handleKeydown]); + + 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); + } + } + }); + setEditing(true); + }; + + const handleSaveEdit = async () => { + if ( + gridParameters.height !== dashboard?.height || + gridParameters.grid !== dashboard?.grid + ) { + try { + const { height: oldHeight, grid: oldGrid, ...rest } = dashboard; + await updateDashboard({ + dashboardID: dashboard?.id, + dashboard: { + height: gridParameters.height, + grid: gridParameters.grid, + ...rest, + }, + }).unwrap(); + } catch (err) { + console.log("error", err); + } + } + + if (widgetsToUpdate.length > 0) { + try { + for (const index in widgetsToUpdate) { + await updateWidget({ + widgetID: widgetsToUpdate[index], + updatedWidget: { + widget: { + ...widgets.find((w) => w.id == widgetsToUpdate[index]), + }, + }, + }).unwrap(); + } + refetchWidgets(dashboard?.id); + } catch (err) { + console.log("error", err); + } + } + + setEditing(false); + setWidgetChangeData([]); + }; + + const handleWidgetChange = async (widget) => { + setWidgetsToUpdate((prevWidgetsToUpdate) => [ + ...prevWidgetsToUpdate, + widget.id, + ]); + setLocalWidgets((prevWidgets) => + prevWidgets.map((w) => (w.id === widget.id ? { ...widget } : w)) + ); + }; + + const handleCloseEditModal = async (data) => { + if (!data) { + setEditModal(false); + setEditModalData(null); + return; + } + + if (data.type === "Image") { + data.customProperties.update = true; + } + + try { + await updateWidget({ + widgetID: data.id, + updatedWidget: { widget: data }, + }).unwrap(); + refetchWidgets(dashboard?.id); + } catch (err) { + console.log("error", err); + } + + setEditModal(false); + setEditModalData(null); + }; + + const handleCancelEdit = () => { + refetchWidgets(); + setEditing(false); + setWidgetChangeData([]); + setGridParameters({ height: dashboard?.height, grid: dashboard?.grid }); + }; + + const handleNewWidgetAdded = 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) { + refetchWidgets(dashboard?.id); + } + } catch (err) { + console.log("error", err); + } + }; + + const editWidget = (widget, index) => { + setEditModal(true); + setEditModalData({ ...widget }); + }; + + 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) { + refetchWidgets(dashboard?.id); + } + } catch (err) { + console.log("error", err); + } + }; + + const onChange = async (widget) => { + try { + await updateWidget({ + widgetID: widget.id, + updatedWidget: { widget: widget }, + }).unwrap(); + refetchWidgets(dashboard?.id); + } catch (err) { + console.log("error", err); + } + }; + + const handleCloseEditSignalsModal = (direction) => { + if (direction === "in") { + setEditInputSignalsModal(false); + } else if (direction === "out") { + setEditOutputSignalsModal(false); + } + }; + + return !isFetchingWidgets && !isFetchingDashboard ? ( +
+
+
+

+ {dashboard?.name} + + + +

+
+ + setPaused(true)} + onUnpause={() => setPaused(false)} + onEditFiles={() => {}} + onEditOutputSignals={() => setEditInputSignalsModal(true)} + onEditInputSignals={() => setEditInputSignalsModal(true)} + /> +
+ +
e.preventDefault()} + > + {editing && ( + + setGridParameters((prevState) => ({ ...prevState, grid: val })) + } + dashboard={dashboard} + onDashboardSizeChange={(val) => + setGridParameters((prevState) => ({ ...prevState, height: val })) + } + widgets={localWidgets} + /> + )} + + + {localWidgets != null && + Object.keys(localWidgets).map((widgetKey) => ( +
+ deleteWidget(widget.id)} + onChange={editing ? handleWidgetChange : onChange} + > + + +
+ ))} +
+ + + + {/* */} + + +
+
+ ) : ( + + ); +}; + +export default Fullscreenable()(DashboardLayout); diff --git a/src/pages/dashboards/dashboard-old.js b/src/pages/dashboards/dashboard-old.js new file mode 100644 index 00000000..50910304 --- /dev/null +++ b/src/pages/dashboards/dashboard-old.js @@ -0,0 +1,618 @@ +/** + * 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, { useState, useEffect, useCallback, useRef, act } from "react"; +import { useDispatch, useSelector } from "react-redux"; +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 EditSignalMappingDialog from "../scenarios/dialogs/edit-signal-mapping"; +import WidgetToolbox from "./widget/widget-toolbox"; +import WidgetArea from "./grid/widget-area"; +import DashboardButtonGroup from "./grid/dashboard-button-group"; +import IconToggleButton from "../../common/buttons/icon-toggle-button"; +import WidgetContainer from "./widget/widget-container"; +import Widget from "./widget/widget-old"; + +import { connectWebSocket, disconnect } from "../../store/websocketSlice"; + +import { + useGetDashboardQuery, + useLazyGetWidgetsQuery, + useLazyGetConfigsQuery, + useAddWidgetMutation, + useUpdateWidgetMutation, + useDeleteWidgetMutation, + useLazyGetFilesQuery, + useUpdateDashboardMutation, + useGetICSQuery, + useLazyGetSignalsQuery, +} from "../../store/apiSlice"; + +const startUpdaterWidgets = new Set(["Slider", "Button", "NumberInput"]); + +const Dashboard = ({ isFullscreen, toggleFullscreen }) => { + const dispatch = useDispatch(); + const params = useParams(); + const { + data: dashboardRes, + error: dashboardError, + isLoading: isDashboardLoading, + } = useGetDashboardQuery(params.dashboard); + const dashboard = dashboardRes ? dashboardRes.dashboard : {}; + const { data: icsRes } = useGetICSQuery(); + const ics = icsRes ? icsRes.ics : []; + + const [triggerGetWidgets] = useLazyGetWidgetsQuery(); + const [triggerGetConfigs] = useLazyGetConfigsQuery(); + const [triggerGetFiles] = useLazyGetFilesQuery(); + const [triggerGetSignals] = useLazyGetSignalsQuery(); + const [addWidget] = useAddWidgetMutation(); + const [updateWidget] = useUpdateWidgetMutation(); + const [deleteWidgetMutation] = useDeleteWidgetMutation(); + const [updateDashboard] = useUpdateDashboardMutation(); + + const [widgets, setWidgets] = useState([]); + const [widgetsToUpdate, setWidgetsToUpdate] = useState([]); + const [configs, setConfigs] = useState([]); + const [signals, setSignals] = useState([]); + const [sessionToken, setSessionToken] = useState( + localStorage.getItem("token") + ); + const [files, setFiles] = 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); + + const [height, setHeight] = useState(10); + const [grid, setGrid] = useState(50); + const [newHeightValue, setNewHeightValue] = useState(0); + + //ics that are included in configurations + const [activeICS, setActiveICS] = useState([]); + + useEffect(() => { + let usedICS = []; + for (const config of configs) { + usedICS.push(config.icID); + } + setActiveICS(ics.filter((i) => usedICS.includes(i.id))); + }, [configs]); + + const activeSocketURLs = useSelector( + (state) => state.websocket.activeSocketURLs + ); + + //connect to websockets + useEffect(() => { + activeICS.forEach((i) => { + if (i.websocketurl) { + if (!activeSocketURLs.includes(i.websocketurl)) + dispatch(connectWebSocket({ url: i.websocketurl, id: i.id })); + } + }); + + return () => { + activeICS.forEach((i) => { + dispatch(disconnect({ id: i.id })); + }); + }; + }, [activeICS]); + + //as soon as dashboard is loaded, load widgets, configs, signals and files for this dashboard + useEffect(() => { + if (dashboard.id) { + fetchWidgets(dashboard.id); + fetchWidgetData(dashboard.scenarioID); + setHeight(dashboard.height); + setGrid(dashboard.grid); + } + }, [dashboard]); + + const fetchWidgets = async (dashboardID) => { + try { + const widgetsRes = await triggerGetWidgets(dashboardID).unwrap(); + if (widgetsRes.widgets) { + setWidgets(widgetsRes.widgets); + } + } catch (err) { + console.log("error fetching data", err); + } + }; + + const fetchWidgetData = async (scenarioID) => { + try { + const filesRes = await triggerGetFiles(scenarioID).unwrap(); + if (filesRes.files) { + setFiles(filesRes.files); + } + const configsRes = await triggerGetConfigs(scenarioID).unwrap(); + if (configsRes.configs) { + setConfigs(configsRes.configs); + //load signals if there are any configs + + if (configsRes.configs.length > 0) { + for (const config of configsRes.configs) { + const signalsInRes = await triggerGetSignals({ + configID: config.id, + direction: "in", + }).unwrap(); + const signalsOutRes = await triggerGetSignals({ + configID: config.id, + direction: "out", + }).unwrap(); + setSignals((prevState) => [ + ...signalsInRes.signals, + ...signalsOutRes.signals, + ...prevState, + ]); + } + } + } + } catch (err) { + console.log("error fetching data", 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) => { + setWidgetsToUpdate((prevWidgetsToUpdate) => [ + ...prevWidgetsToUpdate, + widget.id, + ]); + setWidgets((prevWidgets) => + prevWidgets.map((w) => (w.id === widget.id ? { ...widget } : w)) + ); + + // 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: 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 = async () => { + // 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); + // } + // }); + + if (height !== dashboard.height || grid !== dashboard.grid) { + try { + const { height: oldHeight, grid: oldGrid, ...rest } = dashboard; + await updateDashboard({ + dashboardID: dashboard.id, + dashboard: { height: height, grid: grid, ...rest }, + }).unwrap(); + } catch (err) { + console.log("error", err); + } + } + + if (widgetsToUpdate.length > 0) { + try { + for (const index in widgetsToUpdate) { + await updateWidget({ + widgetID: widgetsToUpdate[index], + updatedWidget: { + widget: { + ...widgets.find((w) => w.id == widgetsToUpdate[index]), + }, + }, + }).unwrap(); + } + fetchWidgets(dashboard.id); + } 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); + // } + // } + // }); + fetchWidgets(dashboard.id); + setEditing(false); + setWidgetChangeData([]); + setHeight(dashboard.height); + setGrid(dashboard.grid); + }; + + const updateGrid = (value) => { + setGrid(value); + }; + + const updateHeight = (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) { + setHeight((prevState) => prevState - 50); + } + } else { + setHeight((prevState) => prevState + 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 boxClasses = classNames("section", "box", { + "fullscreen-padding": isFullscreen, + }); + + 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} + > + onSimulationStarted()} + /> + +
+ ))} +
+ + + + {/* */} + + +
+
+ ); +}; + +export default Fullscreenable()(Dashboard); diff --git a/src/pages/dashboards/dashboard.js b/src/pages/dashboards/dashboard.js index 49d262cc..757153e5 100644 --- a/src/pages/dashboards/dashboard.js +++ b/src/pages/dashboards/dashboard.js @@ -15,548 +15,33 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import React, { useState, useEffect, useCallback, useRef, act } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -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 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 { connectWebSocket, disconnect,reportLength } from '../../store/websocketSlice'; - -import { - useGetDashboardQuery, - useLazyGetWidgetsQuery, - useLazyGetConfigsQuery, - useAddWidgetMutation, - useUpdateWidgetMutation, - useDeleteWidgetMutation, - useLazyGetFilesQuery, - useUpdateDashboardMutation, +import { useParams } from "react-router-dom"; +import { + useGetDashboardQuery, useGetICSQuery, - useLazyGetSignalsQuery -} from '../../store/apiSlice'; - -const startUpdaterWidgets = new Set(['Slider', 'Button', 'NumberInput']); - -const Dashboard = ({ isFullscreen, toggleFullscreen }) => { - const dispatch = useDispatch(); - const params = useParams(); - const { data: dashboardRes, error: dashboardError, isLoading: isDashboardLoading } = useGetDashboardQuery(params.dashboard); - const dashboard = dashboardRes ? dashboardRes.dashboard : {}; - const {data: icsRes} = useGetICSQuery(); - const ics = icsRes ? icsRes.ics : []; - - const [triggerGetWidgets] = useLazyGetWidgetsQuery(); + useLazyGetWidgetsQuery, + useLazyGetConfigsQuery, + useLazyGetFilesQuery, + useLazyGetSignalsQuery, +} from "../../store/apiSlice"; +import { useState } from "react"; +import DashboardLayout from "./dashboard-layout"; +import ErrorBoundary from "./dashboard-error-boundry"; + +const Dashboard = ({}) => { + const { data: { ics } = [] } = useGetICSQuery(); const [triggerGetConfigs] = useLazyGetConfigsQuery(); const [triggerGetFiles] = useLazyGetFilesQuery(); const [triggerGetSignals] = useLazyGetSignalsQuery(); - const [addWidget] = useAddWidgetMutation(); - const [updateWidget] = useUpdateWidgetMutation(); - const [deleteWidgetMutation] = useDeleteWidgetMutation(); - const [updateDashboard] = useUpdateDashboardMutation(); - const [widgets, setWidgets] = useState([]); - const [widgetsToUpdate, setWidgetsToUpdate] = useState([]); - const [configs, setConfigs] = useState([]); - const [signals, setSignals] = useState([]); - const [sessionToken, setSessionToken] = useState(localStorage.getItem("token")); - const [files, setFiles] = 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); - const [height, setHeight] = useState(10); - const [grid, setGrid] = useState(50); - const [newHeightValue, setNewHeightValue] = useState(0); - - //ics that are included in configurations - const [activeICS, setActiveICS] = useState([]); - - useEffect(() => { - let usedICS = []; - for(const config of configs){ - usedICS.push(config.icID); - } - setActiveICS(ics.filter((i) => usedICS.includes(i.id))); - }, [configs]) - - const activeSocketURLs = useSelector((state) => state.websocket.activeSocketURLs); - - //connect to websockets - useEffect(() => { - activeICS.forEach((i) => { - if(i.websocketurl){ - if(!activeSocketURLs.includes(i.websocketurl)) - dispatch(connectWebSocket({ url: i.websocketurl, id: i.id })); - } - }) - - return () => { - activeICS.forEach((i) => { - dispatch(disconnect({ id: i.id })); - }); - }; - - }, [activeICS]); - - - //as soon as dashboard is loaded, load widgets, configs, signals and files for this dashboard - useEffect(() => { - if (dashboard.id) { - fetchWidgets(dashboard.id); - fetchWidgetData(dashboard.scenarioID); - setHeight(dashboard.height); - setGrid(dashboard.grid); - - } - }, [dashboard]); - - const fetchWidgets = async (dashboardID) => { - try { - const widgetsRes = await triggerGetWidgets(dashboardID).unwrap(); - if (widgetsRes.widgets) { - setWidgets(widgetsRes.widgets); - } - } catch (err) { - console.log('error fetching data', err); - } - } - - const fetchWidgetData = async (scenarioID) => { - try { - const filesRes = await triggerGetFiles(scenarioID).unwrap(); - if (filesRes.files) { - setFiles(filesRes.files); - } - const configsRes = await triggerGetConfigs(scenarioID).unwrap(); - if (configsRes.configs) { - setConfigs(configsRes.configs); - //load signals if there are any configs - - if(configsRes.configs.length > 0){ - for(const config of configsRes.configs){ - const signalsInRes = await triggerGetSignals({configID: config.id, direction: "in"}).unwrap(); - const signalsOutRes = await triggerGetSignals({configID: config.id, direction: "out"}).unwrap(); - dispatch(reportLength(signalsInRes.signals.length)) - setSignals(prevState => ([...signalsInRes.signals, ...signalsOutRes.signals, ...prevState])); - } - } - - } - } catch (err) { - console.log('error fetching data', 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) => { - setWidgetsToUpdate(prevWidgetsToUpdate => [...prevWidgetsToUpdate, widget.id]); - setWidgets(prevWidgets => prevWidgets.map(w => w.id === widget.id ? {...widget} : w)); - - // 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: widget } }).unwrap(); - fetchWidgets(dashboard.id); - } catch (err) { - console.log('error', err); - } - }; - - const onSimulationStarted = () => { - widgets.forEach(async (widget) => { - if (startUpdaterWidgets.has(widget.type)) { - let updatedWidget = structuredClone(widget) - updatedWidget.customProperties.simStartedSendValue = true; - try { - await updateWidget({ widgetID: widget.id, updatedWidget: {widget:updatedWidget} }).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 = async () => { - // 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); - // } - // }); - - - if(height !== dashboard.height || grid !== dashboard.grid) { - try { - const {height: oldHeight, grid: oldGrid, ...rest} = dashboard; - await updateDashboard({dashboardID: dashboard.id, dashboard:{height: height, grid: grid, ...rest}}).unwrap(); - } catch (err) { - console.log('error', err); - } - } - - - if(widgetsToUpdate.length > 0){ - try { - for(const index in widgetsToUpdate){ - await updateWidget({ widgetID: widgetsToUpdate[index], updatedWidget: { widget: {...widgets.find(w => w.id == widgetsToUpdate[index])} } }).unwrap(); - } - fetchWidgets(dashboard.id); - } 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); - // } - // } - // }); - fetchWidgets(dashboard.id); - setEditing(false); - setWidgetChangeData([]); - setHeight(dashboard.height); - setGrid(dashboard.grid); - }; - - const updateGrid = (value) => { - setGrid(value); - }; - - const updateHeight = (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)) { - setHeight(prevState => (prevState - 50)); - } - } else { - setHeight( prevState => ( prevState + 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 boxClasses = classNames('section', 'box', { 'fullscreen-padding': isFullscreen }); - - 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} - > - onSimulationStarted()} - /> - -
- ))} -
- - - - {/* */} - - -
-
+ + + ); }; -export default Fullscreenable()(Dashboard); +export default Dashboard; diff --git a/src/pages/dashboards/grid/dashboard-button-group.js b/src/pages/dashboards/grid/dashboard-button-group.js new file mode 100644 index 00000000..e97536ed --- /dev/null +++ b/src/pages/dashboards/grid/dashboard-button-group.js @@ -0,0 +1,154 @@ +/** + * 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/dropzone.js b/src/pages/dashboards/grid/dropzone.js similarity index 100% rename from src/pages/dashboards/dropzone.js rename to src/pages/dashboards/grid/dropzone.js diff --git a/src/pages/dashboards/grid.js b/src/pages/dashboards/grid/grid.js similarity index 100% rename from src/pages/dashboards/grid.js rename to src/pages/dashboards/grid/grid.js diff --git a/src/pages/dashboards/widget-area.js b/src/pages/dashboards/grid/widget-area.js similarity index 71% rename from src/pages/dashboards/widget-area.js rename to src/pages/dashboards/grid/widget-area.js index a70fd9c0..63a58910 100644 --- a/src/pages/dashboards/widget-area.js +++ b/src/pages/dashboards/grid/widget-area.js @@ -15,11 +15,11 @@ * 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'; +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) { @@ -39,23 +39,24 @@ class WidgetArea extends React.Component { if (this.props.onWidgetAdded != null) { this.props.onWidgetAdded(widget); } - } + }; render() { + return ( + + {this.props.children} - return - {this.props.children} - - - ; + + + ); } } @@ -64,11 +65,11 @@ WidgetArea.propTypes = { editing: PropTypes.bool, grid: PropTypes.number, //widgets: PropTypes.array, - onWidgetAdded: PropTypes.func + onWidgetAdded: PropTypes.func, }; WidgetArea.defaultProps = { - widgets: {} + widgets: {}, }; export default WidgetArea; diff --git a/src/pages/dashboards/widget/widget-old.js b/src/pages/dashboards/widget/widget-old.js new file mode 100644 index 00000000..f29fe830 --- /dev/null +++ b/src/pages/dashboards/widget/widget-old.js @@ -0,0 +1,290 @@ +/** + * 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 { useState, useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import WidgetLabel from "./widgets/label.jsx"; +import WidgetLine from "./widgets/line.jsx"; +import WidgetBox from "./widgets/box.jsx"; +import WidgetImage from "./widgets/image.jsx"; +import WidgetPlot from "./widgets/plot.jsx"; +import WidgetTable from "./widgets/table.jsx"; +import WidgetValue from "./widgets/value.jsx"; +import WidgetLamp from "./widgets/lamp.jsx"; +import WidgetGauge from "./widgets/gauge.jsx"; +import WidgetTimeOffset from "./widgets/time-offset.jsx"; +import WidgetICstatus from "./widgets/icstatus.jsx"; +// import WidgetCustomAction from './widgets/custom-action'; +// import WidgetAction from './widgets/action'; +import WidgetButton from "./widgets/button.jsx"; +import WidgetInput from "./widgets/input.jsx"; +import WidgetSlider from "./widgets/slider.jsx"; +// import WidgetTopology from './widgets/topology'; +import WidgetPlayer from "./widgets/player.jsx"; +//import WidgetHTML from './widgets/html'; +import "../../../styles/widgets.css"; +import { useUpdateWidgetMutation } from "../../../store/apiSlice.js"; +import { sendMessageToWebSocket } from "../../../store/websocketSlice.js"; +import { useGetResultsQuery } from "../../../store/apiSlice.js"; + +const Widget = ({ + widget, + editing, + files, + configs, + signals, + paused, + ics, + scenarioID, + onSimulationStarted, +}) => { + const dispatch = useDispatch(); + const { token: sessionToken } = useSelector((state) => state.auth); + const { data, refetch: refetchResults } = useGetResultsQuery(scenarioID); + const results = data ? data.results : []; + const [icIDs, setICIDs] = useState([]); + + const icdata = useSelector((state) => state.websocket.icdata); + + const [websockets, setWebsockets] = useState([]); + const activeSocketURLs = useSelector( + (state) => state.websocket.activeSocketURLs + ); + const [update] = useUpdateWidgetMutation(); + + useEffect(() => { + if (activeSocketURLs.length > 0) { + activeSocketURLs.forEach((url) => { + setWebsockets((prevState) => [ + ...prevState, + { url: url.replace(/^wss:\/\//, "https://"), connected: true }, + ]); + }); + } + }, [activeSocketURLs]); + + useEffect(() => { + if (signals.length > 0) { + let ids = []; + + for (let id of widget.signalIDs) { + let signal = signals.find((s) => s.id === id); + if (signal !== undefined) { + let config = configs.find((m) => m.id === signal.configID); + if (config !== undefined) { + ids[signal.id] = config.icID; + } + } + } + + setICIDs(ids); + } + }, [signals]); + + const inputDataChanged = ( + widget, + data, + controlID, + controlValue, + isFinalChange + ) => { + if (controlID !== "" && isFinalChange) { + let updatedWidget = JSON.parse(JSON.stringify(widget)); + updatedWidget.customProperties[controlID] = controlValue; + + updateWidget(updatedWidget); + } + + let signalID = widget.signalIDs[0]; + let signal = signals.filter((s) => s.id === signalID); + if (signal.length === 0) { + console.warn( + "Unable to send signal for signal ID", + signalID, + ". Signal not found." + ); + return; + } + // determine ID of infrastructure component related to signal[0] + // Remark: there is only one selected signal for an input type widget + let icID = icIDs[signal[0].id]; + dispatch( + sendMessageToWebSocket({ + message: { + ic: icID, + signalID: signal[0].id, + signalIndex: signal[0].index, + data: signal[0].scalingFactor * data, + }, + }) + ); + }; + + const updateWidget = async (updatedWidget) => { + try { + await update({ + widgetID: widget.id, + updatedWidget: { widget: updatedWidget }, + }).unwrap(); + } catch (err) { + console.log("error", err); + } + }; + + if (widget.type === "Line") { + return ; + } else if (widget.type === "Box") { + return ; + } else if (widget.type === "Label") { + return ; + } else if (widget.type === "Image") { + return ; + } else if (widget.type === "Plot") { + return ( + + ); + } else if (widget.type === "Table") { + return ( + + ); + } else if (widget.type === "Value") { + return ( + + ); + } else if (widget.type === "Lamp") { + return ( + + ); + } else if (widget.type === "Gauge") { + return ( + + ); + } else if (widget.type === "TimeOffset") { + return ( + + ); + } else if (widget.type === "ICstatus") { + return ; + } else if (widget.type === "Button") { + return ( + + inputDataChanged( + widget, + value, + controlID, + controlValue, + isFinalChange + ) + } + signals={signals} + token={sessionToken} + /> + ); + } else if (widget.type === "NumberInput") { + return ( + + inputDataChanged( + widget, + value, + controlID, + controlValue, + isFinalChange + ) + } + signals={signals} + token={sessionToken} + /> + ); + } else if (widget.type === "Slider") { + return ( + + inputDataChanged( + widget, + value, + controlID, + controlValue, + isFinalChange + ) + } + signals={signals} + token={sessionToken} + /> + ); + } else if (widget.type === "Player") { + return ( + + ); + } else { + console.log("Unknown widget type", widget.type); + return
Error: Widget not found!
; + } +}; + +export default Widget; diff --git a/src/pages/dashboards/widget/widget.js b/src/pages/dashboards/widget/widget.js index 39017c01..d3c74c52 100644 --- a/src/pages/dashboards/widget/widget.js +++ b/src/pages/dashboards/widget/widget.js @@ -15,176 +15,14 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import React from 'react'; -import { useState, useEffect } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import WidgetLabel from './widgets/label'; -import WidgetLine from './widgets/line'; -import WidgetBox from './widgets/box'; -import WidgetImage from './widgets/image'; -import WidgetPlot from './widgets/plot'; -import WidgetTable from './widgets/table'; -import WidgetValue from './widgets/value'; -import WidgetLamp from './widgets/lamp'; -import WidgetGauge from './widgets/gauge'; -import WidgetTimeOffset from './widgets/time-offset'; -import WidgetICstatus from './widgets/icstatus'; -// import WidgetCustomAction from './widgets/custom-action'; -// import WidgetAction from './widgets/action'; -import WidgetButton from './widgets/button'; -import WidgetInput from './widgets/input'; -import WidgetSlider from './widgets/slider'; -// import WidgetTopology from './widgets/topology'; -import WidgetPlayer from './widgets/player.jsx'; -//import WidgetHTML from './widgets/html'; -import '../../../styles/widgets.css'; -import { useUpdateWidgetMutation } from '../../../store/apiSlice'; -import { initValue, sendMessageToWebSocket } from '../../../store/websocketSlice'; -import { useGetResultsQuery } from '../../../store/apiSlice'; +import WidgetButton from "./widgets/button"; -const Widget = ({widget, editing, files, configs, signals, paused, ics, scenarioID, onSimulationStarted}) => { - const dispatch = useDispatch(); - const { token: sessionToken } = useSelector((state) => state.auth); - const {data, refetch: refetchResults } = useGetResultsQuery(scenarioID); - const results = data ? data.results : []; - const [icIDs, setICIDs] = useState([]); +const Widget = ({ widget, editing }) => { + const widgetTypeMap = { + Button: , + }; - const icdata = useSelector((state) => state.websocket.icdata); - - const [websockets, setWebsockets] = useState([]); - const activeSocketURLs = useSelector((state) => state.websocket.activeSocketURLs); - const [update] = useUpdateWidgetMutation(); - - useEffect(() => { - if(activeSocketURLs.length > 0){ - activeSocketURLs.forEach(url => { - setWebsockets(prevState=>([...prevState, { url: url.replace(/^wss:\/\//, "https://"), connected:true}])) - }) - } - }, [activeSocketURLs]) - - useEffect(() => { - if(signals.length > 0){ - let ids = []; - - for (let id of widget.signalIDs){ - let signal = signals.find(s => s.id === id); - if (signal !== undefined) { - let config = configs.find(m => m.id === signal.configID); - if (config !== undefined){ - ids[signal.id] = config.icID; - } - } - } - - setICIDs(ids); - } - }, [signals]) - - const inputDataChanged = (widget, data, controlID, controlValue, isFinalChange) => { - if (controlID !== '' && isFinalChange) { - let updatedWidget = JSON.parse(JSON.stringify(widget)); - updatedWidget.customProperties[controlID] = controlValue; - updateWidget(updatedWidget); - } - - let signalID = widget.signalIDs[0]; - let signal = signals.filter(s => s.id === signalID) - if (signal.length === 0){ - console.warn("Unable to send signal for signal ID", signalID, ". Signal not found."); - return; - } - if(!isFinalChange){ - dispatch(initValue({idx:signal[0].index,initVal:data})) - return; - } - // determine ID of infrastructure component related to signal[0] - // Remark: there is only one selected signal for an input type widget - let icID = icIDs[signal[0].id]; - dispatch(sendMessageToWebSocket({message: {ic: icID, signalID: signal[0].id, signalIndex: signal[0].index, data: signal[0].scalingFactor * data}})); - } - - const updateWidget = async (updatedWidget) => { - try { - await update({ widgetID: widget.id, updatedWidget: { widget: updatedWidget } }).unwrap(); - } catch (err) { - console.log('error', err); - } - } - - - if (widget.type === 'Line') { - return ; - } else if (widget.type === 'Box') { - return ; - } else if (widget.type === 'Label') { - return ; - } else if (widget.type === 'Image') { - return ; - } else if (widget.type === 'Plot') { - return ; - } else if (widget.type === 'Table') { - return ; - } else if (widget.type === 'Value') { - return ; - } else if (widget.type === 'Lamp') { - return ; - } else if (widget.type === 'Gauge') { - return ; - } else if (widget.type === 'TimeOffset') { - return ; - } else if (widget.type === 'ICstatus') { - return ; - } else if (widget.type === 'Button') { - return ( - - inputDataChanged(widget, value, controlID, controlValue, isFinalChange) - } - signals={signals} - /> - ); - } else if (widget.type === 'NumberInput') { - return ( - - inputDataChanged(widget, value, controlID, controlValue, isFinalChange) - } - signals={signals} - /> - ); - } else if (widget.type === 'Slider') { - return ( - - inputDataChanged(widget, value, controlID, controlValue, isFinalChange) - } - signals={signals} - /> - ); - } else if (widget.type === 'Player') { - return ( - - ); - } else { - console.log('Unknown widget type', widget); - return
Error: Widget not found!
; - } -} + return widgetTypeMap["Button"]; +}; export default Widget; diff --git a/src/pages/dashboards/widget/widgets/button.jsx b/src/pages/dashboards/widget/widgets/button.jsx index 926a9276..574f8274 100644 --- a/src/pages/dashboards/widget/widgets/button.jsx +++ b/src/pages/dashboards/widget/widgets/button.jsx @@ -15,62 +15,82 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import React, { useState, useEffect } from 'react'; -import { Button } from 'react-bootstrap'; +import React, { useState, useEffect } from "react"; +import { Button } from "react-bootstrap"; -const WidgetButton = (props) => { - const [pressed, setPressed] = useState(props.widget.customProperties.pressed); +const WidgetButton = ({ widget, editing }) => { + const [pressed, setPressed] = useState(widget.customProperties.pressed); - useEffect(() => { - let widget = { ...props.widget }; - widget.customProperties.simStartedSendValue = false; - widget.customProperties.pressed = false; - if(props.onInputChanged && props.signals && props.signals.length > 0){ - props.onInputChanged(widget.customProperties.value, "", "", false); - } - }, [props.widget]); + // useEffect(() => { + // let widget = props.widget; + // widget.customProperties.simStartedSendValue = false; + // widget.customProperties.pressed = false; - useEffect(() => { - if (props.widget.customProperties.simStartedSendValue) { - let widget = props.widget; - widget.customProperties.simStartedSendValue = false; - widget.customProperties.pressed = false; - props.onInputChanged(widget.customProperties.off_value, '', false, false); - } - }, [props, setPressed]); + // // AppDispatcher.dispatch({ + // // type: 'widgets/start-edit', + // // token: props.token, + // // data: widget + // // }); - useEffect(() => { - setPressed(props.widget.customProperties.pressed); - }, [props.widget.customProperties.pressed]); + // // Effect cleanup + // return () => { + // // Clean up if needed + // }; + // }, [props.token, props.widget]); - const onPress = (e) => { - if (e.button === 0 && !props.widget.customProperties.toggle) { - valueChanged(props.widget.customProperties.on_value, true); - } - }; + // useEffect(() => { + // if (props.widget.customProperties.simStartedSendValue) { + // let widget = props.widget; + // widget.customProperties.simStartedSendValue = false; + // widget.customProperties.pressed = false; + // AppDispatcher.dispatch({ + // type: 'widgets/start-edit', + // token: props.token, + // data: widget + // }); - const onRelease = (e) => { - if (e.button === 0) { - let nextState = false; - if (props.widget.customProperties.toggle) { - nextState = !pressed; - } - valueChanged(nextState ? props.widget.customProperties.on_value : props.widget.customProperties.off_value, nextState); - } - }; + // props.onInputChanged(widget.customProperties.off_value, '', false, false); + // } + // }, [props, setPressed]); + + // useEffect(() => { + // setPressed(props.widget.customProperties.pressed); + // }, [props.widget.customProperties.pressed]); + + // const onPress = (e) => { + // if (e.button === 0 && !props.widget.customProperties.toggle) { + // valueChanged(props.widget.customProperties.on_value, true); + // } + // }; + + // const onRelease = (e) => { + // if (e.button === 0) { + // let nextState = false; + // if (props.widget.customProperties.toggle) { + // nextState = !pressed; + // } + // valueChanged(nextState ? props.widget.customProperties.on_value : props.widget.customProperties.off_value, nextState); + // } + // }; - const valueChanged = (newValue, newPressed) => { - if (props.onInputChanged) { - props.onInputChanged(newValue, 'pressed', newPressed, true); + // const valueChanged = (newValue, newPressed) => { + // if (props.onInputChanged) { + // props.onInputChanged(newValue, 'pressed', newPressed, true); + // } + // setPressed(newPressed); + // }; + + useEffect(() => { + if (pressed) { + console.log("Yo"); } - setPressed(newPressed); - }; + }, [pressed]); - let opacity = props.widget.customProperties.background_color_opacity; + let opacity = widget.customProperties.background_color_opacity; const buttonStyle = { - backgroundColor: props.widget.customProperties.background_color, - borderColor: props.widget.customProperties.border_color, - color: props.widget.customProperties.font_color, + backgroundColor: widget.customProperties.background_color, + borderColor: widget.customProperties.border_color, + color: widget.customProperties.font_color, opacity: pressed ? (opacity + 1) / 4 : opacity, }; @@ -80,11 +100,11 @@ const WidgetButton = (props) => { className="full" style={buttonStyle} active={pressed} - disabled={props.editing} - onMouseDown={(e) => onPress(e)} - onMouseUp={(e) => onRelease(e)} + disabled={editing} + onMouseDown={(e) => setPressed(true)} + onMouseUp={(e) => setPressed(false)} > - {props.widget.name} + {widget.name} ); diff --git a/src/pages/scenarios/dialogs/edit-signal-mapping.js b/src/pages/scenarios/dialogs/edit-signal-mapping.js index 20b0720e..9e9fae52 100644 --- a/src/pages/scenarios/dialogs/edit-signal-mapping.js +++ b/src/pages/scenarios/dialogs/edit-signal-mapping.js @@ -7,7 +7,7 @@ import Icon from "../../../common/icon"; import { useGetSignalsQuery, useAddSignalMutation, useDeleteSignalMutation, useUpdateSignalMutation } from "../../../store/apiSlice"; import { Collapse } from 'react-collapse'; -const ExportSignalMappingDialog = ({isShown, direction, onClose, configID}) => { +const EditSignalMappingDialog = ({isShown, direction, onClose, configID}) => { const [isCollapseOpened, setCollapseOpened] = useState(false); const [checkedSignalsIDs, setCheckedSignalsIDs] = useState([]); @@ -256,4 +256,4 @@ const ExportSignalMappingDialog = ({isShown, direction, onClose, configID}) => { return isShown ? DialogWindow : <> } -export default ExportSignalMappingDialog; +export default EditSignalMappingDialog; diff --git a/src/store/apiSlice.js b/src/store/apiSlice.js index 3080f15d..45433be0 100644 --- a/src/store/apiSlice.js +++ b/src/store/apiSlice.js @@ -1,25 +1,44 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; -import { sessionToken } from '../localStorage'; -import { widgetEndpoints } from './endpoints/widget-endpoints'; -import { scenarioEndpoints } from './endpoints/scenario-endpoints'; -import { dashboardEndpoints } from './endpoints/dashboard-endpoints'; -import { icEndpoints } from './endpoints/ic-endpoints'; -import { configEndpoints } from './endpoints/config-endpoints'; -import { userEndpoints } from './endpoints/user-endpoints'; -import { fileEndpoints } from './endpoints/file-endpoints'; -import { signalEndpoints } from './endpoints/signal-endpoints'; -import { resultEndpoints } from './endpoints/result-endpoints'; -import { authEndpoints } from './endpoints/auth-endpoints'; -import { websocketEndpoints } from './endpoints/websocket-endpoints'; +/** + * 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 { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; +import { sessionToken } from "../localStorage"; +import { widgetEndpoints } from "./endpoints/widget-endpoints"; +import { scenarioEndpoints } from "./endpoints/scenario-endpoints"; +import { dashboardEndpoints } from "./endpoints/dashboard-endpoints"; +import { icEndpoints } from "./endpoints/ic-endpoints"; +import { configEndpoints } from "./endpoints/config-endpoints"; +import { userEndpoints } from "./endpoints/user-endpoints"; +import { fileEndpoints } from "./endpoints/file-endpoints"; +import { signalEndpoints } from "./endpoints/signal-endpoints"; +import { resultEndpoints } from "./endpoints/result-endpoints"; +import { authEndpoints } from "./endpoints/auth-endpoints"; +import { websocketEndpoints } from "./endpoints/websocket-endpoints"; +import { usergroupEndpoints } from "./endpoints/usergroup-endpoints"; +import { selectToken } from "./authSlice"; export const apiSlice = createApi({ - reducerPath: 'api', + reducerPath: "api", baseQuery: fetchBaseQuery({ - baseUrl: '/api/v2', - prepareHeaders: (headers) => { - const token = sessionToken; + baseUrl: "/api/v2", + prepareHeaders: (headers, { getState }) => { + const token = selectToken(getState()); if (token) { - headers.set('Authorization', `Bearer ${token}`); + headers.set("Authorization", `Bearer ${token}`); } return headers; }, @@ -39,14 +58,14 @@ export const apiSlice = createApi({ }), }); -export const { - useGetScenariosQuery, - useGetScenarioByIdQuery, - useGetConfigsQuery, +export const { + useGetScenariosQuery, + useGetScenarioByIdQuery, + useGetConfigsQuery, useLazyGetConfigsQuery, - useGetDashboardsQuery, + useGetDashboardsQuery, useGetICSQuery, - useAddScenarioMutation, + useAddScenarioMutation, useDeleteScenarioMutation, useUpdateScenarioMutation, useGetUsersOfScenarioQuery, @@ -86,5 +105,14 @@ export const { useUpdateSignalMutation, useGetIcDataQuery, useLazyDownloadImageQuery, - useUpdateComponentConfigMutation + useUpdateComponentConfigMutation, + useGetUsergroupsQuery, + useAddUsergroupMutation, + useDeleteUsergroupMutation, + useGetUsergroupByIdQuery, + useGetUsersByUsergroupIdQuery, + useAddUserToUsergroupMutation, + useDeleteUserFromUsergroupMutation, + useUpdateUsergroupMutation, + useGetWidgetsQuery, } = apiSlice; diff --git a/src/store/dashboardSlice.js b/src/store/dashboardSlice.js new file mode 100644 index 00000000..23807acf --- /dev/null +++ b/src/store/dashboardSlice.js @@ -0,0 +1,101 @@ +/** + * 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 { createSlice } from "@reduxjs/toolkit"; +import { sendMessageToWebSocket } from "./webSocketActions"; // Adjust the import path as needed + +const initialState = { + data: null, + controlID: "", + controlValue: null, + isFinalChange: false, + signals: [], + icIDs: {}, +}; + +const widgetSlice = createSlice({ + name: "widget", + initialState, + reducers: { + setData(state, action) { + state.data = action.payload; + }, + setControlID(state, action) { + state.controlID = action.payload; + }, + setControlValue(state, action) { + state.controlValue = action.payload; + }, + setIsFinalChange(state, action) { + state.isFinalChange = action.payload; + }, + setSignals(state, action) { + state.signals = action.payload; + }, + setIcIDs(state, action) { + state.icIDs = action.payload; + }, + }, +}); + +export const { + setWidget, + setData, + setControlID, + setControlValue, + setIsFinalChange, + setSignals, + setIcIDs, +} = widgetSlice.actions; + +export const inputDataChanged = + (widget, data, controlID, controlValue, isFinalChange) => + async (dispatch, getState) => { + if (controlID !== "" && isFinalChange) { + const updatedWidget = JSON.parse(JSON.stringify(widget)); + updatedWidget.customProperties[controlID] = controlValue; + + dispatch(setWidget(updatedWidget)); + } + + const state = getState().widget; + const signalID = widget.signalIDs[0]; + const signal = state.signals.filter((s) => s.id === signalID); + + if (signal.length === 0) { + console.warn( + "Unable to send signal for signal ID", + signalID, + ". Signal not found." + ); + return; + } + + const icID = state.icIDs[signal[0].id]; + dispatch( + sendMessageToWebSocket({ + message: { + ic: icID, + signalID: signal[0].id, + signalIndex: signal[0].index, + data: signal[0].scalingFactor * data, + }, + }) + ); + }; + +export default widgetSlice.reducer; From c206ae02d323ae516c01ee72a1c5211d946307c0 Mon Sep 17 00:00:00 2001 From: Andrii Podriez Date: Thu, 16 Jan 2025 23:44:26 +0100 Subject: [PATCH 05/12] Change widget context menu into a functional component Signed-off-by: Andrii Podriez Signed-off-by: SystemsPurge --- .../dashboards/widget/widget-context-menu.js | 148 ------------------ .../dashboards/widget/widget-context-menu.jsx | 144 +++++++++++++++++ 2 files changed, 144 insertions(+), 148 deletions(-) delete mode 100644 src/pages/dashboards/widget/widget-context-menu.js create mode 100644 src/pages/dashboards/widget/widget-context-menu.jsx diff --git a/src/pages/dashboards/widget/widget-context-menu.js b/src/pages/dashboards/widget/widget-context-menu.js deleted file mode 100644 index 51d96e85..00000000 --- a/src/pages/dashboards/widget/widget-context-menu.js +++ /dev/null @@ -1,148 +0,0 @@ -/** - * 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 ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import { Menu, Item, Separator } from 'react-contexify'; -import "react-contexify/dist/ReactContexify.css"; - -class WidgetContextMenu extends React.Component { - - editWidget = event => { - if (this.props.onEdit != null) { - this.props.onEdit(this.props.widget, this.props.index); - } - }; - - duplicateWidget = event => { - if (this.props.onDuplicate != null) { - this.props.onDuplicate(this.props.widget); - } - }; - - deleteWidget = event => { - if (this.props.onDelete != null) { - this.props.onDelete(this.props.widget, this.props.index); - } - }; - - moveAbove = event => { - this.props.widget.z += 10; - if (this.props.widget.z > 100) { - this.props.widget.z = 100; - } - - if (this.props.onChange != null) { - this.props.onChange(this.props.widget, this.props.index); - } - }; - - moveToFront = event => { - this.props.widget.z = 100; - - if (this.props.onChange != null) { - this.props.onChange(this.props.widget, this.props.index); - } - }; - - moveUnderneath = event => { - this.props.widget.z -= 10; - if (this.props.widget.z < 0) { - this.props.widget.z = 0; - } - - if (this.props.onChange != null) { - this.props.onChange(this.props.widget, this.props.index); - } - }; - - moveToBack = event => { - this.props.widget.z = 0; - - if (this.props.onChange != null) { - this.props.onChange(this.props.widget, this.props.index); - } - }; - - lockWidget = event => { - this.props.widget.isLocked = true; - - if (this.props.onChange != null) { - this.props.onChange(this.props.widget, this.props.index); - } - }; - - unlockWidget = event => { - this.props.widget.isLocked = false; - - if (this.props.onChange != null) { - this.props.onChange(this.props.widget, this.props.index); - } - }; - - - renderContextMenu() { - const isLocked = this.props.widget.locked; - - let dim = { - width: '100%', - height: '100%' - }; - - return ( -
- - Edit - Duplicate - Delete - - - - Move above - Move to front - Move underneath - Move to back - - - - Lock - Unlock - -
- ) - } - - - render() { - - return ReactDOM.createPortal( - this.renderContextMenu(), - document.body - ); - } -} - -WidgetContextMenu.propTypes = { - index: PropTypes.number.isRequired, - widget: PropTypes.object.isRequired, - onEdit: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired -}; - -export default WidgetContextMenu; diff --git a/src/pages/dashboards/widget/widget-context-menu.jsx b/src/pages/dashboards/widget/widget-context-menu.jsx new file mode 100644 index 00000000..88af3b57 --- /dev/null +++ b/src/pages/dashboards/widget/widget-context-menu.jsx @@ -0,0 +1,144 @@ +/** + * 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 ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import { Menu, Item, Separator } from "react-contexify"; +import "react-contexify/dist/ReactContexify.css"; + +const WidgetContextMenu = ({ + index, + widget, + onEdit, + onDuplicate, + onDelete, + onChange, +}) => { + const editWidget = () => { + if (onEdit) { + onEdit(widget, index); + } + }; + + const duplicateWidget = () => { + if (onDuplicate) { + onDuplicate(widget); + } + }; + + const deleteWidget = () => { + if (onDelete) { + onDelete(widget, index); + } + }; + + const moveAbove = () => { + const updatedWidget = { ...widget, z: Math.min(widget.z + 10, 100) }; + if (onChange) { + onChange(updatedWidget, index); + } + }; + + const moveToFront = () => { + const updatedWidget = { ...widget, z: 100 }; + if (onChange) { + onChange(updatedWidget, index); + } + }; + + const moveUnderneath = () => { + const updatedWidget = { ...widget, z: Math.max(widget.z - 10, 0) }; + if (onChange) { + onChange(updatedWidget, index); + } + }; + + const moveToBack = () => { + const updatedWidget = { ...widget, z: 0 }; + if (onChange) { + onChange(updatedWidget, index); + } + }; + + const lockWidget = () => { + const updatedWidget = { ...widget, isLocked: true }; + if (onChange) { + onChange(updatedWidget, index); + } + }; + + const unlockWidget = () => { + const updatedWidget = { ...widget, isLocked: false }; + if (onChange) { + onChange(updatedWidget, index); + } + }; + + const renderContextMenu = () => ( +
+ + + Edit + + + Duplicate + + + Delete + + + + + + Move above + + + Move to front + + + Move underneath + + + Move to back + + + + + + Lock + + + Unlock + + +
+ ); + + return ReactDOM.createPortal(renderContextMenu(), document.body); +}; + +WidgetContextMenu.propTypes = { + index: PropTypes.number.isRequired, + widget: PropTypes.object.isRequired, + onEdit: PropTypes.func.isRequired, + onDuplicate: PropTypes.func, + onDelete: PropTypes.func, + onChange: PropTypes.func.isRequired, +}; + +export default WidgetContextMenu; From 4e3ce18017a6f4c55ce8662a7d5a0c75f6aa9290 Mon Sep 17 00:00:00 2001 From: Andrii Podriez Date: Wed, 22 Jan 2025 03:20:18 +0100 Subject: [PATCH 06/12] Update refactored widget behavior Signed-off-by: Andrii Podriez Signed-off-by: SystemsPurge --- src/app.js | 179 +++-- .../dashboards/dashboard-error-boundry.js | 4 +- src/pages/dashboards/dashboard-layout.js | 37 - .../{dashboard.js => dashboard-new.js} | 45 ++ .../{dashboard-old.js => dashboard.jsx} | 106 ++- src/pages/dashboards/grid/widget-area.js | 2 +- .../dashboards/widget/websocket-store.js | 59 -- .../dashboards/widget/widget-container.js | 124 +-- src/pages/dashboards/widget/widget-factory.js | 248 ------ .../dashboards/widget/widget-factory.jsx | 245 ++++++ src/pages/dashboards/widget/widget-store.js | 95 --- src/pages/dashboards/widget/widget.js | 28 - .../widget/{widget-old.js => widget.jsx} | 77 +- .../dashboards/widget/widgets/button.jsx | 93 +-- .../dashboards/widget/widgets/icstatus.jsx | 32 +- src/pages/dashboards/widget/widgets/input.jsx | 37 +- .../dashboards/widget/widgets/player.jsx | 715 ++++++++++-------- .../dashboards/widget/widgets/slider.jsx | 34 +- src/store/apiSlice.js | 1 + src/store/endpoints/ic-endpoints.js | 22 +- 20 files changed, 1077 insertions(+), 1106 deletions(-) rename src/pages/dashboards/{dashboard.js => dashboard-new.js} (53%) rename src/pages/dashboards/{dashboard-old.js => dashboard.jsx} (88%) delete mode 100644 src/pages/dashboards/widget/websocket-store.js delete mode 100644 src/pages/dashboards/widget/widget-factory.js create mode 100644 src/pages/dashboards/widget/widget-factory.jsx delete mode 100644 src/pages/dashboards/widget/widget-store.js delete mode 100644 src/pages/dashboards/widget/widget.js rename src/pages/dashboards/widget/{widget-old.js => widget.jsx} (82%) diff --git a/src/app.js b/src/app.js index 79405053..ce3074ed 100644 --- a/src/app.js +++ b/src/app.js @@ -15,102 +15,129 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import React from 'react'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend }from 'react-dnd-html5-backend'; -import NotificationSystem from 'react-notification-system'; -import { Redirect, Route } from 'react-router-dom'; -import jwt from 'jsonwebtoken'; -import NotificationsDataManager from './common/data-managers/notifications-data-manager'; -import Home from './common/home'; -import Header from './common/header'; -import Menu from './common/menu'; -import InfrastructureComponent from './pages/infrastructure/ic'; -import Scenarios from './pages/scenarios/scenarios'; -import APIBrowser from './common/api-browser'; -import Scenario from './pages/scenarios/scenario'; -import Users from './pages/users/users'; -import Dashboard from './pages/dashboards/dashboard'; -import Account from './pages/account/account'; -import './styles/app.css'; -import './styles/login.css'; -import branding from './branding/branding'; -import Logout from './pages/login/logout'; -import Infrastructure from './pages/infrastructure/infrastructure'; -import { useSelector } from 'react-redux'; +import React from "react"; +import { useRef, useEffect } from "react"; +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import NotificationSystem from "react-notification-system"; +import { Redirect, Route } from "react-router-dom"; +import jwt from "jsonwebtoken"; +import NotificationsDataManager from "./common/data-managers/notifications-data-manager"; +import Home from "./common/home"; +import Header from "./common/header"; +import Menu from "./common/menu"; +import InfrastructureComponent from "./pages/infrastructure/ic"; +import Scenarios from "./pages/scenarios/scenarios"; +import APIBrowser from "./common/api-browser"; +import Scenario from "./pages/scenarios/scenario"; +import Users from "./pages/users/users"; +import Dashboard from "./pages/dashboards/dashboard.jsx"; +import Account from "./pages/account/account"; +import "./styles/app.css"; +import "./styles/login.css"; +import branding from "./branding/branding"; +import Logout from "./pages/login/logout"; +import Infrastructure from "./pages/infrastructure/infrastructure"; +import Usergroup from "./pages/usergroups/usergroup"; +import { useSelector } from "react-redux"; +import DashboardErrorBoundary from "./pages/dashboards/dashboard-error-boundry"; const App = () => { - const isTokenExpired = (token) => { let decodedToken = jwt.decode(token); let timeNow = (new Date().getTime() + 1) / 1000; return decodedToken.exp < timeNow; - } + }; const { isAuthenticated, token, user } = useSelector((state) => state.auth); if (!isAuthenticated || isTokenExpired(token)) { console.log("APP redirecting to logout/login"); - return (); + return ; } else { console.log("APP rendering app"); const pages = branding.values.pages; - return ( -
- -
+ return ( + +
+ +
-
- +
+ -
- - { pages.home ? : '' } - { pages.scenarios ? <> - - - - - - - - - - - - - - - - - : '' } - { user.role === "Admin" || pages.infrastructure ? <> - - - - - - - - : '' } - { pages.account ? : '' } - { user.role === "Admin" ? - - - - : '' } - { user.role === "Admin" || pages.api ? - - : '' } +
+ + {pages.home ? : ""} + {pages.scenarios ? ( + <> + + + + + + + + + + + + + + + + + + + ) : ( + "" + )} + {user.role === "Admin" || pages.infrastructure ? ( + <> + + + + + + + + ) : ( + "" + )} + {pages.account ? ( + + + + ) : ( + "" + )} + {user.role === "Admin" ? ( + <> + + + + + + + + ) : ( + "" + )} + {user.role === "Admin" || pages.api ? ( + + ) : ( + "" + )} +
-
- {branding.getFooter()} -
- ) + {branding.getFooter()} +
+
+ ); } -} +}; export default App; diff --git a/src/pages/dashboards/dashboard-error-boundry.js b/src/pages/dashboards/dashboard-error-boundry.js index 42e881ac..57caf2b9 100644 --- a/src/pages/dashboards/dashboard-error-boundry.js +++ b/src/pages/dashboards/dashboard-error-boundry.js @@ -1,6 +1,6 @@ import React from "react"; -class ErrorBoundary extends React.Component { +class DashboardErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; @@ -27,4 +27,4 @@ class ErrorBoundary extends React.Component { } } -export default ErrorBoundary; +export default DashboardErrorBoundary; diff --git a/src/pages/dashboards/dashboard-layout.js b/src/pages/dashboards/dashboard-layout.js index 69e5a2c5..823ec030 100644 --- a/src/pages/dashboards/dashboard-layout.js +++ b/src/pages/dashboards/dashboard-layout.js @@ -83,48 +83,11 @@ const DashboardLayout = ({ isFullscreen, toggleFullscreen }) => { } }, [widgets]); - const fetchWidgetData = async (scenarioID) => { - try { - const configsRes = await triggerGetConfigs(scenarioID).unwrap(); - if (configsRes.configs) { - setConfigs(configsRes.configs); - //load signals if there are any configs - - if (configsRes.configs.length > 0) { - for (const config of configsRes.configs) { - const signalsInRes = await triggerGetSignals({ - configID: config.id, - direction: "in", - }).unwrap(); - const signalsOutRes = await triggerGetSignals({ - configID: config.id, - direction: "out", - }).unwrap(); - setSignals((prevState) => [ - ...signalsInRes.signals, - ...signalsOutRes.signals, - ...prevState, - ]); - } - } - } - } catch (err) { - console.log("error fetching data", err); - } - }; - const [gridParameters, setGridParameters] = useState({ height: 10, grid: 50, }); - useEffect(() => { - if (!isFetchingDashboard) { - setGridParameters({ height: dashboard.height, grid: dashboard.grid }); - fetchWidgetData(dashboard.scenarioID); - } - }, [isFetchingDashboard]); - const boxClasses = classNames("section", "box", { "fullscreen-padding": isFullscreen, }); diff --git a/src/pages/dashboards/dashboard.js b/src/pages/dashboards/dashboard-new.js similarity index 53% rename from src/pages/dashboards/dashboard.js rename to src/pages/dashboards/dashboard-new.js index 757153e5..5ac7d8e2 100644 --- a/src/pages/dashboards/dashboard.js +++ b/src/pages/dashboards/dashboard-new.js @@ -15,6 +15,7 @@ * along with VILLASweb. If not, see . ******************************************************************************/ +import { useEffect } from "react"; import { useParams } from "react-router-dom"; import { useGetDashboardQuery, @@ -23,13 +24,20 @@ import { useLazyGetConfigsQuery, useLazyGetFilesQuery, useLazyGetSignalsQuery, + useGetWidgetsQuery, } from "../../store/apiSlice"; import { useState } from "react"; import DashboardLayout from "./dashboard-layout"; import ErrorBoundary from "./dashboard-error-boundry"; const Dashboard = ({}) => { + const params = useParams(); const { data: { ics } = [] } = useGetICSQuery(); + const { + data: { dashboard } = {}, + isFetching: isFetchingDashboard, + refetch: refetchDashboard, + } = useGetDashboardQuery(params.dashboard); const [triggerGetConfigs] = useLazyGetConfigsQuery(); const [triggerGetFiles] = useLazyGetFilesQuery(); const [triggerGetSignals] = useLazyGetSignalsQuery(); @@ -37,6 +45,43 @@ const Dashboard = ({}) => { const [editing, setEditing] = useState(false); const [locked, setLocked] = useState(false); + useEffect(() => { + if (!isFetchingDashboard) { + setGridParameters({ height: dashboard.height, grid: dashboard.grid }); + fetchWidgetData(dashboard.scenarioID); + } + }, [isFetchingDashboard]); + + const fetchWidgetData = async (scenarioID) => { + try { + const configsRes = await triggerGetConfigs(scenarioID).unwrap(); + if (configsRes.configs) { + setConfigs(configsRes.configs); + //load signals if there are any configs + + if (configsRes.configs.length > 0) { + for (const config of configsRes.configs) { + const signalsInRes = await triggerGetSignals({ + configID: config.id, + direction: "in", + }).unwrap(); + const signalsOutRes = await triggerGetSignals({ + configID: config.id, + direction: "out", + }).unwrap(); + setSignals((prevState) => [ + ...signalsInRes.signals, + ...signalsOutRes.signals, + ...prevState, + ]); + } + } + } + } catch (err) { + console.log("error fetching data", err); + } + }; + return ( diff --git a/src/pages/dashboards/dashboard-old.js b/src/pages/dashboards/dashboard.jsx similarity index 88% rename from src/pages/dashboards/dashboard-old.js rename to src/pages/dashboards/dashboard.jsx index 50910304..9f424717 100644 --- a/src/pages/dashboards/dashboard-old.js +++ b/src/pages/dashboards/dashboard.jsx @@ -19,7 +19,6 @@ import React, { useState, useEffect, useCallback, useRef, act } from "react"; import { useDispatch, useSelector } from "react-redux"; 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 EditSignalMappingDialog from "../scenarios/dialogs/edit-signal-mapping"; @@ -28,7 +27,7 @@ import WidgetArea from "./grid/widget-area"; import DashboardButtonGroup from "./grid/dashboard-button-group"; import IconToggleButton from "../../common/buttons/icon-toggle-button"; import WidgetContainer from "./widget/widget-container"; -import Widget from "./widget/widget-old"; +import Widget from "./widget/widget.jsx"; import { connectWebSocket, disconnect } from "../../store/websocketSlice"; @@ -44,11 +43,13 @@ import { useGetICSQuery, useLazyGetSignalsQuery, } from "../../store/apiSlice"; +import { Spinner } from "react-bootstrap"; const startUpdaterWidgets = new Set(["Slider", "Button", "NumberInput"]); const Dashboard = ({ isFullscreen, toggleFullscreen }) => { const dispatch = useDispatch(); + const { token: sessionToken } = useSelector((state) => state.auth); const params = useParams(); const { data: dashboardRes, @@ -72,9 +73,6 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { const [widgetsToUpdate, setWidgetsToUpdate] = useState([]); const [configs, setConfigs] = useState([]); const [signals, setSignals] = useState([]); - const [sessionToken, setSessionToken] = useState( - localStorage.getItem("token") - ); const [files, setFiles] = useState([]); const [editing, setEditing] = useState(false); const [paused, setPaused] = useState(false); @@ -85,14 +83,11 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { 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); const [height, setHeight] = useState(10); const [grid, setGrid] = useState(50); - const [newHeightValue, setNewHeightValue] = useState(0); //ics that are included in configurations const [activeICS, setActiveICS] = useState([]); @@ -233,12 +228,15 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { prevWidgets.map((w) => (w.id === widget.id ? { ...widget } : w)) ); - // try { - // await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap(); - // fetchWidgets(dashboard.id); - // } catch (err) { - // console.log('error', err); - // } + try { + await updateWidget({ + widgetID: widget.id, + updatedWidget: { widget }, + }).unwrap(); + fetchWidgets(dashboard.id); + } catch (err) { + console.log("error", err); + } }; const onChange = async (widget) => { @@ -368,16 +366,19 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { }; const saveEditing = async () => { - // 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); - // } - // }); + 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); + } + }); if (height !== dashboard.height || grid !== dashboard.grid) { try { @@ -410,25 +411,23 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { } 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); - // } - // } - // }); + 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); + } + } + }); fetchWidgets(dashboard.id); setEditing(false); - setWidgetChangeData([]); setHeight(dashboard.height); setGrid(dashboard.grid); }; @@ -465,14 +464,8 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { } }; - const buttonStyle = { marginLeft: "10px" }; - const iconStyle = { height: "25px", width: "25px" }; - const boxClasses = classNames("section", "box", { - "fullscreen-padding": isFullscreen, - }); - if (isDashboardLoading) { - return
Loading...
; + return ; } if (dashboardError) { @@ -480,7 +473,11 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { } return ( -
+

@@ -494,9 +491,9 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { uncheckedIcon="lock-open" tooltipChecked="Dashboard is locked, cannot be edited" tooltipUnchecked="Dashboard is unlocked, can be edited" - disabled={true} - buttonStyle={buttonStyle} - iconStyle={iconStyle} + onChange={() => setLocked(!locked)} + buttonStyle={{ marginLeft: "10px" }} + iconStyle={{ height: "25px", width: "25px" }} />

@@ -589,17 +586,6 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { scenarioID={dashboard.scenarioID} /> - {/* */} - . - ******************************************************************************/ - -import ArrayStore from '../common/array-store'; - -class WebsocketStore extends ArrayStore { - - updateSocketStatus(state, socket) { - let checkInclusion = false; - state.forEach((element) => { - if (element.url === socket.url) { - element.connected = socket.connected; - checkInclusion = true; - } - }) - if (!checkInclusion) { - state.push(socket); - } - this.__emitChange(); - - return state; - } - - reduce(state, action) { - let tempSocket = {}; - switch (action.type) { - - case 'websocket/connected': - tempSocket.url = action.data; - tempSocket.connected = true; - return this.updateSocketStatus(state, tempSocket); - - case 'websocket/connection-error': - tempSocket.url = action.data; - tempSocket.connected = false; - return this.updateSocketStatus(state, tempSocket); - - - default: - return super.reduce(state, action); - } - } -} - -export default new WebsocketStore(); diff --git a/src/pages/dashboards/widget/widget-container.js b/src/pages/dashboards/widget/widget-container.js index ff336e14..61ea65e8 100644 --- a/src/pages/dashboards/widget/widget-container.js +++ b/src/pages/dashboards/widget/widget-container.js @@ -15,12 +15,12 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { Rnd } from 'react-rnd'; -import WidgetContextMenu from '../widget/widget-context-menu'; -import {contextMenu} from "react-contexify"; +import React from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { Rnd } from "react-rnd"; +import WidgetContextMenu from "../widget/widget-context-menu.jsx"; +import { contextMenu } from "react-contexify"; class WidgetContainer extends React.Component { constructor(props) { @@ -36,7 +36,7 @@ class WidgetContainer extends React.Component { return Math.round(value / this.props.grid) * this.props.grid; } - borderWasClicked = event => { + borderWasClicked = (event) => { if (event.button !== 2) { return; } @@ -60,11 +60,19 @@ class WidgetContainer extends React.Component { const widget = this.props.widget; // resize depends on direction - if (direction === 'left' || direction === 'topLeft' || direction === 'bottomLeft') { + if ( + direction === "left" || + direction === "topLeft" || + direction === "bottomLeft" + ) { widget.x -= delta.width; } - if (direction === 'top' || direction === 'topLeft' || direction === 'topRight') { + if ( + direction === "top" || + direction === "topLeft" || + direction === "topRight" + ) { widget.y -= delta.height; } @@ -87,16 +95,15 @@ class WidgetContainer extends React.Component { e.preventDefault(); contextMenu.show({ event: e, - id: 'widgetMenu' + index, - }) + id: "widgetMenu" + index, + }); } render() { - const widget = this.props.widget; let contextMenu = ( ) + /> + ); - if ( !this.props.editing ){ + if (!this.props.editing) { const containerStyle = { width: Number(widget.width), height: Number(widget.height), left: Number(widget.x), top: Number(widget.y), zIndex: Number(widget.z), - position: 'absolute' + position: "absolute", }; - return
this.showMenu(e, this.props.index, this.props.editing)}> - {this.props.children} - {contextMenu} -
; + return ( +
+ this.showMenu(e, this.props.index, this.props.editing) + } + > + {this.props.children} + {contextMenu} +
+ ); } let resizingRestricted = false; - if (widget.customProperties.resizeLeftRightLock || widget.customProperties.resizeTopBottomLock) { + if ( + widget.customProperties.resizeLeftRightLock || + widget.customProperties.resizeTopBottomLock + ) { resizingRestricted = true; } @@ -136,40 +155,51 @@ class WidgetContainer extends React.Component { right: !(widget.customProperties.resizeLeftRightLock || widget.isLocked), top: !(widget.customProperties.resizeTopBottomLock || widget.isLocked), topLeft: !(resizingRestricted || widget.isLocked), - topRight: !(resizingRestricted || widget.isLocked) + topRight: !(resizingRestricted || widget.isLocked), }; const gridArray = [this.props.grid, this.props.grid]; const widgetClasses = classNames({ - 'editing-widget': true, - 'locked': widget.isLocked + "editing-widget": true, + locked: widget.isLocked, }); - return (
this.showMenu(e, this.props.index, this.props.editing)}> - { this.rnd = c; }} - size={{width: Number(widget.width), height: Number(widget.height)}} - position={{x: Number(widget.x), y: Number(widget.y)}} - minWidth={widget.minWidth} - minHeight={widget.minHeight} - lockAspectRatio={Boolean(widget.customProperties.lockAspect)} - bounds={'.toolbox-dropzone'} - className={widgetClasses} - onResizeStart={this.borderWasClicked} - onResizeStop={this.resizeStop} - onDragStop={this.dragStop} - dragGrid={gridArray} - resizeGrid={gridArray} - enableResizing={resizing} - disableDragging={widget.isLocked} + return ( +
+ this.showMenu(e, this.props.index, this.props.editing) + } > - {this.props.children} - + { + this.rnd = c; + }} + size={{ width: Number(widget.width), height: Number(widget.height) }} + position={{ x: Number(widget.x), y: Number(widget.y) }} + minWidth={widget.minWidth} + minHeight={widget.minHeight} + lockAspectRatio={Boolean(widget.customProperties.lockAspect)} + bounds={".toolbox-dropzone"} + className={widgetClasses} + onResizeStart={this.borderWasClicked} + onResizeStop={this.resizeStop} + onDragStop={this.dragStop} + dragGrid={gridArray} + resizeGrid={gridArray} + enableResizing={resizing} + disableDragging={widget.isLocked} + > + {this.props.children} + - {contextMenu} -
); + {contextMenu} +
+ ); } } @@ -182,4 +212,4 @@ WidgetContainer.propTypes = { editing: PropTypes.bool.isRequired, }; -export default WidgetContainer +export default WidgetContainer; diff --git a/src/pages/dashboards/widget/widget-factory.js b/src/pages/dashboards/widget/widget-factory.js deleted file mode 100644 index 213226ce..00000000 --- a/src/pages/dashboards/widget/widget-factory.js +++ /dev/null @@ -1,248 +0,0 @@ -/** - * 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 WidgetSlider from './widgets/slider'; - -class WidgetFactory { - - static createWidgetOfType(type, position) { - - let widget = { - name: 'Name', - type: type, - width: 100, - height: 100, - x: position.x, - y: position.y, - z: position.z, - locked: false, - customProperties: {}, - signalIDs: [], - }; - - // set type specific properties - switch(type) { - case 'CustomAction': - widget.customProperties.actions = [ - { - action: 'stop' - }, - { - action: 'pause', - model: { - url: 'ftp://user:pass@example.com/projectA/model.zip' - }, - parameters: { - timestep: 50e-6 - } - } - ]; - widget.name = 'Action'; - widget.customProperties.icon = 'star'; - widget.width = 100; - widget.height = 50; - break; - case 'Action': - break; - case 'Lamp': - widget.minWidth = 5; - widget.minHeight = 5; - widget.width = 20; - widget.height = 20; - widget.customProperties.on_color = '#4287f5'; - widget.customProperties.on_color_opacity = 1; - widget.customProperties.off_color = '#4287f5'; - widget.customProperties.off_color_opacity = 1; - widget.customProperties.threshold = 0.5; - break; - case 'Value': - widget.minWidth = 70; - widget.minHeight = 20; - widget.width = 110; - widget.height = 30; - widget.customProperties.textSize = 16; - widget.name = 'Value'; - widget.customProperties.showUnit = false; - widget.customProperties.resizeTopBottomLock = true; - widget.customProperties.showScalingFactor = true; - break; - case 'Plot': - widget.customProperties.ylabel = ''; - widget.customProperties.time = 60; - widget.minWidth = 400; - widget.minHeight = 200; - widget.width = 400; - widget.height = 200; - widget.customProperties.yMin = 0; - widget.customProperties.yMax = 10; - widget.customProperties.yUseMinMax = false; - widget.customProperties.lineColors = []; - widget.customProperties.showUnit = false; - widget.customProperties.mode = 'auto time-scrolling'; - widget.customProperties.nbrSamples = 100; - break; - case 'Table': - widget.minWidth = 200; - widget.width = 300; - widget.height = 200; - widget.customProperties.showUnit = false; - widget.customProperties.showScalingFactor = true; - break; - case 'Label': - widget.minWidth = 20; - widget.minHeight = 20; - widget.customProperties.maxWidth = 100; // TODO: this props is currently ignored! - widget.width = 100; - widget.height = 35; - widget.name = 'Label'; - widget.customProperties.textSize = 32; - widget.customProperties.fontColor = '#4287f5'; - widget.customProperties.fontColor_opacity = 1; - widget.customProperties.resizeTopBottomLock = true; - break; - case 'Image': - widget.minWidth = 20; - widget.minHeight = 20; - widget.width = 200; - widget.height = 200; - widget.customProperties.lockAspect = true; - widget.customProperties.file = -1; // ID of image file, -1 means non selected - widget.customProperties.update = false; - break; - case 'Button': - widget.minWidth = 100; - widget.minHeight = 50; - widget.width = 100; - widget.height = 100; - widget.customProperties.background_color = '#527984'; - widget.customProperties.font_color = '#4287f5'; - widget.customProperties.border_color = '#4287f5'; - widget.customProperties.background_color_opacity = 1; - widget.customProperties.on_value = 1; - widget.customProperties.off_value = 0; - widget.customProperties.toggle = false; - widget.customProperties.pressed = false; - widget.customProperties.simStartedSendValue = false; - break; - case 'NumberInput': - widget.minWidth = 150; - widget.minHeight = 50; - widget.width = 200; - widget.height = 50; - widget.customProperties.showUnit = false; - widget.customProperties.resizeTopBottomLock = true; - widget.customProperties.value = ''; - widget.customProperties.simStartedSendValue = false; - break; - // case 'Slider': - // widget.minWidth = 380; - // widget.minHeight = 30; - // widget.width = 400; - // widget.height = 50; - // widget.customProperties.orientation = WidgetSlider.OrientationTypes.HORIZONTAL.value; // Assign default orientation - // widget.customProperties.rangeMin = 0; - // widget.customProperties.rangeMax = 200; - // widget.customProperties.rangeUseMinMax = true; - // widget.customProperties.showUnit = false; - // widget.customProperties.continous_update = false; - // widget.customProperties.value = ''; - // widget.customProperties.resizeLeftRightLock = false; - // widget.customProperties.resizeTopBottomLock = true; - // widget.customProperties.simStartedSendValue = false; - - // break; - case 'Gauge': - widget.minWidth = 100; - widget.minHeight = 150; - widget.width = 150; - widget.height = 150; - widget.customProperties.colorZones = false; - widget.customProperties.zones = []; - widget.customProperties.valueMin = 0; - widget.customProperties.valueMax = 1; - widget.customProperties.valueUseMinMax = false; - widget.customProperties.lockAspect = true; - widget.customProperties.showScalingFactor = true; - break; - case 'Box': - widget.minWidth = 50; - widget.minHeight = 50; - widget.width = 100; - widget.height = 100; - widget.customProperties.border_color = '#4287f5'; - widget.customProperties.border_color_opacity = 1; - widget.customProperties.border_width = 2; - widget.customProperties.background_color = '#961520'; - widget.customProperties.background_color_opacity = 1; - widget.z = 0; - break; - /*case 'HTML': - widget.customProperties.content = 'Hello World'; - break;*/ - case 'Topology': - widget.width = 600; - widget.height = 400; - widget.customProperties.file = -1; // ID of file, -1 means non selected - break; - case 'Line': - widget.height = 26; - widget.width = 100; - widget.customProperties.border_color = '#4287f5'; - widget.customProperties.border_color_opacity = 1; - widget.customProperties.border_width = 2; - widget.customProperties.rotation = 0; - widget.customProperties.lockAspect = false; - widget.customProperties.resizeLeftRightLock = false; - widget.customProperties.resizeTopBottomLock = true; - break; - - case 'TimeOffset': - widget.minWidth = 200; - widget.minHeight = 80; - widget.width = 200; - widget.height = 80; - widget.customProperties.threshold_yellow = 1; - widget.customProperties.threshold_red = 2; - widget.customProperties.icID = -1; - widget.customProperties.horizontal = true; - widget.customProperties.showOffset = true; - widget.customProperties.lockAspect = true; - widget.customProperties.showName = true; - break; - - case 'Player': - widget.minWidth = 144; - widget.minHeight = 226; - widget.width = 400; - widget.height = 606; - widget.customProperties.configIDs = []; - widget.customProperties.uploadResults = false; - break; - - case 'ICstatus': - widget.customProperties.checkedIDs = [] - break; - - default: - widget.width = 100; - widget.height = 100; - } - return widget; - } -} - -export default WidgetFactory; diff --git a/src/pages/dashboards/widget/widget-factory.jsx b/src/pages/dashboards/widget/widget-factory.jsx new file mode 100644 index 00000000..7118161a --- /dev/null +++ b/src/pages/dashboards/widget/widget-factory.jsx @@ -0,0 +1,245 @@ +/** + * 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 . + ******************************************************************************/ + +const widgetsMap = { + CustomAction: { + name: "Action", + width: 100, + height: 50, + customProperties: { + actions: [ + { action: "stop" }, + { + action: "pause", + model: { url: "ftp://user:pass@example.com/projectA/model.zip" }, + parameters: { timestep: 50e-6 }, + }, + ], + icon: "star", + }, + }, + Lamp: { + minWidth: 5, + minHeight: 5, + width: 20, + height: 20, + customProperties: { + on_color: "#4287f5", + on_color_opacity: 1, + off_color: "#4287f5", + off_color_opacity: 1, + threshold: 0.5, + }, + }, + Value: { + name: "Value", + minWidth: 70, + minHeight: 20, + width: 110, + height: 30, + customProperties: { + textSize: 16, + showUnit: false, + resizeTopBottomLock: true, + showScalingFactor: true, + }, + }, + Plot: { + minWidth: 400, + minHeight: 200, + width: 400, + height: 200, + customProperties: { + ylabel: "", + time: 60, + yMin: 0, + yMax: 10, + yUseMinMax: false, + lineColors: [], + showUnit: false, + mode: "auto time-scrolling", + nbrSamples: 100, + }, + }, + Table: { + minWidth: 200, + width: 300, + height: 200, + customProperties: { + showUnit: false, + showScalingFactor: true, + }, + }, + Label: { + name: "Label", + minWidth: 20, + minHeight: 20, + width: 100, + height: 35, + customProperties: { + maxWidth: 100, // Currently ignored + textSize: 32, + fontColor: "#4287f5", + fontColor_opacity: 1, + resizeTopBottomLock: true, + }, + }, + Image: { + minWidth: 20, + minHeight: 20, + width: 200, + height: 200, + customProperties: { + lockAspect: true, + file: -1, // ID of image file, -1 means not selected + update: false, + }, + }, + Button: { + minWidth: 100, + minHeight: 50, + width: 100, + height: 100, + customProperties: { + background_color: "#527984", + font_color: "#4287f5", + border_color: "#4287f5", + background_color_opacity: 1, + on_value: 1, + off_value: 0, + toggle: false, + pressed: false, + simStartedSendValue: false, + }, + }, + NumberInput: { + minWidth: 150, + minHeight: 50, + width: 200, + height: 50, + customProperties: { + showUnit: false, + resizeTopBottomLock: true, + value: "", + simStartedSendValue: false, + }, + }, + Gauge: { + minWidth: 100, + minHeight: 150, + width: 150, + height: 150, + customProperties: { + colorZones: false, + zones: [], + valueMin: 0, + valueMax: 1, + valueUseMinMax: false, + lockAspect: true, + showScalingFactor: true, + }, + }, + Box: { + minWidth: 50, + minHeight: 50, + width: 100, + height: 100, + customProperties: { + border_color: "#4287f5", + border_color_opacity: 1, + border_width: 2, + background_color: "#961520", + background_color_opacity: 1, + }, + }, + Topology: { + width: 600, + height: 400, + customProperties: { + file: -1, // ID of file, -1 means not selected + }, + }, + Line: { + width: 100, + height: 26, + customProperties: { + border_color: "#4287f5", + border_color_opacity: 1, + border_width: 2, + rotation: 0, + lockAspect: false, + resizeLeftRightLock: false, + resizeTopBottomLock: true, + }, + }, + TimeOffset: { + minWidth: 200, + minHeight: 80, + width: 200, + height: 80, + customProperties: { + threshold_yellow: 1, + threshold_red: 2, + icID: -1, + horizontal: true, + showOffset: true, + lockAspect: true, + showName: true, + }, + }, + Player: { + minWidth: 144, + minHeight: 226, + width: 400, + height: 606, + customProperties: { + configIDs: [], + uploadResults: false, + }, + }, + ICstatus: { + customProperties: { + checkedIDs: [], + }, + }, +}; + +const defaultWidget = { + name: "Name", + width: 100, + height: 100, + x: 0, + y: 0, + z: 0, + locked: false, + customProperties: {}, + signalIDs: [], +}; + +const WidgetFactory = { + createWidgetOfType: (type, position) => { + const widget = { + ...defaultWidget, + type: type, + ...widgetsMap[type], + ...position, + }; + return widget; + }, +}; + +export default WidgetFactory; diff --git a/src/pages/dashboards/widget/widget-store.js b/src/pages/dashboards/widget/widget-store.js deleted file mode 100644 index d350691d..00000000 --- a/src/pages/dashboards/widget/widget-store.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * 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 ArrayStore from '../common/array-store'; -import WidgetsDataManager from './widgets-data-manager'; - -class WidgetStore extends ArrayStore { - constructor() { - super('widgets', WidgetsDataManager); - } - - reduce(state, action) { - switch (action.type) { - - case 'widgets/loaded': - - //WidgetsDataManager.loadFiles(action.token, action.data); - // TODO make sure files of scenario are loaded - return super.reduce(state, action); - - case 'widgets/signal-value-changed': - // update all widgets in widget store that use the current signal - - if (action.values.length === 0 || action.signalID === 0){ - console.log("WARNING: attempt to update widget signal value(s) on loopback, " + - "but no value(s) or invalid signal ID provided"); - return; - } - - state.forEach(function (widget, index) { - - if (!widget.hasOwnProperty("signalIDs")){ - return; - } - - for(let i = 0; i 1){ - // widget uses array of signals, save complete array - widget.customProperties.value = action.values - } else { - console.log("WARNING: attempt tp update widget signal value(s) on loopback, " + - "but incompatible values (type or length of array) provided"); - } - - } else { - console.log("WARNING: attempt tp update widget signal value(s) on loopback, " + - "but affected widget (ID=", widget.id, ") does not have customProperties.value field"); - } - } else { - console.log("WARNING: attempt tp update widget signal value(s) on loopback, " + - "but affected widget (ID=", widget.id, ") does not have customProperties field"); - } - } // if signal found - } // for - }) - - // explicit call to prevent array copy - this.__emitChange(); - - return state; - - default: - return super.reduce(state, action); - - } - } - -} - -export default new WidgetStore(); diff --git a/src/pages/dashboards/widget/widget.js b/src/pages/dashboards/widget/widget.js deleted file mode 100644 index d3c74c52..00000000 --- a/src/pages/dashboards/widget/widget.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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 WidgetButton from "./widgets/button"; - -const Widget = ({ widget, editing }) => { - const widgetTypeMap = { - Button: , - }; - - return widgetTypeMap["Button"]; -}; - -export default Widget; diff --git a/src/pages/dashboards/widget/widget-old.js b/src/pages/dashboards/widget/widget.jsx similarity index 82% rename from src/pages/dashboards/widget/widget-old.js rename to src/pages/dashboards/widget/widget.jsx index f29fe830..467723d9 100644 --- a/src/pages/dashboards/widget/widget-old.js +++ b/src/pages/dashboards/widget/widget.jsx @@ -29,14 +29,10 @@ import WidgetLamp from "./widgets/lamp.jsx"; import WidgetGauge from "./widgets/gauge.jsx"; import WidgetTimeOffset from "./widgets/time-offset.jsx"; import WidgetICstatus from "./widgets/icstatus.jsx"; -// import WidgetCustomAction from './widgets/custom-action'; -// import WidgetAction from './widgets/action'; import WidgetButton from "./widgets/button.jsx"; import WidgetInput from "./widgets/input.jsx"; import WidgetSlider from "./widgets/slider.jsx"; -// import WidgetTopology from './widgets/topology'; import WidgetPlayer from "./widgets/player.jsx"; -//import WidgetHTML from './widgets/html'; import "../../../styles/widgets.css"; import { useUpdateWidgetMutation } from "../../../store/apiSlice.js"; import { sendMessageToWebSocket } from "../../../store/websocketSlice.js"; @@ -146,16 +142,12 @@ const Widget = ({ } }; - if (widget.type === "Line") { - return ; - } else if (widget.type === "Box") { - return ; - } else if (widget.type === "Label") { - return ; - } else if (widget.type === "Image") { - return ; - } else if (widget.type === "Plot") { - return ( + const widgetMap = { + Line: , + Box: , + Label: , + Image: , + Plot: ( - ); - } else if (widget.type === "Table") { - return ( + ), + Table: ( - ); - } else if (widget.type === "Value") { - return ( + ), + Value: ( - ); - } else if (widget.type === "Lamp") { - return ( + ), + Lamp: ( - ); - } else if (widget.type === "Gauge") { - return ( + ), + Gauge: ( - ); - } else if (widget.type === "TimeOffset") { - return ( + ), + TimeOffset: ( - ); - } else if (widget.type === "ICstatus") { - return ; - } else if (widget.type === "Button") { - return ( + ), + ICstatus: , + Button: ( - ); - } else if (widget.type === "NumberInput") { - return ( + ), + NumberInput: ( - ); - } else if (widget.type === "Slider") { - return ( + ), + Slider: ( - ); - } else if (widget.type === "Player") { - return ( + ), + Player: ( - ); - } else { - console.log("Unknown widget type", widget.type); - return
Error: Widget not found!
; - } + ), + }; + + return widgetMap[widget.type] ||
Error: Widget not found!
; }; export default Widget; diff --git a/src/pages/dashboards/widget/widgets/button.jsx b/src/pages/dashboards/widget/widgets/button.jsx index 574f8274..4edd2e3d 100644 --- a/src/pages/dashboards/widget/widgets/button.jsx +++ b/src/pages/dashboards/widget/widgets/button.jsx @@ -17,75 +17,50 @@ import React, { useState, useEffect } from "react"; import { Button } from "react-bootstrap"; +import { useUpdateWidgetMutation } from "../../../../store/apiSlice"; const WidgetButton = ({ widget, editing }) => { const [pressed, setPressed] = useState(widget.customProperties.pressed); + const [updateWidget] = useUpdateWidgetMutation(); - // useEffect(() => { - // let widget = props.widget; - // widget.customProperties.simStartedSendValue = false; - // widget.customProperties.pressed = false; - - // // AppDispatcher.dispatch({ - // // type: 'widgets/start-edit', - // // token: props.token, - // // data: widget - // // }); - - // // Effect cleanup - // return () => { - // // Clean up if needed - // }; - // }, [props.token, props.widget]); - - // useEffect(() => { - // if (props.widget.customProperties.simStartedSendValue) { - // let widget = props.widget; - // widget.customProperties.simStartedSendValue = false; - // widget.customProperties.pressed = false; - // AppDispatcher.dispatch({ - // type: 'widgets/start-edit', - // token: props.token, - // data: widget - // }); - - // props.onInputChanged(widget.customProperties.off_value, '', false, false); - // } - // }, [props, setPressed]); - - // useEffect(() => { - // setPressed(props.widget.customProperties.pressed); - // }, [props.widget.customProperties.pressed]); - - // const onPress = (e) => { - // if (e.button === 0 && !props.widget.customProperties.toggle) { - // valueChanged(props.widget.customProperties.on_value, true); - // } - // }; - - // const onRelease = (e) => { - // if (e.button === 0) { - // let nextState = false; - // if (props.widget.customProperties.toggle) { - // nextState = !pressed; - // } - // valueChanged(nextState ? props.widget.customProperties.on_value : props.widget.customProperties.off_value, nextState); - // } - // }; + useEffect(() => { + updateSimStartedAndPressedValues(false, false); + }, [widget]); - // const valueChanged = (newValue, newPressed) => { - // if (props.onInputChanged) { - // props.onInputChanged(newValue, 'pressed', newPressed, true); - // } - // setPressed(newPressed); - // }; + const updateSimStartedAndPressedValues = async (isSimStarted, isPressed) => { + try { + await updateWidget({ + widgetID: widget.id, + updatedWidget: { + widget: { + ...widget, + customProperties: { + ...widget.customProperties, + simStartedSendValue: isSimStarted, + pressed: isPressed, + }, + }, + }, + }).unwrap(); + } catch (err) { + console.log("error", err); + } + }; useEffect(() => { - if (pressed) { - console.log("Yo"); + if (widget.customProperties.simStartedSendValue) { + let widget = widget; + widget.customProperties.simStartedSendValue = false; + widget.customProperties.pressed = false; + + onInputChanged(widget.customProperties.off_value, "", false, false); } }, [pressed]); + useEffect(() => { + setPressed(widget.customProperties.pressed); + }, [widget.customProperties.pressed]); + let opacity = widget.customProperties.background_color_opacity; const buttonStyle = { backgroundColor: widget.customProperties.background_color, diff --git a/src/pages/dashboards/widget/widgets/icstatus.jsx b/src/pages/dashboards/widget/widgets/icstatus.jsx index 94af01a6..d6cf677f 100644 --- a/src/pages/dashboards/widget/widgets/icstatus.jsx +++ b/src/pages/dashboards/widget/widgets/icstatus.jsx @@ -17,30 +17,36 @@ import React, { useState, useEffect } from "react"; import { Badge } from "react-bootstrap"; -import {stateLabelStyle} from "../../../infrastructure/styles"; +import { stateLabelStyle } from "../../../infrastructure/styles"; import { loadICbyId } from "../../../../store/icSlice"; import { sessionToken } from "../../../../localStorage"; import { useDispatch } from "react-redux"; +import { useLazyGetICbyIdQuery } from "../../../../store/apiSlice"; -let timer = null +let timer = null; const WidgetICstatus = (props) => { - const dispatch = useDispatch() - const [ics,setIcs] = useState(props.ics) - const refresh = async() => { + const dispatch = useDispatch(); + const [ics, setIcs] = useState(props.ics); + const [triggerGetICbyId] = useLazyGetICbyIdQuery(); + const refresh = async () => { if (props.ics) { - let iccs = []; - for(let ic of props.ics){ - const res = await dispatch(loadICbyId({id: ic.id, token:sessionToken})); - iccs.push(res.payload) + try { + const requests = props.ics.map((ic) => + triggerGetICbyId({ id: ic.id, token: sessionToken }).unwrap() + ); + + const results = await Promise.all(requests); + setIcs(results); + } catch (error) { + console.error("Error loading ICs:", error); } - setIcs(iccs) } }; useEffect(() => { - window.clearInterval(timer) - timer = window.setInterval(refresh,3000) + window.clearInterval(timer); + timer = window.setInterval(refresh, 3000); // Function to refresh data - refresh() + refresh(); // Cleanup function equivalent to componentWillUnmount return () => { window.clearInterval(timer); diff --git a/src/pages/dashboards/widget/widgets/input.jsx b/src/pages/dashboards/widget/widgets/input.jsx index 056ce75c..554c1eaf 100644 --- a/src/pages/dashboards/widget/widgets/input.jsx +++ b/src/pages/dashboards/widget/widgets/input.jsx @@ -16,19 +16,54 @@ ******************************************************************************/ import React, { useState, useEffect } from "react"; import { Form, Col, InputGroup } from "react-bootstrap"; +import { useUpdateWidgetMutation } from "../../../../store/apiSlice"; function WidgetInput(props) { const [value, setValue] = useState(""); const [unit, setUnit] = useState(""); + const [updateWidget] = useUpdateWidgetMutation(); + useEffect(() => { - let widget = { ...props.widget }; + const widget = { ...props.widget }; + widget.customProperties.simStartedSendValue = false; + + updateWidgetSimStatus(true); + }, [props.token, props.widget]); + + useEffect(() => { + if (props.widget.customProperties.simStartedSendValue) { + const widget = { ...props.widget }; widget.customProperties.simStartedSendValue = false; if(props.onInputChanged && props.signals && props.signals.length > 0){ props.onInputChanged(widget.customProperties.value, "", "", false); } }, [props.widget]); + updateWidgetSimStatus(false); + + props.onInputChanged(Number(value), "", "", false); + } + }, [props, value]); + + const updateWidgetSimStatus = async (isSimStarted) => { + try { + await updateWidget({ + widgetID: widget.id, + updatedWidget: { + widget: { + ...widget, + customProperties: { + ...widget.customProperties, + simStartedSendValue: isSimStarted, + }, + }, + }, + }).unwrap(); + } catch (err) { + console.log("error", err); + } + }; useEffect(() => { let newValue = ""; diff --git a/src/pages/dashboards/widget/widgets/player.jsx b/src/pages/dashboards/widget/widgets/player.jsx index 14e472ce..d257bb72 100644 --- a/src/pages/dashboards/widget/widgets/player.jsx +++ b/src/pages/dashboards/widget/widgets/player.jsx @@ -14,361 +14,432 @@ * You should have received a copy of the GNU General Public License * along with VILLASweb. If not, see . ******************************************************************************/ -import JSZip from 'jszip'; -import React, { useState, useEffect } from 'react'; -import { Container, Row, Col,Form } from 'react-bootstrap'; -import {sendActionToIC,loadICbyId} from "../../../../store/icSlice"; -import { sessionToken } from '../../../../localStorage'; -import IconButton from '../../../../common/buttons/icon-button'; -import IconTextButton from '../../../../common/buttons/icon-text-button'; -import ParametersEditor from '../../../../common/parameters-editor'; -import ResultPythonDialog from '../../../scenarios/dialogs/result-python-dialog'; -import { playerMachine } from '../widget-player/player-machine'; -import { interpret } from 'xstate'; -import { useAddResultMutation, useLazyDownloadFileQuery, useGetResultsQuery, useGetFilesQuery,useUpdateComponentConfigMutation } from '../../../../store/apiSlice'; -import notificationsDataManager from '../../../../common/data-managers/notifications-data-manager'; -import NotificationsFactory from '../../../../common/data-managers/notifications-factory'; -import { start } from 'xstate/lib/actions'; +import JSZip from "jszip"; +import React, { useState, useEffect } from "react"; +import { Container, Row, Col, Form } from "react-bootstrap"; +import { sendActionToIC, loadICbyId } from "../../../../store/icSlice"; +import { sessionToken } from "../../../../localStorage"; +import IconButton from "../../../../common/buttons/icon-button"; +import IconTextButton from "../../../../common/buttons/icon-text-button"; +import ParametersEditor from "../../../../common/parameters-editor"; +import ResultPythonDialog from "../../../scenarios/dialogs/result-python-dialog"; +import { playerMachine } from "../widget-player/player-machine"; +import { interpret } from "xstate"; +import { + useAddResultMutation, + useLazyDownloadFileQuery, + useGetResultsQuery, + useGetFilesQuery, + useUpdateComponentConfigMutation, +} from "../../../../store/apiSlice"; +import notificationsDataManager from "../../../../common/data-managers/notifications-data-manager"; +import NotificationsFactory from "../../../../common/data-managers/notifications-factory"; +import { start } from "xstate/lib/actions"; import FileSaver from "file-saver"; -import { useDispatch } from 'react-redux'; +import { useDispatch } from "react-redux"; -const WidgetPlayer = ( - {widget, editing, configs, onStarted, ics, results, files, scenarioID}) => { - const dispatch = useDispatch() - const zip = new JSZip() - const [triggerDownloadFile] = useLazyDownloadFileQuery(); - const {refetch: refetchResults} = useGetResultsQuery(scenarioID); - const [updateComponentConfig] = useUpdateComponentConfigMutation(); - const {refetch: refetchFiles} = useGetFilesQuery(scenarioID); - const [addResult, {isError: isErrorAddingResult}] = useAddResultMutation(); - const [playerState, setPlayerState] = useState(playerMachine.initialState); - const [configID, setConfigID] = useState(-1); - const [config, setConfig] = useState({}); - const [icState, setICState] = useState("unknown"); - const [startParameters, setStartParameters] = useState({}); - const [playerIC, setPlayerIC] = useState({name: ""}); - const [showPythonModal, setShowPythonModal] = useState(false); - const [showConfig, setShowConfig] = useState(false); - const [isUploadResultsChecked, setIsUploadResultsChecked] = useState(widget.customProperties.uploadResults); - const [resultArrayId, setResultArrayId] = useState(0); - const [filesToDownload, setFilesToDownload] = useState([]); - const [showWarning, setShowWarning] = useState(false); - const [warningText, setWarningText] = useState(""); - const [configBtnText, setConfigBtnText] = useState("Component Configuration"); +const WidgetPlayer = ({ + widget, + editing, + configs, + onStarted, + ics, + results, + files, + scenarioID, +}) => { + const dispatch = useDispatch(); + const zip = new JSZip(); + const [triggerDownloadFile] = useLazyDownloadFileQuery(); + const { refetch: refetchResults } = useGetResultsQuery(scenarioID); + const [updateComponentConfig] = useUpdateComponentConfigMutation(); + const { refetch: refetchFiles } = useGetFilesQuery(scenarioID); + const [addResult, { isError: isErrorAddingResult }] = useAddResultMutation(); + const [playerState, setPlayerState] = useState(playerMachine.initialState); + const [configID, setConfigID] = useState(-1); + const [config, setConfig] = useState({}); + const [icState, setICState] = useState("unknown"); + const [startParameters, setStartParameters] = useState({}); + const [playerIC, setPlayerIC] = useState({ name: "" }); + const [showPythonModal, setShowPythonModal] = useState(false); + const [showConfig, setShowConfig] = useState(false); + const [isUploadResultsChecked, setIsUploadResultsChecked] = useState( + widget.customProperties.uploadResults + ); + const [resultArrayId, setResultArrayId] = useState(0); + const [filesToDownload, setFilesToDownload] = useState([]); + const [showWarning, setShowWarning] = useState(false); + const [warningText, setWarningText] = useState(""); + const [configBtnText, setConfigBtnText] = useState("Component Configuration"); - const playerService = interpret(playerMachine); - playerService.start(); - useEffect(()=>{ - setIsUploadResultsChecked(widget.customProperties.uploadResults) - },[widget.customProperties.uploadResults]) + const playerService = interpret(playerMachine); + playerService.start(); + useEffect(() => { + setIsUploadResultsChecked(widget.customProperties.uploadResults); + }, [widget.customProperties.uploadResults]); - useEffect(()=>{ - if(playerIC.name.length !== 0){ - const refresh = async() => { - const res = await dispatch(loadICbyId({id: playerIC.id, token:sessionToken})); - setICState(res.payload.state) - } - const timer = window.setInterval(() => refresh(), 1000); - return () => { - window.clearInterval(timer); - }; - } - },[playerIC]) - - useEffect(() => { - if (typeof widget.customProperties.configID !== "undefined" - && configID !== widget.customProperties.configID) { - let configID = widget.customProperties.configID; - let config = configs.find(cfg => cfg.id === parseInt(configID, 10)); - if (config) { - let t_playeric = ics.find(ic => ic.id === parseInt(config.icID, 10)); - if (t_playeric) { - var afterCreateState = ''; - if (t_playeric.state === 'idle') { - afterCreateState = transitionState(playerState, 'ICIDLE'); - } else { - afterCreateState = transitionState(playerState, 'ICBUSY'); - } - setPlayerIC(t_playeric); - setConfigID(configID); - setPlayerState(afterCreateState); - setConfig(config); - setStartParameters(config.startParameters); - } + useEffect(() => { + if (playerIC.name.length !== 0) { + const refresh = async () => { + const res = await dispatch( + loadICbyId({ id: playerIC.id, token: sessionToken }) + ); + setICState(res.payload.state); + }; + const timer = window.setInterval(() => refresh(), 1000); + return () => { + window.clearInterval(timer); + }; + } + }, [playerIC]); + + useEffect(() => { + if ( + typeof widget.customProperties.configID !== "undefined" && + configID !== widget.customProperties.configID + ) { + let configID = widget.customProperties.configID; + let config = configs.find((cfg) => cfg.id === parseInt(configID, 10)); + if (config) { + let t_playeric = ics.find((ic) => ic.id === parseInt(config.icID, 10)); + if (t_playeric) { + var afterCreateState = ""; + if (t_playeric.state === "idle") { + afterCreateState = transitionState(playerState, "ICIDLE"); + } else { + afterCreateState = transitionState(playerState, "ICBUSY"); } + setPlayerIC(t_playeric); + setConfigID(configID); + setPlayerState(afterCreateState); + setConfig(config); + setStartParameters(config.startParameters); + } } - }, [configs,ics]); + } + }, [configs, ics]); - useEffect(() => { - if (results && results.length != resultArrayId) { - setResultArrayId(results.length - 1); - } - }, [results]); + useEffect(() => { + if (results && results.length != resultArrayId) { + setResultArrayId(results.length - 1); + } + }, [results]); - useEffect(() => { - var newState = ""; - switch (icState) { - case 'stopping': // if configured, show results - if (isUploadResultsChecked) { - refetchResults(); - refetchFiles().then(v=>{ - setFilesToDownload(v.data.files) - }); - } - newState = transitionState(playerState, 'FINISH') - setPlayerState(newState); - return { playerState: newState, icState: icState } - case 'idle': - newState = transitionState(playerState, 'ICIDLE') - setPlayerState(newState); - return { playerState: newState, icState: icState } - default: - if (icState === 'running') { - onStarted() - } - newState = transitionState(playerState, 'ICBUSY') - setPlayerState(newState); - return { playerState: newState, icState: icState } + useEffect(() => { + var newState = ""; + switch (icState) { + case "stopping": // if configured, show results + if (isUploadResultsChecked) { + refetchResults(); + refetchFiles().then((v) => { + setFilesToDownload(v.data.files); + }); } - }, [icState]); - - const transitionState = (currentState, playerEvent) => { - return playerMachine.transition(currentState, { type: playerEvent }) + newState = transitionState(playerState, "FINISH"); + setPlayerState(newState); + return { playerState: newState, icState: icState }; + case "idle": + newState = transitionState(playerState, "ICIDLE"); + setPlayerState(newState); + return { playerState: newState, icState: icState }; + default: + if (icState === "running") { + onStarted(); + } + newState = transitionState(playerState, "ICBUSY"); + setPlayerState(newState); + return { playerState: newState, icState: icState }; } + }, [icState]); - const clickStart = async () => { - try { - setPlayerState(transitionState(playerState, 'ICBUSY')); - let pld = {action:"start",when:Math.round((new Date()).getTime() / 1000),parameters:{...startParameters}} - if(isUploadResultsChecked){ - addResult({result: { - scenarioID: scenarioID - }}) - .then(v=>{ - pld.results = { - url: `https://slew.k8s.eonerc.rwth-aachen.de/api/v2/results/${v.data.result.id}/file`, - type: "url", - token: sessionToken - } - dispatch(sendActionToIC({token:sessionToken,id:config.icID,actions:[pld]})) - }) - .catch(e=>{ - notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(e.toString())); - }) - } - else{ - dispatch(sendActionToIC({token:sessionToken,id:config.icID,actions:[pld]})) - } - //sendAction({ icid: startConfig.icID, action: "start", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); - } catch(error) { - notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(error?.data?.message)); + const transitionState = (currentState, playerEvent) => { + return playerMachine.transition(currentState, { type: playerEvent }); + }; + + const clickStart = async () => { + try { + setPlayerState(transitionState(playerState, "ICBUSY")); + let pld = { + action: "start", + when: Math.round(new Date().getTime() / 1000), + parameters: { ...startParameters }, + }; + if (isUploadResultsChecked) { + addResult({ + result: { + scenarioID: scenarioID, + }, + }) + .then((v) => { + pld.results = { + url: `https://slew.k8s.eonerc.rwth-aachen.de/api/v2/results/${v.data.result.id}/file`, + type: "url", + token: sessionToken, + }; + dispatch( + sendActionToIC({ + token: sessionToken, + id: config.icID, + actions: [pld], + }) + ); + }) + .catch((e) => { + notificationsDataManager.addNotification( + NotificationsFactory.LOAD_ERROR(e.toString()) + ); + }); + } else { + dispatch( + sendActionToIC({ + token: sessionToken, + id: config.icID, + actions: [pld], + }) + ); } + //sendAction({ icid: startConfig.icID, action: "start", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); + } catch (error) { + notificationsDataManager.addNotification( + NotificationsFactory.LOAD_ERROR(error?.data?.message) + ); } + }; - const clickReset = async () => { - try { - dispatch(sendActionToIC({token:sessionToken,id:config.icID,actions:[{action:"reset",when:Math.round((new Date()).getTime() / 1000)}]})) - //sendAction({ icid: ic.id, action: "reset", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); - } catch(error) { - notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(error?.data?.message)); - console.log(error); - } - + const clickReset = async () => { + try { + dispatch( + sendActionToIC({ + token: sessionToken, + id: config.icID, + actions: [ + { action: "reset", when: Math.round(new Date().getTime() / 1000) }, + ], + }) + ); + //sendAction({ icid: ic.id, action: "reset", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); + } catch (error) { + notificationsDataManager.addNotification( + NotificationsFactory.LOAD_ERROR(error?.data?.message) + ); + console.log(error); } + }; - const updateStartParameters = async (data)=>{ - let copy = structuredClone(config) - copy.startParameters = data - if (copy.fileIDs === null){ - copy.fileIDs = [] - } - console.log(copy) - const newConf = {id: config.id, config: {config:copy}} - updateComponentConfig(newConf) - .then(v=>{ - setStartParameters(data) - notificationsDataManager.addNotification(NotificationsFactory.ACTION_INFO()); - }) - .catch((error)=>{ - notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(error?.data?.message)); - }) + const updateStartParameters = async (data) => { + let copy = structuredClone(config); + copy.startParameters = data; + if (copy.fileIDs === null) { + copy.fileIDs = []; } - const downloadResultFiles = () => { - if (results.length <= resultArrayId) { - setShowWarning(true); - setWarningText('no new result'); - return; - } + const newConf = { id: config.id, config: { config: copy } }; + updateComponentConfig(newConf) + .then((v) => { + setStartParameters(data); + notificationsDataManager.addNotification( + NotificationsFactory.ACTION_INFO() + ); + }) + .catch((error) => { + notificationsDataManager.addNotification( + NotificationsFactory.UPDATE_ERROR(error?.data?.message) + ); + }); + }; - const result = results[resultArrayId]; - const toDownload = result.resultFileIDs; + const downloadResultFiles = () => { + if (results.length <= resultArrayId) { + setShowWarning(true); + setWarningText("no new result"); + return; + } - if(toDownload.length <= 0){ - setShowWarning(true); - setWarningText('no result files'); - } else { - toDownload.forEach(fileID => handleDownloadFile(fileID)) - } + const result = results[resultArrayId]; + const toDownload = result.resultFileIDs; - setFilesToDownload(toDownload); + if (toDownload.length <= 0) { + setShowWarning(true); + setWarningText("no result files"); + } else { + toDownload.forEach((fileID) => handleDownloadFile(fileID)); } - const handleDownloadFile = async (fileID) => { - triggerDownloadFile(fileID) - .then(v=>{ - const file = filesToDownload.find(f => f.id === fileID); - const blob = new Blob([v.data], { type: 'application/octet-stream' }); + setFilesToDownload(toDownload); + }; + + const handleDownloadFile = async (fileID) => { + triggerDownloadFile(fileID) + .then((v) => { + const file = filesToDownload.find((f) => f.id === fileID); + const blob = new Blob([v.data], { type: "application/octet-stream" }); zip.file(file.name, blob); - zip.generateAsync({ type: 'blob' }) - .then((content) => { - FileSaver.saveAs(content, `result-${file.id}.zip`); - }) - .catch((err) => { - notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR('Failed to create ZIP archive')); - console.error('Failed to create ZIP archive', err); - }); + zip + .generateAsync({ type: "blob" }) + .then((content) => { + FileSaver.saveAs(content, `result-${file.id}.zip`); + }) + .catch((err) => { + notificationsDataManager.addNotification( + NotificationsFactory.UPDATE_ERROR("Failed to create ZIP archive") + ); + console.error("Failed to create ZIP archive", err); + }); }) - .catch(e=>{ - notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(e)); - }) - - } + .catch((e) => { + notificationsDataManager.addNotification( + NotificationsFactory.UPDATE_ERROR(e) + ); + }); + }; - const openPythonDialog = () => { - if (results.length <= resultArrayId) { - setShowWarning(true); - setWarningText('no new result'); - return; - } - - setShowPythonModal(true); + const openPythonDialog = () => { + if (results.length <= resultArrayId) { + setShowWarning(true); + setWarningText("no new result"); + return; } - const iconStyle = { - height: '20px', - width: '20px' - } + setShowPythonModal(true); + }; - let configButton = { - height: '70px', - width: '120px', - fontSize: '13px' - } + const iconStyle = { + height: "20px", + width: "20px", + }; - return ( -
-
- - - - - clickStart()} - icon='play' - disabled={!(playerState && playerState.matches('startable'))} - iconStyle={iconStyle} - tooltip='Start Component' - /> - - - - - clickReset()} - icon='undo' - iconStyle={iconStyle} - tooltip='Reset Component' - /> - - - + const configButton = { + height: "70px", + width: "120px", + fontSize: "13px", + }; - - - - setShowConfig(prevState => (!prevState))} - icon='cogs' - text={configBtnText + ' '} - buttonStyle={configButton} - disabled={false} - tooltip='Open/Close Component Configuration' - /> - - - - - - - setIsUploadResultsChecked(prevState => !prevState)} - /> - - - + return ( +
+
+ + + + + clickStart()} + icon="play" + disabled={!(playerState && playerState.matches("startable"))} + iconStyle={iconStyle} + tooltip="Start Component" + /> + + + + + clickReset()} + icon="undo" + iconStyle={iconStyle} + tooltip="Reset Component" + /> + + + - {isUploadResultsChecked ? -
- - - - openPythonDialog()} - icon={['fab', 'python']} - disabled={(playerState && playerState.matches('finished')&& isUploadResultsChecked) ? false : true} - iconStyle={iconStyle} - /> - - - - - downloadResultFiles()} - icon='file-download' - disabled={(playerState && playerState.matches('finished') ) ? false : true} - iconStyle={iconStyle} - /> - - - -
- : <> - } -
+ + + + setShowConfig((prevState) => !prevState)} + icon="cogs" + text={configBtnText + " "} + buttonStyle={configButton} + disabled={false} + tooltip="Open/Close Component Configuration" + /> + + + + + + + + setIsUploadResultsChecked((prevState) => !prevState) + } + /> + + + - -
- {showConfig && config ? -
- updateStartParameters(data)} - />
- : <> - } - {showWarning ? -

{warningText}

: <> - } - {isUploadResultsChecked ? - setShowPythonModal(false)} - /> : <> - } + {isUploadResultsChecked ? ( +
+ + + + openPythonDialog()} + icon={["fab", "python"]} + disabled={ + playerState && + playerState.matches("finished") && + isUploadResultsChecked + ? false + : true + } + iconStyle={iconStyle} + /> + + + + + downloadResultFiles()} + icon="file-download" + disabled={ + playerState && playerState.matches("finished") + ? false + : true + } + iconStyle={iconStyle} + /> + + + +
+ ) : ( + <> + )} +
- ); -} + {showConfig && config ? ( +
+ updateStartParameters(data)} + /> +
+ ) : ( + <> + )} + {showWarning ?

{warningText}

: <>} + {isUploadResultsChecked ? ( + setShowPythonModal(false)} + /> + ) : ( + <> + )} +
+ ); +}; export default WidgetPlayer; diff --git a/src/pages/dashboards/widget/widgets/slider.jsx b/src/pages/dashboards/widget/widgets/slider.jsx index f1314cfa..1ef384fa 100644 --- a/src/pages/dashboards/widget/widgets/slider.jsx +++ b/src/pages/dashboards/widget/widgets/slider.jsx @@ -20,19 +20,51 @@ import { format } from "d3"; import classNames from "classnames"; import Slider from "rc-slider"; import "rc-slider/assets/index.css"; +import { useUpdateWidgetMutation } from "../../../../store/apiSlice"; const WidgetSlider = (props) => { const [value, setValue] = useState(""); const [unit, setUnit] = useState(""); + const [updateWidget] = useUpdateWidgetMutation(); + useEffect(() => { let widget = { ...props.widget }; widget.customProperties.simStartedSendValue = false; - if(props.onInputChanged && props.signals && props.signals.length > 0){ + updateWidgetSimStatus(false); + }, [props.token, props.widget]); + + useEffect(() => { + // A simulation was started, make an update + if (props.widget.customProperties.simStartedSendValue) { + let widget = { ...props.widget }; + widget.customProperties.simStartedSendValue = false; + updateWidgetSimStatus(true); + + // Send value without changing widget props.onInputChanged(widget.customProperties.value, "", "", false); } }, [props.widget]); + const updateWidgetSimStatus = async (isSimStarted) => { + try { + await updateWidget({ + widgetID: props.widget.id, + updatedWidget: { + widget: { + ...props.widget, + customProperties: { + ...props.widget.customProperties, + simStartedSendValue: isSimStarted, + }, + }, + }, + }).unwrap(); + } catch (err) { + console.log("error", err); + } + }; + useEffect(() => { let newValue = ""; let newUnit = ""; diff --git a/src/store/apiSlice.js b/src/store/apiSlice.js index 45433be0..55698e8d 100644 --- a/src/store/apiSlice.js +++ b/src/store/apiSlice.js @@ -115,4 +115,5 @@ export const { useDeleteUserFromUsergroupMutation, useUpdateUsergroupMutation, useGetWidgetsQuery, + useLazyGetICbyIdQuery, } = apiSlice; diff --git a/src/store/endpoints/ic-endpoints.js b/src/store/endpoints/ic-endpoints.js index fdb2f0ae..438f93a9 100644 --- a/src/store/endpoints/ic-endpoints.js +++ b/src/store/endpoints/ic-endpoints.js @@ -16,14 +16,18 @@ ******************************************************************************/ export const icEndpoints = (builder) => ({ - getICS: builder.query({ - query: () => 'ic', - }), - sendAction: builder.mutation({ - query: (params) => ({ - url: `/ic/${params.icid}/action`, - method: 'POST', - body: [params], - }), + getICS: builder.query({ + query: () => "ic", + }), + sendAction: builder.mutation({ + query: (params) => ({ + url: `/ic/${params.icid}/action`, + method: "POST", + body: [params], }), + }), + + getICbyId: builder.query({ + query: (icID) => `/dashboards/${icID}`, + }), }); From 7ccf44b6b4b5affe13d37a714d9efde62e36497a Mon Sep 17 00:00:00 2001 From: Andrii Podriez Date: Wed, 9 Apr 2025 20:08:37 +0200 Subject: [PATCH 07/12] Fix wrong ic modal type/category filtration Signed-off-by: SystemsPurge --- src/pages/infrastructure/dialogs/new-ic-form-builder.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/infrastructure/dialogs/new-ic-form-builder.js b/src/pages/infrastructure/dialogs/new-ic-form-builder.js index 576a0ca6..7dade28a 100644 --- a/src/pages/infrastructure/dialogs/new-ic-form-builder.js +++ b/src/pages/infrastructure/dialogs/new-ic-form-builder.js @@ -97,7 +97,9 @@ const FormFromParameterSchema = forwardRef( const isRequired = createparameterschema.required?.includes(key); //filter the type and category fields as they are always in the parent form - if (key != "type" || key != "category") { + console.log("ADDING FIELD WITH KEY", key); + + if (key != "type" && key != "category") { //right now only text field and checkbox are supported. Text field is default return property.type != "boolean" ? ( From f8346d730e4424697287b73ef3d5e0c88fddcba2 Mon Sep 17 00:00:00 2001 From: Andrii Podriez Date: Tue, 3 Dec 2024 11:11:11 +0100 Subject: [PATCH 08/12] add error boundary to the dashboard refactor widgets and dashboard components Signed-off-by: Andrii Podriez Signed-off-by: SystemsPurge --- src/branding/branding.js | 12 - src/branding/kopernikus/img/Logo_BMBF.jpg | Bin 25769 -> 0 bytes .../kopernikus/img/kopernikus_logo.jpg | Bin 32825 -> 0 bytes .../kopernikus/kopernikus-functions.js | 71 -- .../dashboards/dashboard-error-boundry.js | 4 +- src/pages/dashboards/dashboard-layout.js | 37 ++ src/pages/dashboards/dashboard-old.js | 618 ++++++++++++++++++ src/pages/dashboards/dashboard.jsx | 577 +--------------- src/pages/dashboards/grid/widget-area.js | 2 +- src/pages/dashboards/widget/widget-old.js | 290 ++++++++ .../dashboards/widget/widget.js} | 35 +- .../dashboards/widget/widgets/button.jsx | 93 ++- src/store/apiSlice.js | 1 - 13 files changed, 1027 insertions(+), 713 deletions(-) delete mode 100644 src/branding/kopernikus/img/Logo_BMBF.jpg delete mode 100644 src/branding/kopernikus/img/kopernikus_logo.jpg delete mode 100644 src/branding/kopernikus/kopernikus-functions.js create mode 100644 src/pages/dashboards/dashboard-old.js create mode 100644 src/pages/dashboards/widget/widget-old.js rename src/{branding/kopernikus/kopernikus-values.js => pages/dashboards/widget/widget.js} (53%) diff --git a/src/branding/branding.js b/src/branding/branding.js index c7a61382..82cf8dc8 100644 --- a/src/branding/branding.js +++ b/src/branding/branding.js @@ -39,9 +39,6 @@ import { } from "./template/template-functions"; import template_values from "./template/template-values"; -import {kopernikus_home,kopernikus_welcome} from "./kopernikus/kopernikus-functions"; -import kopernikus_values from "./kopernikus/kopernikus-values"; - class Branding { constructor(brand) { this.brand = brand; @@ -67,9 +64,6 @@ class Branding { case "template": this.values = template_values; break; - case 'kopernikus': - this.values = kopernikus_values - break; default: console.error( "Branding '" + @@ -97,9 +91,6 @@ class Branding { case "template": homepage = template_home(); break; - case "kopernikus": - homepage = kopernikus_home(); - break; default: homepage = villasweb_home(this.getTitle(), username, userid, role); break; @@ -141,9 +132,6 @@ class Branding { case "template": welcome = template_welcome(); break; - case "kopernikus": - welcome = kopernikus_welcome(); - break; default: welcome = this.defaultWelcome(); break; diff --git a/src/branding/kopernikus/img/Logo_BMBF.jpg b/src/branding/kopernikus/img/Logo_BMBF.jpg deleted file mode 100644 index e164f3a3aaa3c3b21c8c69a23d22b417145be30a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25769 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 F|1a+cGUxyR diff --git a/src/branding/kopernikus/img/kopernikus_logo.jpg b/src/branding/kopernikus/img/kopernikus_logo.jpg deleted file mode 100644 index 9739ae46e6852b22bc96fc67b09f374e4194bfa5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32825 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 DQyp+% diff --git a/src/branding/kopernikus/kopernikus-functions.js b/src/branding/kopernikus/kopernikus-functions.js deleted file mode 100644 index 7ae97d0c..00000000 --- a/src/branding/kopernikus/kopernikus-functions.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * 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"; - -export function kopernikus_home() { - return ( -
-

Home

-

- Welcome to the Kopernikus Projects - ENSURE! -

-

- Rethinking the energy system for Germany and finding answers to the - questions of the energy transition: This is the great challenge that the - Kopernikus projects are facing. Find out more about the four main - pillars on these pages. The Kopernikus projects are making a significant - contribution to ensuring that Germany can achieve its climate targets by - 2045. -

- -

Contacts

- - -

Credits

-
- Logo BMBF -
-
- ); -} - -export function kopernikus_welcome() { - return ( -
-

Welcome!

-

- SLEW is a learning platform for running experiments in a virtual power - engineering world. The platform enables to interact with the experiments - in real time and perform analyses on the experimental results. -

-
- ); -} \ No newline at end of file diff --git a/src/pages/dashboards/dashboard-error-boundry.js b/src/pages/dashboards/dashboard-error-boundry.js index 57caf2b9..42e881ac 100644 --- a/src/pages/dashboards/dashboard-error-boundry.js +++ b/src/pages/dashboards/dashboard-error-boundry.js @@ -1,6 +1,6 @@ import React from "react"; -class DashboardErrorBoundary extends React.Component { +class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; @@ -27,4 +27,4 @@ class DashboardErrorBoundary extends React.Component { } } -export default DashboardErrorBoundary; +export default ErrorBoundary; diff --git a/src/pages/dashboards/dashboard-layout.js b/src/pages/dashboards/dashboard-layout.js index 823ec030..69e5a2c5 100644 --- a/src/pages/dashboards/dashboard-layout.js +++ b/src/pages/dashboards/dashboard-layout.js @@ -83,11 +83,48 @@ const DashboardLayout = ({ isFullscreen, toggleFullscreen }) => { } }, [widgets]); + const fetchWidgetData = async (scenarioID) => { + try { + const configsRes = await triggerGetConfigs(scenarioID).unwrap(); + if (configsRes.configs) { + setConfigs(configsRes.configs); + //load signals if there are any configs + + if (configsRes.configs.length > 0) { + for (const config of configsRes.configs) { + const signalsInRes = await triggerGetSignals({ + configID: config.id, + direction: "in", + }).unwrap(); + const signalsOutRes = await triggerGetSignals({ + configID: config.id, + direction: "out", + }).unwrap(); + setSignals((prevState) => [ + ...signalsInRes.signals, + ...signalsOutRes.signals, + ...prevState, + ]); + } + } + } + } catch (err) { + console.log("error fetching data", err); + } + }; + const [gridParameters, setGridParameters] = useState({ height: 10, grid: 50, }); + useEffect(() => { + if (!isFetchingDashboard) { + setGridParameters({ height: dashboard.height, grid: dashboard.grid }); + fetchWidgetData(dashboard.scenarioID); + } + }, [isFetchingDashboard]); + const boxClasses = classNames("section", "box", { "fullscreen-padding": isFullscreen, }); diff --git a/src/pages/dashboards/dashboard-old.js b/src/pages/dashboards/dashboard-old.js new file mode 100644 index 00000000..50910304 --- /dev/null +++ b/src/pages/dashboards/dashboard-old.js @@ -0,0 +1,618 @@ +/** + * 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, { useState, useEffect, useCallback, useRef, act } from "react"; +import { useDispatch, useSelector } from "react-redux"; +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 EditSignalMappingDialog from "../scenarios/dialogs/edit-signal-mapping"; +import WidgetToolbox from "./widget/widget-toolbox"; +import WidgetArea from "./grid/widget-area"; +import DashboardButtonGroup from "./grid/dashboard-button-group"; +import IconToggleButton from "../../common/buttons/icon-toggle-button"; +import WidgetContainer from "./widget/widget-container"; +import Widget from "./widget/widget-old"; + +import { connectWebSocket, disconnect } from "../../store/websocketSlice"; + +import { + useGetDashboardQuery, + useLazyGetWidgetsQuery, + useLazyGetConfigsQuery, + useAddWidgetMutation, + useUpdateWidgetMutation, + useDeleteWidgetMutation, + useLazyGetFilesQuery, + useUpdateDashboardMutation, + useGetICSQuery, + useLazyGetSignalsQuery, +} from "../../store/apiSlice"; + +const startUpdaterWidgets = new Set(["Slider", "Button", "NumberInput"]); + +const Dashboard = ({ isFullscreen, toggleFullscreen }) => { + const dispatch = useDispatch(); + const params = useParams(); + const { + data: dashboardRes, + error: dashboardError, + isLoading: isDashboardLoading, + } = useGetDashboardQuery(params.dashboard); + const dashboard = dashboardRes ? dashboardRes.dashboard : {}; + const { data: icsRes } = useGetICSQuery(); + const ics = icsRes ? icsRes.ics : []; + + const [triggerGetWidgets] = useLazyGetWidgetsQuery(); + const [triggerGetConfigs] = useLazyGetConfigsQuery(); + const [triggerGetFiles] = useLazyGetFilesQuery(); + const [triggerGetSignals] = useLazyGetSignalsQuery(); + const [addWidget] = useAddWidgetMutation(); + const [updateWidget] = useUpdateWidgetMutation(); + const [deleteWidgetMutation] = useDeleteWidgetMutation(); + const [updateDashboard] = useUpdateDashboardMutation(); + + const [widgets, setWidgets] = useState([]); + const [widgetsToUpdate, setWidgetsToUpdate] = useState([]); + const [configs, setConfigs] = useState([]); + const [signals, setSignals] = useState([]); + const [sessionToken, setSessionToken] = useState( + localStorage.getItem("token") + ); + const [files, setFiles] = 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); + + const [height, setHeight] = useState(10); + const [grid, setGrid] = useState(50); + const [newHeightValue, setNewHeightValue] = useState(0); + + //ics that are included in configurations + const [activeICS, setActiveICS] = useState([]); + + useEffect(() => { + let usedICS = []; + for (const config of configs) { + usedICS.push(config.icID); + } + setActiveICS(ics.filter((i) => usedICS.includes(i.id))); + }, [configs]); + + const activeSocketURLs = useSelector( + (state) => state.websocket.activeSocketURLs + ); + + //connect to websockets + useEffect(() => { + activeICS.forEach((i) => { + if (i.websocketurl) { + if (!activeSocketURLs.includes(i.websocketurl)) + dispatch(connectWebSocket({ url: i.websocketurl, id: i.id })); + } + }); + + return () => { + activeICS.forEach((i) => { + dispatch(disconnect({ id: i.id })); + }); + }; + }, [activeICS]); + + //as soon as dashboard is loaded, load widgets, configs, signals and files for this dashboard + useEffect(() => { + if (dashboard.id) { + fetchWidgets(dashboard.id); + fetchWidgetData(dashboard.scenarioID); + setHeight(dashboard.height); + setGrid(dashboard.grid); + } + }, [dashboard]); + + const fetchWidgets = async (dashboardID) => { + try { + const widgetsRes = await triggerGetWidgets(dashboardID).unwrap(); + if (widgetsRes.widgets) { + setWidgets(widgetsRes.widgets); + } + } catch (err) { + console.log("error fetching data", err); + } + }; + + const fetchWidgetData = async (scenarioID) => { + try { + const filesRes = await triggerGetFiles(scenarioID).unwrap(); + if (filesRes.files) { + setFiles(filesRes.files); + } + const configsRes = await triggerGetConfigs(scenarioID).unwrap(); + if (configsRes.configs) { + setConfigs(configsRes.configs); + //load signals if there are any configs + + if (configsRes.configs.length > 0) { + for (const config of configsRes.configs) { + const signalsInRes = await triggerGetSignals({ + configID: config.id, + direction: "in", + }).unwrap(); + const signalsOutRes = await triggerGetSignals({ + configID: config.id, + direction: "out", + }).unwrap(); + setSignals((prevState) => [ + ...signalsInRes.signals, + ...signalsOutRes.signals, + ...prevState, + ]); + } + } + } + } catch (err) { + console.log("error fetching data", 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) => { + setWidgetsToUpdate((prevWidgetsToUpdate) => [ + ...prevWidgetsToUpdate, + widget.id, + ]); + setWidgets((prevWidgets) => + prevWidgets.map((w) => (w.id === widget.id ? { ...widget } : w)) + ); + + // 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: 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 = async () => { + // 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); + // } + // }); + + if (height !== dashboard.height || grid !== dashboard.grid) { + try { + const { height: oldHeight, grid: oldGrid, ...rest } = dashboard; + await updateDashboard({ + dashboardID: dashboard.id, + dashboard: { height: height, grid: grid, ...rest }, + }).unwrap(); + } catch (err) { + console.log("error", err); + } + } + + if (widgetsToUpdate.length > 0) { + try { + for (const index in widgetsToUpdate) { + await updateWidget({ + widgetID: widgetsToUpdate[index], + updatedWidget: { + widget: { + ...widgets.find((w) => w.id == widgetsToUpdate[index]), + }, + }, + }).unwrap(); + } + fetchWidgets(dashboard.id); + } 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); + // } + // } + // }); + fetchWidgets(dashboard.id); + setEditing(false); + setWidgetChangeData([]); + setHeight(dashboard.height); + setGrid(dashboard.grid); + }; + + const updateGrid = (value) => { + setGrid(value); + }; + + const updateHeight = (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) { + setHeight((prevState) => prevState - 50); + } + } else { + setHeight((prevState) => prevState + 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 boxClasses = classNames("section", "box", { + "fullscreen-padding": isFullscreen, + }); + + 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} + > + onSimulationStarted()} + /> + +
+ ))} +
+ + + + {/* */} + + +
+
+ ); +}; + +export default Fullscreenable()(Dashboard); diff --git a/src/pages/dashboards/dashboard.jsx b/src/pages/dashboards/dashboard.jsx index 9f424717..757153e5 100644 --- a/src/pages/dashboards/dashboard.jsx +++ b/src/pages/dashboards/dashboard.jsx @@ -15,590 +15,33 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import React, { useState, useEffect, useCallback, useRef, act } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; -import Fullscreenable from "react-fullscreenable"; -import "react-contexify/dist/ReactContexify.min.css"; -import EditWidget from "./widget/edit-widget/edit-widget"; -import EditSignalMappingDialog from "../scenarios/dialogs/edit-signal-mapping"; -import WidgetToolbox from "./widget/widget-toolbox"; -import WidgetArea from "./grid/widget-area"; -import DashboardButtonGroup from "./grid/dashboard-button-group"; -import IconToggleButton from "../../common/buttons/icon-toggle-button"; -import WidgetContainer from "./widget/widget-container"; -import Widget from "./widget/widget.jsx"; - -import { connectWebSocket, disconnect } from "../../store/websocketSlice"; - import { useGetDashboardQuery, + useGetICSQuery, useLazyGetWidgetsQuery, useLazyGetConfigsQuery, - useAddWidgetMutation, - useUpdateWidgetMutation, - useDeleteWidgetMutation, useLazyGetFilesQuery, - useUpdateDashboardMutation, - useGetICSQuery, useLazyGetSignalsQuery, } from "../../store/apiSlice"; -import { Spinner } from "react-bootstrap"; +import { useState } from "react"; +import DashboardLayout from "./dashboard-layout"; +import ErrorBoundary from "./dashboard-error-boundry"; -const startUpdaterWidgets = new Set(["Slider", "Button", "NumberInput"]); - -const Dashboard = ({ isFullscreen, toggleFullscreen }) => { - const dispatch = useDispatch(); - const { token: sessionToken } = useSelector((state) => state.auth); - const params = useParams(); - const { - data: dashboardRes, - error: dashboardError, - isLoading: isDashboardLoading, - } = useGetDashboardQuery(params.dashboard); - const dashboard = dashboardRes ? dashboardRes.dashboard : {}; - const { data: icsRes } = useGetICSQuery(); - const ics = icsRes ? icsRes.ics : []; - - const [triggerGetWidgets] = useLazyGetWidgetsQuery(); +const Dashboard = ({}) => { + const { data: { ics } = [] } = useGetICSQuery(); const [triggerGetConfigs] = useLazyGetConfigsQuery(); const [triggerGetFiles] = useLazyGetFilesQuery(); const [triggerGetSignals] = useLazyGetSignalsQuery(); - const [addWidget] = useAddWidgetMutation(); - const [updateWidget] = useUpdateWidgetMutation(); - const [deleteWidgetMutation] = useDeleteWidgetMutation(); - const [updateDashboard] = useUpdateDashboardMutation(); - const [widgets, setWidgets] = useState([]); - const [widgetsToUpdate, setWidgetsToUpdate] = useState([]); - const [configs, setConfigs] = useState([]); - const [signals, setSignals] = useState([]); - const [files, setFiles] = 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 [widgetOrigIDs, setWidgetOrigIDs] = useState([]); const [locked, setLocked] = useState(false); - const [height, setHeight] = useState(10); - const [grid, setGrid] = useState(50); - - //ics that are included in configurations - const [activeICS, setActiveICS] = useState([]); - - useEffect(() => { - let usedICS = []; - for (const config of configs) { - usedICS.push(config.icID); - } - setActiveICS(ics.filter((i) => usedICS.includes(i.id))); - }, [configs]); - - const activeSocketURLs = useSelector( - (state) => state.websocket.activeSocketURLs - ); - - //connect to websockets - useEffect(() => { - activeICS.forEach((i) => { - if (i.websocketurl) { - if (!activeSocketURLs.includes(i.websocketurl)) - dispatch(connectWebSocket({ url: i.websocketurl, id: i.id })); - } - }); - - return () => { - activeICS.forEach((i) => { - dispatch(disconnect({ id: i.id })); - }); - }; - }, [activeICS]); - - //as soon as dashboard is loaded, load widgets, configs, signals and files for this dashboard - useEffect(() => { - if (dashboard.id) { - fetchWidgets(dashboard.id); - fetchWidgetData(dashboard.scenarioID); - setHeight(dashboard.height); - setGrid(dashboard.grid); - } - }, [dashboard]); - - const fetchWidgets = async (dashboardID) => { - try { - const widgetsRes = await triggerGetWidgets(dashboardID).unwrap(); - if (widgetsRes.widgets) { - setWidgets(widgetsRes.widgets); - } - } catch (err) { - console.log("error fetching data", err); - } - }; - - const fetchWidgetData = async (scenarioID) => { - try { - const filesRes = await triggerGetFiles(scenarioID).unwrap(); - if (filesRes.files) { - setFiles(filesRes.files); - } - const configsRes = await triggerGetConfigs(scenarioID).unwrap(); - if (configsRes.configs) { - setConfigs(configsRes.configs); - //load signals if there are any configs - - if (configsRes.configs.length > 0) { - for (const config of configsRes.configs) { - const signalsInRes = await triggerGetSignals({ - configID: config.id, - direction: "in", - }).unwrap(); - const signalsOutRes = await triggerGetSignals({ - configID: config.id, - direction: "out", - }).unwrap(); - setSignals((prevState) => [ - ...signalsInRes.signals, - ...signalsOutRes.signals, - ...prevState, - ]); - } - } - } - } catch (err) { - console.log("error fetching data", 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) => { - setWidgetsToUpdate((prevWidgetsToUpdate) => [ - ...prevWidgetsToUpdate, - widget.id, - ]); - setWidgets((prevWidgets) => - prevWidgets.map((w) => (w.id === widget.id ? { ...widget } : w)) - ); - - 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: 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 = async () => { - 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); - } - }); - - if (height !== dashboard.height || grid !== dashboard.grid) { - try { - const { height: oldHeight, grid: oldGrid, ...rest } = dashboard; - await updateDashboard({ - dashboardID: dashboard.id, - dashboard: { height: height, grid: grid, ...rest }, - }).unwrap(); - } catch (err) { - console.log("error", err); - } - } - - if (widgetsToUpdate.length > 0) { - try { - for (const index in widgetsToUpdate) { - await updateWidget({ - widgetID: widgetsToUpdate[index], - updatedWidget: { - widget: { - ...widgets.find((w) => w.id == widgetsToUpdate[index]), - }, - }, - }).unwrap(); - } - fetchWidgets(dashboard.id); - } catch (err) { - console.log("error", err); - } - } - - setEditing(false); - }; - - 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); - } - } - }); - fetchWidgets(dashboard.id); - setEditing(false); - setHeight(dashboard.height); - setGrid(dashboard.grid); - }; - - const updateGrid = (value) => { - setGrid(value); - }; - - const updateHeight = (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) { - setHeight((prevState) => prevState - 50); - } - } else { - setHeight((prevState) => prevState + 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); - } - }; - - if (isDashboardLoading) { - return ; - } - - if (dashboardError) { - return
Error. Dashboard not found
; - } - return ( -
-
-
-

- {dashboard.name} - - setLocked(!locked)} - buttonStyle={{ marginLeft: "10px" }} - iconStyle={{ height: "25px", width: "25px" }} - /> - -

-
- - -
- -
e.preventDefault()} - > - {editing && ( - - )} - - - {widgets != null && - Object.keys(widgets).map((widgetKey) => ( -
- deleteWidget(widget.id)} - onChange={editing ? widgetChange : onChange} - > - onSimulationStarted()} - /> - -
- ))} -
- - - - -
-
+ + + ); }; -export default Fullscreenable()(Dashboard); +export default Dashboard; diff --git a/src/pages/dashboards/grid/widget-area.js b/src/pages/dashboards/grid/widget-area.js index 3ba51455..63a58910 100644 --- a/src/pages/dashboards/grid/widget-area.js +++ b/src/pages/dashboards/grid/widget-area.js @@ -19,7 +19,7 @@ import React from "react"; import PropTypes from "prop-types"; import Dropzone from "./dropzone"; import Grid from "./grid"; -import WidgetFactory from "../widget/widget-factory.jsx"; +import WidgetFactory from "../widget/widget-factory"; class WidgetArea extends React.Component { snapToGrid(value) { diff --git a/src/pages/dashboards/widget/widget-old.js b/src/pages/dashboards/widget/widget-old.js new file mode 100644 index 00000000..f29fe830 --- /dev/null +++ b/src/pages/dashboards/widget/widget-old.js @@ -0,0 +1,290 @@ +/** + * 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 { useState, useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import WidgetLabel from "./widgets/label.jsx"; +import WidgetLine from "./widgets/line.jsx"; +import WidgetBox from "./widgets/box.jsx"; +import WidgetImage from "./widgets/image.jsx"; +import WidgetPlot from "./widgets/plot.jsx"; +import WidgetTable from "./widgets/table.jsx"; +import WidgetValue from "./widgets/value.jsx"; +import WidgetLamp from "./widgets/lamp.jsx"; +import WidgetGauge from "./widgets/gauge.jsx"; +import WidgetTimeOffset from "./widgets/time-offset.jsx"; +import WidgetICstatus from "./widgets/icstatus.jsx"; +// import WidgetCustomAction from './widgets/custom-action'; +// import WidgetAction from './widgets/action'; +import WidgetButton from "./widgets/button.jsx"; +import WidgetInput from "./widgets/input.jsx"; +import WidgetSlider from "./widgets/slider.jsx"; +// import WidgetTopology from './widgets/topology'; +import WidgetPlayer from "./widgets/player.jsx"; +//import WidgetHTML from './widgets/html'; +import "../../../styles/widgets.css"; +import { useUpdateWidgetMutation } from "../../../store/apiSlice.js"; +import { sendMessageToWebSocket } from "../../../store/websocketSlice.js"; +import { useGetResultsQuery } from "../../../store/apiSlice.js"; + +const Widget = ({ + widget, + editing, + files, + configs, + signals, + paused, + ics, + scenarioID, + onSimulationStarted, +}) => { + const dispatch = useDispatch(); + const { token: sessionToken } = useSelector((state) => state.auth); + const { data, refetch: refetchResults } = useGetResultsQuery(scenarioID); + const results = data ? data.results : []; + const [icIDs, setICIDs] = useState([]); + + const icdata = useSelector((state) => state.websocket.icdata); + + const [websockets, setWebsockets] = useState([]); + const activeSocketURLs = useSelector( + (state) => state.websocket.activeSocketURLs + ); + const [update] = useUpdateWidgetMutation(); + + useEffect(() => { + if (activeSocketURLs.length > 0) { + activeSocketURLs.forEach((url) => { + setWebsockets((prevState) => [ + ...prevState, + { url: url.replace(/^wss:\/\//, "https://"), connected: true }, + ]); + }); + } + }, [activeSocketURLs]); + + useEffect(() => { + if (signals.length > 0) { + let ids = []; + + for (let id of widget.signalIDs) { + let signal = signals.find((s) => s.id === id); + if (signal !== undefined) { + let config = configs.find((m) => m.id === signal.configID); + if (config !== undefined) { + ids[signal.id] = config.icID; + } + } + } + + setICIDs(ids); + } + }, [signals]); + + const inputDataChanged = ( + widget, + data, + controlID, + controlValue, + isFinalChange + ) => { + if (controlID !== "" && isFinalChange) { + let updatedWidget = JSON.parse(JSON.stringify(widget)); + updatedWidget.customProperties[controlID] = controlValue; + + updateWidget(updatedWidget); + } + + let signalID = widget.signalIDs[0]; + let signal = signals.filter((s) => s.id === signalID); + if (signal.length === 0) { + console.warn( + "Unable to send signal for signal ID", + signalID, + ". Signal not found." + ); + return; + } + // determine ID of infrastructure component related to signal[0] + // Remark: there is only one selected signal for an input type widget + let icID = icIDs[signal[0].id]; + dispatch( + sendMessageToWebSocket({ + message: { + ic: icID, + signalID: signal[0].id, + signalIndex: signal[0].index, + data: signal[0].scalingFactor * data, + }, + }) + ); + }; + + const updateWidget = async (updatedWidget) => { + try { + await update({ + widgetID: widget.id, + updatedWidget: { widget: updatedWidget }, + }).unwrap(); + } catch (err) { + console.log("error", err); + } + }; + + if (widget.type === "Line") { + return ; + } else if (widget.type === "Box") { + return ; + } else if (widget.type === "Label") { + return ; + } else if (widget.type === "Image") { + return ; + } else if (widget.type === "Plot") { + return ( + + ); + } else if (widget.type === "Table") { + return ( + + ); + } else if (widget.type === "Value") { + return ( + + ); + } else if (widget.type === "Lamp") { + return ( + + ); + } else if (widget.type === "Gauge") { + return ( + + ); + } else if (widget.type === "TimeOffset") { + return ( + + ); + } else if (widget.type === "ICstatus") { + return ; + } else if (widget.type === "Button") { + return ( + + inputDataChanged( + widget, + value, + controlID, + controlValue, + isFinalChange + ) + } + signals={signals} + token={sessionToken} + /> + ); + } else if (widget.type === "NumberInput") { + return ( + + inputDataChanged( + widget, + value, + controlID, + controlValue, + isFinalChange + ) + } + signals={signals} + token={sessionToken} + /> + ); + } else if (widget.type === "Slider") { + return ( + + inputDataChanged( + widget, + value, + controlID, + controlValue, + isFinalChange + ) + } + signals={signals} + token={sessionToken} + /> + ); + } else if (widget.type === "Player") { + return ( + + ); + } else { + console.log("Unknown widget type", widget.type); + return
Error: Widget not found!
; + } +}; + +export default Widget; diff --git a/src/branding/kopernikus/kopernikus-values.js b/src/pages/dashboards/widget/widget.js similarity index 53% rename from src/branding/kopernikus/kopernikus-values.js rename to src/pages/dashboards/widget/widget.js index d24dab7b..d3c74c52 100644 --- a/src/branding/kopernikus/kopernikus-values.js +++ b/src/pages/dashboards/widget/widget.js @@ -15,29 +15,14 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -const kopernikus_values = { - title: "Kopernikus Projekte", - subtitle: "ENSURE", - logo: "kopernikus_logo.jpg", - pages: { - home: true, - scenarios: true, - infrastructure: false, - account: false, - api: false, - }, - links: { - "DPsim Simulator": "https://dpsim.fein-aachen.org", - VILLASframework: "https://villas.fein-aachen.org/doc", - }, - style: { - background: "rgba(189, 195, 199 , 1)", - highlights: "rgba(2,36,97, 0.75)", - main: "rgba(80,80,80, 1)", - secondaryText: "rgba(80,80,80, 0.9)", - fontFamily: "Roboto, sans-serif", - borderRadius: "10px", - }, +import WidgetButton from "./widgets/button"; + +const Widget = ({ widget, editing }) => { + const widgetTypeMap = { + Button: , }; - - export default kopernikus_values; \ No newline at end of file + + return widgetTypeMap["Button"]; +}; + +export default Widget; diff --git a/src/pages/dashboards/widget/widgets/button.jsx b/src/pages/dashboards/widget/widgets/button.jsx index 4edd2e3d..574f8274 100644 --- a/src/pages/dashboards/widget/widgets/button.jsx +++ b/src/pages/dashboards/widget/widgets/button.jsx @@ -17,49 +17,74 @@ import React, { useState, useEffect } from "react"; import { Button } from "react-bootstrap"; -import { useUpdateWidgetMutation } from "../../../../store/apiSlice"; const WidgetButton = ({ widget, editing }) => { const [pressed, setPressed] = useState(widget.customProperties.pressed); - const [updateWidget] = useUpdateWidgetMutation(); - useEffect(() => { - updateSimStartedAndPressedValues(false, false); - }, [widget]); + // useEffect(() => { + // let widget = props.widget; + // widget.customProperties.simStartedSendValue = false; + // widget.customProperties.pressed = false; - const updateSimStartedAndPressedValues = async (isSimStarted, isPressed) => { - try { - await updateWidget({ - widgetID: widget.id, - updatedWidget: { - widget: { - ...widget, - customProperties: { - ...widget.customProperties, - simStartedSendValue: isSimStarted, - pressed: isPressed, - }, - }, - }, - }).unwrap(); - } catch (err) { - console.log("error", err); - } - }; + // // AppDispatcher.dispatch({ + // // type: 'widgets/start-edit', + // // token: props.token, + // // data: widget + // // }); - useEffect(() => { - if (widget.customProperties.simStartedSendValue) { - let widget = widget; - widget.customProperties.simStartedSendValue = false; - widget.customProperties.pressed = false; + // // Effect cleanup + // return () => { + // // Clean up if needed + // }; + // }, [props.token, props.widget]); - onInputChanged(widget.customProperties.off_value, "", false, false); - } - }, [pressed]); + // useEffect(() => { + // if (props.widget.customProperties.simStartedSendValue) { + // let widget = props.widget; + // widget.customProperties.simStartedSendValue = false; + // widget.customProperties.pressed = false; + // AppDispatcher.dispatch({ + // type: 'widgets/start-edit', + // token: props.token, + // data: widget + // }); + + // props.onInputChanged(widget.customProperties.off_value, '', false, false); + // } + // }, [props, setPressed]); + + // useEffect(() => { + // setPressed(props.widget.customProperties.pressed); + // }, [props.widget.customProperties.pressed]); + + // const onPress = (e) => { + // if (e.button === 0 && !props.widget.customProperties.toggle) { + // valueChanged(props.widget.customProperties.on_value, true); + // } + // }; + + // const onRelease = (e) => { + // if (e.button === 0) { + // let nextState = false; + // if (props.widget.customProperties.toggle) { + // nextState = !pressed; + // } + // valueChanged(nextState ? props.widget.customProperties.on_value : props.widget.customProperties.off_value, nextState); + // } + // }; + + // const valueChanged = (newValue, newPressed) => { + // if (props.onInputChanged) { + // props.onInputChanged(newValue, 'pressed', newPressed, true); + // } + // setPressed(newPressed); + // }; useEffect(() => { - setPressed(widget.customProperties.pressed); - }, [widget.customProperties.pressed]); + if (pressed) { + console.log("Yo"); + } + }, [pressed]); let opacity = widget.customProperties.background_color_opacity; const buttonStyle = { diff --git a/src/store/apiSlice.js b/src/store/apiSlice.js index 55698e8d..45433be0 100644 --- a/src/store/apiSlice.js +++ b/src/store/apiSlice.js @@ -115,5 +115,4 @@ export const { useDeleteUserFromUsergroupMutation, useUpdateUsergroupMutation, useGetWidgetsQuery, - useLazyGetICbyIdQuery, } = apiSlice; From cad50bb9d85c9f1204a40e5dea40bc7f1474d9b9 Mon Sep 17 00:00:00 2001 From: Andrii Podriez Date: Tue, 8 Apr 2025 10:30:27 +0200 Subject: [PATCH 09/12] transfer dashboard and widgets changes to a new branch Signed-off-by: Andrii Podriez Signed-off-by: SystemsPurge --- src/common/api/websocket-api.js | 30 +- src/pages/dashboards/dashboard-new.js | 92 --- src/pages/dashboards/dashboard.jsx | 586 +++++++++++++++++- .../dashboards/dialogs/edit-file-content.js | 82 --- .../dashboards/dialogs/edit-file-content.jsx | 57 ++ .../dashboards/dialogs/edit-files-dialog.js | 192 ------ .../dashboards/dialogs/edit-files-dialog.jsx | 111 ++++ .../dialogs/edit-signal-mapping.jsx | 273 ++++++++ .../dashboards/hooks/use-dashboard-data.js | 103 +++ .../hooks/use-websocket-connection.js | 84 +++ src/pages/dashboards/styles.js | 21 + src/pages/dashboards/widget/widget.jsx | 41 +- .../dashboards/widget/widgets/button.jsx | 32 +- .../dashboards/widget/widgets/icstatus.jsx | 16 +- src/pages/dashboards/widget/widgets/input.jsx | 108 ++-- .../scenarios/dialogs/edit-signal-mapping.js | 488 ++++++++------- src/store/apiSlice.js | 3 +- src/store/dashboardSlice.js | 101 --- src/store/endpoints/file-endpoints.js | 86 +-- src/store/endpoints/ic-endpoints.js | 2 +- src/store/endpoints/websocket-endpoints.js | 103 --- src/store/index.js | 28 +- src/store/websocketSlice.js | 261 ++++---- 23 files changed, 1798 insertions(+), 1102 deletions(-) delete mode 100644 src/pages/dashboards/dashboard-new.js delete mode 100644 src/pages/dashboards/dialogs/edit-file-content.js create mode 100644 src/pages/dashboards/dialogs/edit-file-content.jsx delete mode 100644 src/pages/dashboards/dialogs/edit-files-dialog.js create mode 100644 src/pages/dashboards/dialogs/edit-files-dialog.jsx create mode 100644 src/pages/dashboards/dialogs/edit-signal-mapping.jsx create mode 100644 src/pages/dashboards/hooks/use-dashboard-data.js create mode 100644 src/pages/dashboards/hooks/use-websocket-connection.js create mode 100644 src/pages/dashboards/styles.js delete mode 100644 src/store/dashboardSlice.js delete mode 100644 src/store/endpoints/websocket-endpoints.js diff --git a/src/common/api/websocket-api.js b/src/common/api/websocket-api.js index 26abe767..a985d70d 100644 --- a/src/common/api/websocket-api.js +++ b/src/common/api/websocket-api.js @@ -24,14 +24,14 @@ class WebSocketManager { } connect(id, url, onMessage, onOpen, onClose) { - const existingSocket = this.sockets.find(s => s.id === id); + const existingSocket = this.sockets.find((s) => s.id === id); if (existingSocket && existingSocket.socket.readyState === WebSocket.OPEN) { - console.log('Already connected to:', url); + console.log("Already connected to:", url); return; } - const socket = new WebSocket(url, 'live'); - socket.binaryType = 'arraybuffer'; + const socket = new WebSocket(url, "live"); + socket.binaryType = "arraybuffer"; socket.onopen = onOpen; socket.onmessage = (event) => { @@ -45,20 +45,22 @@ class WebSocketManager { } disconnect(id) { - const socket = this.sockets.find(s => s.id === id); + const socket = this.sockets.find((s) => s.id === id); if (socket) { socket.socket.close(); - this.sockets = this.sockets.filter(s => s.id !== id); - console.log('WebSocket connection closed for id:', id); + this.sockets = this.sockets.filter((s) => s.id !== id); + console.log("WebSocket connection closed for id:", id); } } send(id, message) { - const socket = this.sockets.find(s => s.id === id); + console.log("MESSAGE", message); + const socket = this.sockets.find((s) => s.id === id); if (socket == null) { return false; } const data = this.messageToBuffer(message); + console.log("📤 Sending binary buffer to server:", new Uint8Array(data)); socket.socket.send(data); return true; @@ -69,11 +71,11 @@ class WebSocketManager { const view = new DataView(buffer); let bits = 0; - bits |= (message.version & 0xF) << OFFSET_VERSION; + bits |= (message.version & 0xf) << OFFSET_VERSION; bits |= (message.type & 0x3) << OFFSET_TYPE; let source_index = 0; - source_index |= (message.source_index & 0xFF); + source_index |= message.source_index & 0xff; const sec = Math.floor(message.timestamp / 1e3); const nsec = (message.timestamp - sec * 1e3) * 1e6; @@ -83,7 +85,7 @@ class WebSocketManager { view.setUint16(0x02, message.length, true); view.setUint32(0x04, message.sequence, true); view.setUint32(0x08, sec, true); - view.setUint32(0x0C, nsec, true); + view.setUint32(0x0c, nsec, true); const data = new Float32Array(buffer, 0x10, message.length); data.set(message.values); @@ -118,19 +120,19 @@ class WebSocketManager { const bytes = length * 4 + 16; return { - version: (bits >> OFFSET_VERSION) & 0xF, + version: (bits >> OFFSET_VERSION) & 0xf, type: (bits >> OFFSET_TYPE) & 0x3, source_index: source_index, length: length, sequence: data.getUint32(0x04, 1), - timestamp: data.getUint32(0x08, 1) * 1e3 + data.getUint32(0x0C, 1) * 1e-6, + timestamp: data.getUint32(0x08, 1) * 1e3 + data.getUint32(0x0c, 1) * 1e-6, values: new Float32Array(data.buffer, data.byteOffset + 0x10, length), blob: new DataView(data.buffer, data.byteOffset + 0x00, bytes), }; } getSocketById(id) { - const socketEntry = this.sockets.find(s => s.id === id); + const socketEntry = this.sockets.find((s) => s.id === id); return socketEntry ? socketEntry.socket : null; } diff --git a/src/pages/dashboards/dashboard-new.js b/src/pages/dashboards/dashboard-new.js deleted file mode 100644 index 5ac7d8e2..00000000 --- a/src/pages/dashboards/dashboard-new.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * 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 { useEffect } from "react"; -import { useParams } from "react-router-dom"; -import { - useGetDashboardQuery, - useGetICSQuery, - useLazyGetWidgetsQuery, - useLazyGetConfigsQuery, - useLazyGetFilesQuery, - useLazyGetSignalsQuery, - useGetWidgetsQuery, -} from "../../store/apiSlice"; -import { useState } from "react"; -import DashboardLayout from "./dashboard-layout"; -import ErrorBoundary from "./dashboard-error-boundry"; - -const Dashboard = ({}) => { - const params = useParams(); - const { data: { ics } = [] } = useGetICSQuery(); - const { - data: { dashboard } = {}, - isFetching: isFetchingDashboard, - refetch: refetchDashboard, - } = useGetDashboardQuery(params.dashboard); - const [triggerGetConfigs] = useLazyGetConfigsQuery(); - const [triggerGetFiles] = useLazyGetFilesQuery(); - const [triggerGetSignals] = useLazyGetSignalsQuery(); - - const [editing, setEditing] = useState(false); - const [locked, setLocked] = useState(false); - - useEffect(() => { - if (!isFetchingDashboard) { - setGridParameters({ height: dashboard.height, grid: dashboard.grid }); - fetchWidgetData(dashboard.scenarioID); - } - }, [isFetchingDashboard]); - - const fetchWidgetData = async (scenarioID) => { - try { - const configsRes = await triggerGetConfigs(scenarioID).unwrap(); - if (configsRes.configs) { - setConfigs(configsRes.configs); - //load signals if there are any configs - - if (configsRes.configs.length > 0) { - for (const config of configsRes.configs) { - const signalsInRes = await triggerGetSignals({ - configID: config.id, - direction: "in", - }).unwrap(); - const signalsOutRes = await triggerGetSignals({ - configID: config.id, - direction: "out", - }).unwrap(); - setSignals((prevState) => [ - ...signalsInRes.signals, - ...signalsOutRes.signals, - ...prevState, - ]); - } - } - } - } catch (err) { - console.log("error fetching data", err); - } - }; - - return ( - - - - ); -}; - -export default Dashboard; diff --git a/src/pages/dashboards/dashboard.jsx b/src/pages/dashboards/dashboard.jsx index 757153e5..b4a81a3d 100644 --- a/src/pages/dashboards/dashboard.jsx +++ b/src/pages/dashboards/dashboard.jsx @@ -15,32 +15,600 @@ * along with VILLASweb. If not, see . ******************************************************************************/ +import React, { useState, useEffect, useCallback, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; +import Fullscreenable from "react-fullscreenable"; +import "react-contexify/dist/ReactContexify.min.css"; +import EditWidget from "./widget/edit-widget/edit-widget"; +import EditSignalMappingDialog from "./dialogs/edit-signal-mapping.jsx"; +import WidgetToolbox from "./widget/widget-toolbox"; +import WidgetArea from "./grid/widget-area"; +import DashboardButtonGroup from "./grid/dashboard-button-group"; +import IconToggleButton from "../../common/buttons/icon-toggle-button"; +import WidgetContainer from "./widget/widget-container"; +import Widget from "./widget/widget.jsx"; +import EditFilesDialog from "./dialogs/edit-files-dialog.jsx"; + +import { disconnect } from "../../store/websocketSlice"; +import { useDashboardData } from "./hooks/use-dashboard-data.js"; +import useWebSocketConnection from "./hooks/use-websocket-connection.js"; + import { useGetDashboardQuery, useGetICSQuery, useLazyGetWidgetsQuery, - useLazyGetConfigsQuery, useLazyGetFilesQuery, - useLazyGetSignalsQuery, + useAddWidgetMutation, + useUpdateWidgetMutation, + useDeleteWidgetMutation, + useUpdateDashboardMutation, + useGetICSQuery, + useDeleteFileMutation, + useAddFileMutation, + useUpdateFileMutation, } from "../../store/apiSlice"; import { useState } from "react"; import DashboardLayout from "./dashboard-layout"; import ErrorBoundary from "./dashboard-error-boundry"; -const Dashboard = ({}) => { - const { data: { ics } = [] } = useGetICSQuery(); - const [triggerGetConfigs] = useLazyGetConfigsQuery(); +const startUpdaterWidgets = new Set(["Slider", "Button", "NumberInput"]); + +const Dashboard = ({ isFullscreen, toggleFullscreen }) => { + const dispatch = useDispatch(); + const { token: sessionToken } = useSelector((state) => state.auth); + const params = useParams(); + + const { + data: { dashboard } = { dashboard: {} }, + error: dashboardError, + isLoading: isDashboardLoading, + } = useGetDashboardQuery(params.dashboard); + const { data: { ics } = {} } = useGetICSQuery(); + + const [triggerGetWidgets] = useLazyGetWidgetsQuery(); + const [triggerDeleteFile] = useDeleteFileMutation(); + const [triggerUploadFile] = useAddFileMutation(); + const [addWidget] = useAddWidgetMutation(); + const [updateWidget] = useUpdateWidgetMutation(); + const [deleteWidgetMutation] = useDeleteWidgetMutation(); + const [updateDashboard] = useUpdateDashboardMutation(); + const [triggerUpdateFile] = useUpdateFileMutation(); const [triggerGetFiles] = useLazyGetFilesQuery(); - const [triggerGetSignals] = useLazyGetSignalsQuery(); + const [widgets, setWidgets] = useState([]); + const [widgetsToUpdate, setWidgetsToUpdate] = useState([]); const [editing, setEditing] = useState(false); + const [paused, setPaused] = useState(false); const [locked, setLocked] = useState(false); + const [editModal, setEditModal] = useState(false); + const [editOutputSignalsModal, setEditOutputSignalsModal] = useState(false); + const [editInputSignalsModal, setEditInputSignalsModal] = useState(false); + const [filesEditModal, setFilesEditModal] = useState(false); + const [modalData, setModalData] = useState(null); + const [widgetOrigIDs, setWidgetOrigIDs] = useState([]); + const [height, setHeight] = useState(10); + const [grid, setGrid] = useState(50); + + const { files, configs, signals, activeICS, refetchDashboardData } = + useDashboardData(dashboard.scenarioID); + + //as soon as dashboard is loaded, load widgets, configs, signals and files for this dashboard + useEffect(() => { + if (dashboard.id) { + fetchWidgets(dashboard.id); + setHeight(dashboard.height); + setGrid(dashboard.grid); + } + }, [dashboard]); + + useWebSocketConnection(activeICS, signals, widgets); + + //disconnect from the websocket on component unmount + useEffect(() => { + return () => { + activeICS.forEach((ic) => { + dispatch(disconnect({ id: ic.id })); + }); + }; + }, []); + + const fetchWidgets = async (dashboardID) => { + try { + const widgetsRes = await triggerGetWidgets(dashboardID).unwrap(); + if (widgetsRes.widgets) { + setWidgets(widgetsRes.widgets); + } + } catch (err) { + console.log("error fetching data", err); + } + }; + + const handleKeydown = useCallback( + ({ key }) => { + const actions = { + " ": () => setPaused((prev) => !prev), + p: () => setPaused((prev) => !prev), + e: () => setEditing((prev) => !prev), + f: toggleFullscreen, + }; + if (actions[key]) actions[key](); + }, + [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) => { + setWidgetsToUpdate((prevWidgetsToUpdate) => [ + ...prevWidgetsToUpdate, + widget.id, + ]); + setWidgets((prevWidgets) => + prevWidgets.map((w) => (w.id === widget.id ? { ...widget } : w)) + ); + + 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: widget }, + }).unwrap(); + fetchWidgets(dashboard.id); + } catch (err) { + console.log("error", err); + } + }; + + const onSimulationStarted = () => { + widgets.forEach(async (widget) => { + if (startUpdaterWidgets.has(widget.type)) { + const updatedWidget = { + ...widget, + customProperties: { + ...widget.customProperties, + simStartedSendValue: true, + }, + }; + try { + await updateWidget({ + widgetID: widget.id, + updatedWidget: { widget: updatedWidget }, + }).unwrap(); + } catch (err) { + console.log("error", err); + } + } + }); + + fetchWidgets(dashboard.id); + }; + + const editWidget = (widget, index) => { + setEditModal(true); + setModalData({ ...widget }); + }; + + 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 closeEditFiles = () => { + widgets.forEach((widget) => { + if (widget.type === "Image") { + //widget.customProperties.update = true; + } + }); + setFilesEditModal(false); + }; + + const closeEdit = async (data) => { + if (!data) { + setEditModal(false); + setModalData(null); + return; + } + + if (data.type === "Image") { + data.customProperties.update = true; + } + + console.log("UPDATING WIDGET", data); + + try { + await updateWidget({ + widgetID: data.id, + updatedWidget: { widget: data }, + }).unwrap(); + fetchWidgets(dashboard.id); + } catch (err) { + console.log("error", err); + } + + setEditModal(false); + setModalData(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 = async () => { + 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); + } + }); + + if (height !== dashboard.height || grid !== dashboard.grid) { + try { + const { height: oldHeight, grid: oldGrid, ...rest } = dashboard; + await updateDashboard({ + dashboardID: dashboard.id, + dashboard: { height: height, grid: grid, ...rest }, + }).unwrap(); + } catch (err) { + console.log("error", err); + } + } + + if (widgetsToUpdate.length > 0) { + try { + for (const index in widgetsToUpdate) { + await updateWidget({ + widgetID: widgetsToUpdate[index], + updatedWidget: { + widget: { + ...widgets.find((w) => w.id == widgetsToUpdate[index]), + }, + }, + }).unwrap(); + } + fetchWidgets(dashboard.id); + } catch (err) { + console.log("error", err); + } + } + + setEditing(false); + }; + + 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); + } + } + }); + fetchWidgets(dashboard.id); + setEditing(false); + setHeight(dashboard.height); + setGrid(dashboard.grid); + }; + + const uploadFile = async (file) => { + await triggerUploadFile({ + scenarioID: dashboard.scenarioID, + file: file, + }) + .then(async () => { + await triggerGetFiles(dashboard.scenarioID).then((fs) => { + setFiles(fs.data.files); + }); + }) + .catch((e) => { + console.log(`File upload failed: ${e}`); + }); + }; + + const deleteFile = async (index) => { + let file = files[index]; + if (file !== undefined) { + await triggerDeleteFile(file.id) + .then(async () => { + await triggerGetFiles(dashboard.scenarioID) + .then((fs) => { + setFiles(fs.data.files); + }) + .catch((e) => { + console.log(`Error fetching files: ${e}`); + }); + }) + .catch((e) => { + console.log(`Error deleting: ${e}`); + }); + } + }; + + const updateFile = async (fileId, file) => { + try { + await triggerUpdateFile({ fileID: fileId, file: file }) + .then(async () => { + await triggerGetFiles(dashboard.scenarioID) + .then((fs) => { + setFiles(fs.data.files); + }) + .catch((e) => { + console.log(`Error fetching files: ${e}`); + }); + }) + .catch((e) => { + console.log(`Error deleting: ${e}`); + }); + } catch (error) { + console.error("Error updating file:", error); + } + }; + + const updateGrid = (value) => { + setGrid(value); + }; + + const updateHeight = (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) { + setHeight((prevState) => prevState - 50); + } + } else { + setHeight((prevState) => prevState + 50); + } + }; + + if (isDashboardLoading) { + return ; + } + + if (dashboardError) { + return
Error. Dashboard not found!
; + } return ( - - - +
+
+
+

+ {dashboard.name} + + setLocked(!locked)} + buttonStyle={{ marginLeft: "10px" }} + iconStyle={{ height: "25px", width: "25px" }} + /> + +

+
+ + setPaused(true)} + onUnpause={() => setPaused(false)} + onEditFiles={() => setFilesEditModal(true)} + onEditOutputSignals={() => setEditOutputSignalsModal(true)} + onEditInputSignals={() => setEditInputSignalsModal(true)} + /> +
+ +
e.preventDefault()} + > + {editing && ( + + )} + + + {widgets != null && + Object.keys(widgets).map((widgetKey) => ( +
+ deleteWidget(widget.id)} + onChange={editing ? widgetChange : onChange} + > + onSimulationStarted()} + /> + +
+ ))} +
+ + + + setEditInputSignalsModal(false)} + direction="in" + signals={[...signals].filter((s) => s.direction == "in")} + configID={null} + configs={configs} + sessionToken={sessionToken} + refetch={refetchDashboardData} + /> + + setEditOutputSignalsModal(false)} + direction="out" + signals={[...signals].filter((s) => s.direction == "out")} + configID={null} + configs={configs} + sessionToken={sessionToken} + refetch={refetchDashboardData} + /> + + +
+
); }; diff --git a/src/pages/dashboards/dialogs/edit-file-content.js b/src/pages/dashboards/dialogs/edit-file-content.js deleted file mode 100644 index c14570da..00000000 --- a/src/pages/dashboards/dialogs/edit-file-content.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * 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 { Form, Button, Col } from 'react-bootstrap'; -import Dialog from '../../../common/dialogs/dialog'; - -class EditFileContent extends React.Component { - valid = true; - - constructor(props) { - super(props); - - this.state = { - uploadFile: null, - }; - } - - selectUploadFile(event) { - this.setState({ uploadFile: event.target.files[0] }); - }; - - startEditContent(){ - const formData = new FormData(); - formData.append("file", this.state.uploadFile); - - AppDispatcher.dispatch({ - type: 'files/start-edit', - data: formData, - token: this.props.sessionToken, - id: this.props.file.id - }); - - this.setState({ uploadFile: null }); - }; - - onClose = () => { - this.props.onClose(); - }; - - render() { - return this.onClose()} - blendOutCancel = {true} - valid={true} - > - - this.selectUploadFile(event)} /> - - - - - - ; - } -} - -export default EditFileContent; \ No newline at end of file diff --git a/src/pages/dashboards/dialogs/edit-file-content.jsx b/src/pages/dashboards/dialogs/edit-file-content.jsx new file mode 100644 index 00000000..d99857bb --- /dev/null +++ b/src/pages/dashboards/dialogs/edit-file-content.jsx @@ -0,0 +1,57 @@ +/** + * 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, { useState } from "react"; +import { Form, Button, Col } from "react-bootstrap"; +import Dialog from "../../../common/dialogs/dialog"; + +const EditFileContent = ({ show, onClose, file, updateFile }) => { + const [uploadFile, setUploadFile] = useState(null); + + const selectUploadFile = (event) => { + const selectedFile = event.target.files[0]; + setUploadFile(selectedFile); + }; + + const handleUploadFile = () => { + updateFile(file.id, uploadFile); + setUploadFile(null); + onClose(); + }; + + return ( + + + + + + + + + + ); +}; + +export default EditFileContent; diff --git a/src/pages/dashboards/dialogs/edit-files-dialog.js b/src/pages/dashboards/dialogs/edit-files-dialog.js deleted file mode 100644 index 3eacda4e..00000000 --- a/src/pages/dashboards/dialogs/edit-files-dialog.js +++ /dev/null @@ -1,192 +0,0 @@ -/** - * 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 {Form, Button, Col, ProgressBar, Row} from 'react-bootstrap'; -import Dialog from '../../../common/dialogs/dialog'; -import { Table, ButtonColumn, DataColumn } from "../../../common/table"; -import EditFileContent from "./edit-file-content"; - -class EditFilesDialog extends React.Component { - valid = true; - - constructor(props) { - super(props); - - this.state = { - uploadFile: null, - uploadProgress: 0, - editModal: false, - modalFile: {} - }; - } - - onClose() { - this.props.onClose(); - } - - selectUploadFile(event) { - this.setState({ uploadFile: event.target.files[0] }); - }; - - startFileUpload(){ - // upload file - const formData = new FormData(); - formData.append("file", this.state.uploadFile); - - AppDispatcher.dispatch({ - type: 'files/start-upload', - data: formData, - token: this.props.sessionToken, - progressCallback: this.updateUploadProgress, - finishedCallback: this.clearProgress, - scenarioID: this.props.scenarioID, - }); - - this.setState({ uploadFile: null }); - }; - - updateUploadProgress = (event) => { - if (event.hasOwnProperty("percent")){ - this.setState({ uploadProgress: parseInt(event.percent.toFixed(), 10) }); - } else { - this.setState({ uploadProgress: 0 }); - } - }; - - clearProgress = (newFileID) => { - this.setState({ uploadProgress: 0 }); - }; - - closeEditModal() { - this.setState({editModal: false}); - } - - deleteFile(index){ - let file = this.props.files[index] - AppDispatcher.dispatch({ - type: 'files/start-remove', - data: file, - token: this.props.sessionToken - }); - } - - render() { - let fileOptions = []; - if (this.props.files.length > 0){ - fileOptions.push( - - ) - fileOptions.push(this.props.files.map((file, index) => ( - - ))) - } else { - fileOptions = - } - - const progressBarStyle = { - marginLeft: '100px', - marginTop: '-40px' - }; - - let title = this.props.locked ? "View files of scenario" : "Edit Files of Scenario"; - - return ( - this.onClose()} - blendOutCancel = {true} - valid={true} - > - - - - - - this.deleteFile(index)} - editButton - onEdit={index => this.setState({ editModal: true, modalFile: this.props.files[index] })} - locked={this.props.locked} - /> -
- -
-
Add file
- - - this.selectUploadFile(event)} - disabled={this.props.locked} - /> - - - - - - - -
- -
- - - - - -
- - this.closeEditModal(data)} - sessionToken={this.props.sessionToken} - file={this.state.modalFile} - /> -
- ); - } -} - -export default EditFilesDialog; \ No newline at end of file diff --git a/src/pages/dashboards/dialogs/edit-files-dialog.jsx b/src/pages/dashboards/dialogs/edit-files-dialog.jsx new file mode 100644 index 00000000..d8fae870 --- /dev/null +++ b/src/pages/dashboards/dialogs/edit-files-dialog.jsx @@ -0,0 +1,111 @@ +/** + * 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, { useState } from "react"; +import { Form, Button, Col, ProgressBar, Row } from "react-bootstrap"; +import Dialog from "../../../common/dialogs/dialog"; +import { Table, ButtonColumn, DataColumn } from "../../../common/table"; +import EditFileContent from "./edit-file-content.jsx"; + +const EditFilesDialog = (props) => { + const [uploadFile, setUploadFile] = useState(null); + const [uploadProgress, setUploadProgress] = useState(0); + const [editModal, setEditModal] = useState(false); + const [modalFile, setModalFile] = useState({}); + + const onClose = () => { + props.onClose(); + }; + + const selectUploadFile = (e) => { + console.log("SELECTED FILE", e.target.files[0]); + setUploadFile(e.target.files[0]); + }; + + let title = props.locked + ? "View files of scenario" + : "Edit Files of Scenario"; + + return ( + onClose()} + blendOutCancel={true} + valid={true} + > + + + + + + { + setEditModal(true); + setModalFile(props.files[index]); + }} + locked={props.locked} + /> +
+ +
+
Add file
+ + + selectUploadFile(event)} + disabled={props.locked} + /> + + + + + + + +
+ +
+ +
+ + setEditModal(false)} + sessionToken={props.sessionToken} + file={modalFile} + updateFile={props.updateFile} + /> +
+ ); +}; + +export default EditFilesDialog; diff --git a/src/pages/dashboards/dialogs/edit-signal-mapping.jsx b/src/pages/dashboards/dialogs/edit-signal-mapping.jsx new file mode 100644 index 00000000..ecc77d36 --- /dev/null +++ b/src/pages/dashboards/dialogs/edit-signal-mapping.jsx @@ -0,0 +1,273 @@ +import { useState, useEffect } from "react"; +import { Button, Form, OverlayTrigger, Tooltip } from "react-bootstrap"; +import { + Table, + ButtonColumn, + CheckboxColumn, + DataColumn, +} from "../../../common/table"; +import { + dialogWarningLabel, + signalDialogCheckButton, + buttonStyle, +} from "../styles"; +import Dialog from "../../../common/dialogs/dialog"; +import Icon from "../../../common/icon"; +import { + useDeleteSignalMutation, + useUpdateSignalMutation, +} from "../../../store/apiSlice"; +import { Collapse } from "react-collapse"; + +const EditSignalMappingDialog = ({ + show, + direction, + onClose, + signals, + refetch, +}) => { + const [isCollapseOpened, setCollapseOpened] = useState(false); + const [checkedSignalsIDs, setCheckedSignalsIDs] = useState([]); + const [deleteSignal] = useDeleteSignalMutation(); + const [updateSignal] = useUpdateSignalMutation(); + + const [updatedSignals, setUpdatedSignals] = useState([]); + const [updatedSignalsIDs, setUpdatedSignalsIDs] = useState([]); + + useEffect(() => { + if (signals.length > 0) { + setUpdatedSignals([...signals]); + } + }, [signals]); + + const handleMappingChange = (e, row, column) => { + const signalToUpdate = { ...updatedSignals[row] }; + switch (column) { + case 1: + signalToUpdate.index = e.target.value; + break; + case 2: + signalToUpdate.name = e.target.value; + break; + case 3: + signalToUpdate.unit = e.target.value; + break; + case 4: + signalToUpdate.scalingFactor = e.target.value; + break; + default: + break; + } + + setUpdatedSignals((prevState) => + prevState.map((signal, index) => + index === row ? signalToUpdate : signal + ) + ); + + setUpdatedSignalsIDs((prevState) => [signalToUpdate.id, ...prevState]); + }; + + const handleDelete = async (signalID) => { + try { + await deleteSignal(signalID).unwrap(); + } catch (err) { + console.log(err); + } + + refetch(); + }; + + const handleUpdate = async () => { + try { + for (let id of updatedSignalsIDs) { + const signalToUpdate = updatedSignals.find( + (signal) => signal.id === id + ); + + if (signalToUpdate) { + await updateSignal({ + signalID: id, + updatedSignal: signalToUpdate, + }).unwrap(); + } + } + + refetch(); + setUpdatedSignalsIDs([]); + } catch (error) { + console.error("Error updating signals:", error); + } + }; + + const onSignalChecked = (signal, event) => { + if (!checkedSignalsIDs.includes(signal.id)) { + setCheckedSignalsIDs((prevState) => [...prevState, signal.id]); + } else { + const index = checkedSignalsIDs.indexOf(signal.id); + setCheckedSignalsIDs((prevState) => + prevState.filter((_, i) => i !== index) + ); + } + }; + + const isSignalChecked = (signal) => { + return checkedSignalsIDs.includes(signal.id); + }; + + const toggleCheckAll = () => { + //check if all signals are already checked + if (checkedSignalsIDs.length === signals.length) { + setCheckedSignalsIDs([]); + } else { + signals.forEach((signal) => { + if (!checkedSignalsIDs.includes(signal.id)) { + setCheckedSignalsIDs((prevState) => [...prevState, signal.id]); + } + }); + } + }; + + const deleteCheckedSignals = async () => { + if (checkedSignalsIDs.length > 0) { + try { + const deletePromises = checkedSignalsIDs.map((signalID) => + deleteSignal(signalID).unwrap() + ); + await Promise.all(deletePromises); + refetchSignals(); + } catch (err) { + console.log(err); + } + } + }; + + const DialogWindow = ( + { + handleUpdate(); + onClose(c); + }} + onReset={() => {}} + valid={true} + > + + + IMPORTANT: Signal configurations that were created before January 2022 + have to be fixed manually. Signal indices have to start at 0 and not + 1. + + + {" "} + Click in table cell to edit + + onSignalChecked(signal)} + data={updatedSignals} + > + onSignalChecked(index, event)} + checked={(signal) => isSignalChecked(signal)} + width="30" + /> + + handleMappingChange(e, row, column) + } + /> + + handleMappingChange(e, row, column) + } + /> + + handleMappingChange(e, row, column) + } + /> + + handleMappingChange(e, row, column) + } + /> + handleDelete(signals[index].id)} + /> +
+ +
+ Check/Uncheck All + } + > + + + +
+
+ +
Choose a Component Configuration to add the signal to:
+
+ {typeof configs !== "undefined" && + configs.map((config) => ( + + ))} +
+
+
+
+
+ ); + + return show ? DialogWindow : <>; +}; + +export default EditSignalMappingDialog; diff --git a/src/pages/dashboards/hooks/use-dashboard-data.js b/src/pages/dashboards/hooks/use-dashboard-data.js new file mode 100644 index 00000000..4639eaf1 --- /dev/null +++ b/src/pages/dashboards/hooks/use-dashboard-data.js @@ -0,0 +1,103 @@ +/** + * 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 { useState, useEffect, useMemo } from "react"; +import { + useLazyGetFilesQuery, + useLazyGetConfigsQuery, + useLazyGetSignalsQuery, + useGetICSQuery, +} from "../../../store/apiSlice"; + +// a custom hook designed to deliver data required for a dashboard and its widgets +export const useDashboardData = (scenarioID) => { + const [files, setFiles] = useState([]); + const [configs, setConfigs] = useState([]); + const [signals, setSignals] = useState([]); + const [activeICS, setActiveICS] = useState([]); + const { data: { ics } = { ics: [] } } = useGetICSQuery(); + + const [triggerGetFiles] = useLazyGetFilesQuery(); + const [triggerGetConfigs] = useLazyGetConfigsQuery(); + const [triggerGetSignals] = useLazyGetSignalsQuery(); + + const fetchDashboardData = async () => { + if (!scenarioID) return; + + //in case of refetching + setFiles([]); + setConfigs([]); + setSignals([]); + + try { + // Fetch files + const filesRes = await triggerGetFiles(scenarioID).unwrap(); + if (filesRes?.files) { + setFiles(filesRes.files); + } + + // Fetch configs and signals + const configsRes = await triggerGetConfigs(scenarioID).unwrap(); + console.log("GOT CONFIGS", configsRes); + if (configsRes?.configs) { + setConfigs(configsRes.configs); + + for (const config of configsRes.configs) { + const signalsInRes = await triggerGetSignals({ + configID: config.id, + direction: "in", + }).unwrap(); + const signalsOutRes = await triggerGetSignals({ + configID: config.id, + direction: "out", + }).unwrap(); + + setSignals((prev) => [ + ...prev, + ...signalsInRes.signals, + ...signalsOutRes.signals, + ]); + } + } + } catch (err) { + console.error("Error fetching dashboard data:", err); + } + }; + + useEffect(() => { + if (scenarioID) { + fetchDashboardData(); + } + }, [scenarioID]); + + // Derive active ICS based on the fetched configs + useEffect(() => { + let usedICS = []; + for (const config of configs) { + usedICS.push(config.icID); + } + setActiveICS(ics.filter((i) => usedICS.includes(i.id))); + }, [configs]); + + return { + files, + configs, + signals, + activeICS, + refetchDashboardData: fetchDashboardData, + }; +}; diff --git a/src/pages/dashboards/hooks/use-websocket-connection.js b/src/pages/dashboards/hooks/use-websocket-connection.js new file mode 100644 index 00000000..96f7f818 --- /dev/null +++ b/src/pages/dashboards/hooks/use-websocket-connection.js @@ -0,0 +1,84 @@ +/** + * 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 { useEffect, useRef } from "react"; +import { useDispatch } from "react-redux"; +import { connectWebSocket } from "../../../store/websocketSlice"; + +//this hook is used to establish websocket connection in the dashboard +const useWebSocketConnection = (activeICS, signals, widgets) => { + const dispatch = useDispatch(); + const hasConnectedRef = useRef(false); + const startUpdaterWidgets = new Set(["Slider", "Button", "NumberInput"]); + + useEffect(() => { + //we want to connect only once after widgets, signals and activeICs are loaded + if ( + !hasConnectedRef.current && + signals.length > 0 && + activeICS.length > 0 && + widgets.length > 0 + ) { + activeICS.forEach((i) => { + if (i.websocketurl) { + console.log("CONNECTING TO A WEBSOCKET FROM", i); + let inputValues = []; + //read values of all the manipulation widgets and set their values to the corresponding input signals + const inputSignals = [...signals].filter((s) => s.direction == "in"); + if (inputSignals.length > 0) { + //initialize values for input signals + inputValues = new Array(inputSignals.length).fill(0); + //go through all the widgets and check if they have a value for any of the input signals + //add this value to the inputValues array at the index + widgets.forEach((widget) => { + if (startUpdaterWidgets.has(widget.type)) { + const matchingSignal = inputSignals.find((signal) => + widget.signalIDs.includes(signal.id) + ); + if ( + matchingSignal && + typeof matchingSignal.index === "number" && + matchingSignal.index < inputValues.length + ) { + if (widget.type == "Button") { + inputValues[matchingSignal.index] = + widget.customProperties.off_value; + } else { + inputValues[matchingSignal.index] = + widget.customProperties.value; + } + } + } + }); + } + + dispatch( + connectWebSocket({ + url: i.websocketurl, + id: i.id, + inputValues: inputValues, + }) + ); + } + }); + + hasConnectedRef.current = true; + } + }, [activeICS, signals, widgets]); +}; + +export default useWebSocketConnection; diff --git a/src/pages/dashboards/styles.js b/src/pages/dashboards/styles.js new file mode 100644 index 00000000..98e69c78 --- /dev/null +++ b/src/pages/dashboards/styles.js @@ -0,0 +1,21 @@ +export const buttonStyle = { + marginLeft: "5px", +}; + +export const iconStyle = { + height: "25px", + width: "25px", +}; + +export const tableHeadingStyle = { + paddingTop: "30px", +}; + +export const dialogWarningLabel = { + background: "#eb4d2a", + color: "white", +}; + +export const signalDialogCheckButton = { + float: "right", +}; diff --git a/src/pages/dashboards/widget/widget.jsx b/src/pages/dashboards/widget/widget.jsx index 467723d9..2ea606d3 100644 --- a/src/pages/dashboards/widget/widget.jsx +++ b/src/pages/dashboards/widget/widget.jsx @@ -92,13 +92,7 @@ const Widget = ({ } }, [signals]); - const inputDataChanged = ( - widget, - data, - controlID, - controlValue, - isFinalChange - ) => { + const inputDataChanged = (value, controlID, controlValue, isFinalChange) => { if (controlID !== "" && isFinalChange) { let updatedWidget = JSON.parse(JSON.stringify(widget)); updatedWidget.customProperties[controlID] = controlValue; @@ -125,7 +119,7 @@ const Widget = ({ ic: icID, signalID: signal[0].id, signalIndex: signal[0].index, - data: signal[0].scalingFactor * data, + data: signal[0].scalingFactor * value, }, }) ); @@ -204,15 +198,7 @@ const Widget = ({ - inputDataChanged( - widget, - value, - controlID, - controlValue, - isFinalChange - ) - } + onInputChanged={inputDataChanged} signals={signals} token={sessionToken} /> @@ -221,32 +207,15 @@ const Widget = ({ - inputDataChanged( - widget, - value, - controlID, - controlValue, - isFinalChange - ) - } + onInputChanged={inputDataChanged} signals={signals} - token={sessionToken} /> ), Slider: ( - inputDataChanged( - widget, - value, - controlID, - controlValue, - isFinalChange - ) - } + onInputChanged={inputDataChanged} signals={signals} token={sessionToken} /> diff --git a/src/pages/dashboards/widget/widgets/button.jsx b/src/pages/dashboards/widget/widgets/button.jsx index 574f8274..0fc56180 100644 --- a/src/pages/dashboards/widget/widgets/button.jsx +++ b/src/pages/dashboards/widget/widgets/button.jsx @@ -81,8 +81,36 @@ const WidgetButton = ({ widget, editing }) => { // }; useEffect(() => { - if (pressed) { - console.log("Yo"); + updateSimStartedAndPressedValues(false, false); + }, [widget]); + + const updateSimStartedAndPressedValues = async (isSimStarted, isPressed) => { + try { + await updateWidget({ + widgetID: widget.id, + updatedWidget: { + widget: { + ...widget, + customProperties: { + ...widget.customProperties, + simStartedSendValue: isSimStarted, + pressed: isPressed, + }, + }, + }, + }).unwrap(); + } catch (err) { + console.log("error", err); + } + }; + + useEffect(() => { + if (widget.customProperties.simStartedSendValue) { + let widgetCopy = { ...widget }; + widgetCopy.customProperties.simStartedSendValue = false; + widgetCopy.customProperties.pressed = false; + + onInputChanged(widget.customProperties.off_value, "", false, false); } }, [pressed]); diff --git a/src/pages/dashboards/widget/widgets/icstatus.jsx b/src/pages/dashboards/widget/widgets/icstatus.jsx index d6cf677f..a1b3c336 100644 --- a/src/pages/dashboards/widget/widgets/icstatus.jsx +++ b/src/pages/dashboards/widget/widgets/icstatus.jsx @@ -16,10 +16,8 @@ ******************************************************************************/ import React, { useState, useEffect } from "react"; -import { Badge } from "react-bootstrap"; +import { Badge, Spinner } from "react-bootstrap"; import { stateLabelStyle } from "../../../infrastructure/styles"; -import { loadICbyId } from "../../../../store/icSlice"; -import { sessionToken } from "../../../../localStorage"; import { useDispatch } from "react-redux"; import { useLazyGetICbyIdQuery } from "../../../../store/apiSlice"; @@ -32,7 +30,7 @@ const WidgetICstatus = (props) => { if (props.ics) { try { const requests = props.ics.map((ic) => - triggerGetICbyId({ id: ic.id, token: sessionToken }).unwrap() + triggerGetICbyId(ic.id).unwrap() ); const results = await Promise.all(requests); @@ -45,9 +43,9 @@ const WidgetICstatus = (props) => { useEffect(() => { window.clearInterval(timer); timer = window.setInterval(refresh, 3000); - // Function to refresh data + refresh(); - // Cleanup function equivalent to componentWillUnmount + return () => { window.clearInterval(timer); }; @@ -58,8 +56,8 @@ const WidgetICstatus = (props) => { if (props.ics && checkedICs) { badges = ics - .filter((ic) => checkedICs.includes(ic.id)) - .map((ic) => { + .filter(({ ic }) => checkedICs.includes(ic?.id)) + .map(({ ic }) => { let badgeStyle = stateLabelStyle(ic.state, ic); return ( @@ -69,7 +67,7 @@ const WidgetICstatus = (props) => { }); } - return
{badges}
; + return badges.length > 0 ?
{badges}
: ; }; export default WidgetICstatus; diff --git a/src/pages/dashboards/widget/widgets/input.jsx b/src/pages/dashboards/widget/widgets/input.jsx index 554c1eaf..f9eb4a4b 100644 --- a/src/pages/dashboards/widget/widgets/input.jsx +++ b/src/pages/dashboards/widget/widgets/input.jsx @@ -18,22 +18,20 @@ import React, { useState, useEffect } from "react"; import { Form, Col, InputGroup } from "react-bootstrap"; import { useUpdateWidgetMutation } from "../../../../store/apiSlice"; -function WidgetInput(props) { - const [value, setValue] = useState(""); +const WidgetInput = ({ signals, widget, editing, onInputChanged }) => { + const initialValue = + widget.customProperties.value !== undefined + ? Number(widget.customProperties.value) + : widget.customProperties.default_value !== undefined + ? Number(widget.customProperties.default_value) + : ""; + const [value, setValue] = useState(initialValue); const [unit, setUnit] = useState(""); const [updateWidget] = useUpdateWidgetMutation(); useEffect(() => { - const widget = { ...props.widget }; - widget.customProperties.simStartedSendValue = false; - - updateWidgetSimStatus(true); - }, [props.token, props.widget]); - - useEffect(() => { - if (props.widget.customProperties.simStartedSendValue) { - const widget = { ...props.widget }; + if (widget.customProperties.simStartedSendValue) { widget.customProperties.simStartedSendValue = false; if(props.onInputChanged && props.signals && props.signals.length > 0){ props.onInputChanged(widget.customProperties.value, "", "", false); @@ -42,9 +40,44 @@ function WidgetInput(props) { updateWidgetSimStatus(false); - props.onInputChanged(Number(value), "", "", false); + onInputChanged(Number(value), "", false, false); + } + }, [value]); + + //once widget is mounted, update status + useEffect(() => { + updateWidgetSimStatus(true); + }, [widget.id]); + + useEffect(() => { + if (widget.customProperties.simStartedSendValue) { + updateWidgetSimStatus(false); + if (onInputChanged) { + onInputChanged(Number(value), "", false, false); + } + } + }, [value]); + + useEffect(() => { + let newValue = widget.customProperties.value; + if ( + newValue === undefined && + widget.customProperties.default_value !== undefined + ) { + newValue = widget.customProperties.default_value; + } + if (newValue !== undefined && Number(newValue) !== Number(value)) { + setValue(Number(newValue)); } - }, [props, value]); + + if (widget.signalIDs && widget.signalIDs.length > 0) { + const signalID = widget.signalIDs[0]; + const signal = signals.find((sig) => sig.id === signalID); + if (signal && signal.unit !== unit) { + setUnit(signal.unit); + } + } + }, [widget, signals]); const updateWidgetSimStatus = async (isSimStarted) => { try { @@ -61,52 +94,19 @@ function WidgetInput(props) { }, }).unwrap(); } catch (err) { - console.log("error", err); + console.log("Error updating simulation status:", err); } }; - useEffect(() => { - let newValue = ""; - let newUnit = ""; - - if ( - props.widget.customProperties.hasOwnProperty("value") && - props.widget.customProperties.value !== value - ) { - newValue = Number(props.widget.customProperties.value); - } else if ( - props.widget.customProperties.hasOwnProperty("default_value") && - value === "" - ) { - newValue = Number(props.widget.customProperties.default_value); - } - - if (props.widget.signalIDs.length > 0) { - const signalID = props.widget.signalIDs[0]; - const signal = props.signals.find((sig) => sig.id === signalID); - if (signal !== undefined) { - newUnit = signal.unit; - } - } - - if (newUnit !== unit) { - setUnit(newUnit); - } - - if (newValue !== value) { - setValue(newValue); - } - }, [props, value, unit]); - const valueIsChanging = (newValue) => { const numericalValue = Number(newValue); setValue(numericalValue); - props.widget.customProperties.value = numericalValue; + widget.customProperties.value = numericalValue; }; const valueChanged = (newValue) => { - if (props.onInputChanged) { - props.onInputChanged(Number(newValue), "value", Number(newValue), true); + if (onInputChanged) { + onInputChanged(Number(newValue), "value", Number(newValue), true); } }; @@ -121,15 +121,15 @@ function WidgetInput(props) {
- {props.widget.name} - {props.widget.customProperties.showUnit ? ` [${unit}]` : ""} + {widget.name} + {widget.customProperties.showUnit ? ` [${unit}]` : ""} valueChanged(value)} onChange={(e) => valueIsChanging(e.target.value)} @@ -142,6 +142,6 @@ function WidgetInput(props) {
); -} +}; export default WidgetInput; diff --git a/src/pages/scenarios/dialogs/edit-signal-mapping.js b/src/pages/scenarios/dialogs/edit-signal-mapping.js index 9e9fae52..e45c4260 100644 --- a/src/pages/scenarios/dialogs/edit-signal-mapping.js +++ b/src/pages/scenarios/dialogs/edit-signal-mapping.js @@ -1,259 +1,309 @@ import { useState, useEffect } from "react"; import { Button, Form, OverlayTrigger, Tooltip } from "react-bootstrap"; -import { Table, ButtonColumn, CheckboxColumn, DataColumn } from '../../../common/table'; -import { dialogWarningLabel, signalDialogCheckButton, buttonStyle } from "../styles"; +import { + Table, + ButtonColumn, + CheckboxColumn, + DataColumn, +} from "../../../common/table"; +import { + dialogWarningLabel, + signalDialogCheckButton, + buttonStyle, +} from "../styles"; import Dialog from "../../../common/dialogs/dialog"; import Icon from "../../../common/icon"; -import { useGetSignalsQuery, useAddSignalMutation, useDeleteSignalMutation, useUpdateSignalMutation } from "../../../store/apiSlice"; -import { Collapse } from 'react-collapse'; +import { + useGetSignalsQuery, + useAddSignalMutation, + useDeleteSignalMutation, + useUpdateSignalMutation, +} from "../../../store/apiSlice"; +import { Collapse } from "react-collapse"; -const EditSignalMappingDialog = ({isShown, direction, onClose, configID}) => { +const EditSignalMappingDialog = ({ show, direction, onClose, configID }) => { + const [isCollapseOpened, setCollapseOpened] = useState(false); + const [checkedSignalsIDs, setCheckedSignalsIDs] = useState([]); + const { data, refetch: refetchSignals } = useGetSignalsQuery({ + configID: configID, + direction: direction, + }); + const [addSignalToConfig] = useAddSignalMutation(); + const [deleteSignal] = useDeleteSignalMutation(); + const [updateSignal] = useUpdateSignalMutation(); + const signals = data ? data.signals : []; - const [isCollapseOpened, setCollapseOpened] = useState(false); - const [checkedSignalsIDs, setCheckedSignalsIDs] = useState([]); - const {data, refetch: refetchSignals } = useGetSignalsQuery({configID: configID, direction: direction}); - const [addSignalToConfig] = useAddSignalMutation(); - const [deleteSignal] = useDeleteSignalMutation(); - const [updateSignal] = useUpdateSignalMutation(); - const signals = data ? data.signals : []; + const [updatedSignals, setUpdatedSignals] = useState([]); + const [updatedSignalsIDs, setUpdatedSignalsIDs] = useState([]); - const [updatedSignals, setUpdatedSignals] = useState([]); - const [updatedSignalsIDs, setUpdatedSignalsIDs] = useState([]); - - useEffect(() => { - if (signals.length > 0) { - setUpdatedSignals([...signals]); - } - }, [signals]); - - const handleMappingChange = (e, row, column) => { - const signalToUpdate = {...updatedSignals[row]}; - switch (column) { - case 1: - signalToUpdate.index = e.target.value; - break; - case 2: - signalToUpdate.name = e.target.value; - break; - case 3: - signalToUpdate.unit = e.target.value; - break; - case 4: - signalToUpdate.scalingFactor = e.target.value; - break; - default: - break; - } - - setUpdatedSignals(prevState => - prevState.map((signal, index) => - index === row ? signalToUpdate : signal - ) - ); + useEffect(() => { + if (signals.length > 0) { + setUpdatedSignals([...signals]); + } + }, [signals]); - setUpdatedSignalsIDs(prevState => ([signalToUpdate.id, ...prevState])); + const handleMappingChange = (e, row, column) => { + const signalToUpdate = { ...updatedSignals[row] }; + switch (column) { + case 1: + signalToUpdate.index = e.target.value; + break; + case 2: + signalToUpdate.name = e.target.value; + break; + case 3: + signalToUpdate.unit = e.target.value; + break; + case 4: + signalToUpdate.scalingFactor = e.target.value; + break; + default: + break; } - const handleAdd = async () => { + setUpdatedSignals((prevState) => + prevState.map((signal, index) => + index === row ? signalToUpdate : signal + ) + ); - let largestIndex = -1; - signals.forEach(signal => { - if(signal.index > largestIndex){ - largestIndex = signal.index; - } - }) + setUpdatedSignalsIDs((prevState) => [signalToUpdate.id, ...prevState]); + }; - const newSignal = { - configID: configID, - direction: direction, - name: "PlaceholderName", - unit: "PlaceholderUnit", - index: largestIndex + 1, - scalingFactor: 1.0 - }; + const handleAdd = async () => { + let largestIndex = -1; + signals.forEach((signal) => { + if (signal.index > largestIndex) { + largestIndex = signal.index; + } + }); - try { - await addSignalToConfig(newSignal).unwrap(); - } catch (err) { - console.log(err); - } + const newSignal = { + configID: configID, + direction: direction, + name: "PlaceholderName", + unit: "PlaceholderUnit", + index: largestIndex + 1, + scalingFactor: 1.0, + }; - refetchSignals(); + try { + await addSignalToConfig(newSignal).unwrap(); + } catch (err) { + console.log(err); } - const handleDelete = async (signalID) => { - try { - await deleteSignal(signalID).unwrap(); - } catch (err) { - console.log(err); - } + refetchSignals(); + }; - refetchSignals(); + const handleDelete = async (signalID) => { + try { + await deleteSignal(signalID).unwrap(); + } catch (err) { + console.log(err); } - const handleUpdate = async () => { - try { - for (let id of updatedSignalsIDs) { - - const signalToUpdate = updatedSignals.find(signal => signal.id === id); - - if (signalToUpdate) { - await updateSignal({ signalID: id, updatedSignal: signalToUpdate }).unwrap(); - } - } + refetchSignals(); + }; - refetchSignals(); - setUpdatedSignalsIDs([]); - } catch (error) { - console.error("Error updating signals:", error); - } - } + const handleUpdate = async () => { + try { + for (let id of updatedSignalsIDs) { + const signalToUpdate = updatedSignals.find( + (signal) => signal.id === id + ); - const onSignalChecked = (signal, event) => { - if(!checkedSignalsIDs.includes(signal.id)){ - setCheckedSignalsIDs(prevState => ([...prevState, signal.id])); - } else { - const index = checkedSignalsIDs.indexOf(signal.id); - setCheckedSignalsIDs(prevState => prevState.filter((_, i) => i !== index)); + if (signalToUpdate) { + await updateSignal({ + signalID: id, + updatedSignal: signalToUpdate, + }).unwrap(); } + } + + refetchSignals(); + setUpdatedSignalsIDs([]); + } catch (error) { + console.error("Error updating signals:", error); } + }; - const isSignalChecked = (signal) => { - return checkedSignalsIDs.includes(signal.id); + const onSignalChecked = (signal, event) => { + if (!checkedSignalsIDs.includes(signal.id)) { + setCheckedSignalsIDs((prevState) => [...prevState, signal.id]); + } else { + const index = checkedSignalsIDs.indexOf(signal.id); + setCheckedSignalsIDs((prevState) => + prevState.filter((_, i) => i !== index) + ); } + }; + + const isSignalChecked = (signal) => { + return checkedSignalsIDs.includes(signal.id); + }; - const toggleCheckAll = () => { - //check if all signals are already checked - if(checkedSignalsIDs.length === signals.length){ - setCheckedSignalsIDs([]); - } else { - signals.forEach(signal => { - if(!checkedSignalsIDs.includes(signal.id)){ - setCheckedSignalsIDs(prevState => ([...prevState, signal.id])); - } - }) + const toggleCheckAll = () => { + //check if all signals are already checked + if (checkedSignalsIDs.length === signals.length) { + setCheckedSignalsIDs([]); + } else { + signals.forEach((signal) => { + if (!checkedSignalsIDs.includes(signal.id)) { + setCheckedSignalsIDs((prevState) => [...prevState, signal.id]); } + }); } + }; - const deleteCheckedSignals = async () => { - if(checkedSignalsIDs.length > 0){ - try { - const deletePromises = checkedSignalsIDs.map(signalID => deleteSignal(signalID).unwrap()); - await Promise.all(deletePromises); - refetchSignals(); - } catch (err) { - console.log(err); - } - } + const deleteCheckedSignals = async () => { + if (checkedSignalsIDs.length > 0) { + try { + const deletePromises = checkedSignalsIDs.map((signalID) => + deleteSignal(signalID).unwrap() + ); + await Promise.all(deletePromises); + refetchSignals(); + } catch (err) { + console.log(err); + } } + }; - const DialogWindow = ( - { - handleUpdate(); - onClose(c) - }} - onReset={() => {}} - valid={true} + const DialogWindow = ( + { + handleUpdate(); + onClose(c); + }} + onReset={() => {}} + valid={true} + > + + + IMPORTANT: Signal configurations that were created before January 2022 + have to be fixed manually. Signal indices have to start at 0 and not + 1. + + + {" "} + Click in table cell to edit + + onSignalChecked(signal)} + data={updatedSignals} > - - IMPORTANT: Signal configurations that were created before January 2022 have to be fixed manually. Signal indices have to start at 0 and not 1. - Click in table cell to edit -
onSignalChecked(signal)} data={updatedSignals}> - onSignalChecked(index, event)} - checked={(signal) => isSignalChecked(signal)} - width='30' - /> - handleMappingChange(e, row, column)} - /> - handleMappingChange(e, row, column)} - /> - handleMappingChange(e, row, column)} - /> - handleMappingChange(e, row, column)} - /> - handleDelete(signals[index].id)} - /> -
+ onSignalChecked(index, event)} + checked={(signal) => isSignalChecked(signal)} + width="30" + /> + + handleMappingChange(e, row, column) + } + /> + + handleMappingChange(e, row, column) + } + /> + + handleMappingChange(e, row, column) + } + /> + + handleMappingChange(e, row, column) + } + /> + handleDelete(signals[index].id)} + /> + -
- Check/Uncheck All } - > - + + + +
+
+ +
Choose a Component Configuration to add the signal to:
+
+ {typeof configs !== "undefined" && + configs.map((config) => ( + - - - -
-
- -
Choose a Component Configuration to add the signal to:
-
- {typeof configs !== "undefined" && configs.map(config => ( - + ))} -
-
- -
- ) + +
+ + + ); - return isShown ? DialogWindow : <> -} + return show ? DialogWindow : <>; +}; export default EditSignalMappingDialog; diff --git a/src/store/apiSlice.js b/src/store/apiSlice.js index 45433be0..cd48ffeb 100644 --- a/src/store/apiSlice.js +++ b/src/store/apiSlice.js @@ -27,7 +27,6 @@ import { fileEndpoints } from "./endpoints/file-endpoints"; import { signalEndpoints } from "./endpoints/signal-endpoints"; import { resultEndpoints } from "./endpoints/result-endpoints"; import { authEndpoints } from "./endpoints/auth-endpoints"; -import { websocketEndpoints } from "./endpoints/websocket-endpoints"; import { usergroupEndpoints } from "./endpoints/usergroup-endpoints"; import { selectToken } from "./authSlice"; @@ -54,7 +53,7 @@ export const apiSlice = createApi({ ...resultEndpoints(builder), ...signalEndpoints(builder), ...authEndpoints(builder), - ...websocketEndpoints(builder), + ...usergroupEndpoints(builder), }), }); diff --git a/src/store/dashboardSlice.js b/src/store/dashboardSlice.js deleted file mode 100644 index 23807acf..00000000 --- a/src/store/dashboardSlice.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * 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 { createSlice } from "@reduxjs/toolkit"; -import { sendMessageToWebSocket } from "./webSocketActions"; // Adjust the import path as needed - -const initialState = { - data: null, - controlID: "", - controlValue: null, - isFinalChange: false, - signals: [], - icIDs: {}, -}; - -const widgetSlice = createSlice({ - name: "widget", - initialState, - reducers: { - setData(state, action) { - state.data = action.payload; - }, - setControlID(state, action) { - state.controlID = action.payload; - }, - setControlValue(state, action) { - state.controlValue = action.payload; - }, - setIsFinalChange(state, action) { - state.isFinalChange = action.payload; - }, - setSignals(state, action) { - state.signals = action.payload; - }, - setIcIDs(state, action) { - state.icIDs = action.payload; - }, - }, -}); - -export const { - setWidget, - setData, - setControlID, - setControlValue, - setIsFinalChange, - setSignals, - setIcIDs, -} = widgetSlice.actions; - -export const inputDataChanged = - (widget, data, controlID, controlValue, isFinalChange) => - async (dispatch, getState) => { - if (controlID !== "" && isFinalChange) { - const updatedWidget = JSON.parse(JSON.stringify(widget)); - updatedWidget.customProperties[controlID] = controlValue; - - dispatch(setWidget(updatedWidget)); - } - - const state = getState().widget; - const signalID = widget.signalIDs[0]; - const signal = state.signals.filter((s) => s.id === signalID); - - if (signal.length === 0) { - console.warn( - "Unable to send signal for signal ID", - signalID, - ". Signal not found." - ); - return; - } - - const icID = state.icIDs[signal[0].id]; - dispatch( - sendMessageToWebSocket({ - message: { - ic: icID, - signalID: signal[0].id, - signalIndex: signal[0].index, - data: signal[0].scalingFactor * data, - }, - }) - ); - }; - -export default widgetSlice.reducer; diff --git a/src/store/endpoints/file-endpoints.js b/src/store/endpoints/file-endpoints.js index 765c03fd..40137f70 100644 --- a/src/store/endpoints/file-endpoints.js +++ b/src/store/endpoints/file-endpoints.js @@ -16,52 +16,52 @@ ******************************************************************************/ export const fileEndpoints = (builder) => ({ - getFiles: builder.query({ - query: (scenarioID) => ({ - url: 'files', - params: { scenarioID }, - }), + getFiles: builder.query({ + query: (scenarioID) => ({ + url: "files", + params: { scenarioID }, }), - addFile: builder.mutation({ - query: ({ scenarioID, file }) => { - const formData = new FormData(); - formData.append('inputFile', file); - return { - url: `files?scenarioID=${scenarioID}`, - method: 'POST', - body: formData, - }; - }, + }), + addFile: builder.mutation({ + query: ({ scenarioID, file }) => { + const formData = new FormData(); + formData.append("file", file); + return { + url: `files?scenarioID=${scenarioID}`, + method: "POST", + body: formData, + }; + }, + }), + downloadFile: builder.query({ + query: (fileID) => ({ + url: `files/${fileID}`, + responseHandler: "blob", + responseType: "blob", }), - downloadFile: builder.query({ - query: (fileID) => ({ - url: `files/${fileID}`, - responseHandler: 'blob', - responseType: 'blob', - }), - }), - downloadImage: builder.query({ - query: (fileID) => ({ - url: `files/${fileID}`, - method: 'GET', - responseHandler: (response) => response.blob(), - }), - }), - updateFile: builder.mutation({ - query: ({ fileID, file }) => { - const formData = new FormData(); - formData.append('inputFile', file); - return { - url: `files/${fileID}`, - method: 'PUT', - body: formData, - }; - }, + }), + downloadImage: builder.query({ + query: (fileID) => ({ + url: `files/${fileID}`, + method: "GET", + responseHandler: (response) => response.blob(), }), - deleteFile: builder.mutation({ - query: (fileID) => ({ + }), + updateFile: builder.mutation({ + query: ({ fileID, file }) => { + const formData = new FormData(); + formData.append("file", file); + return { url: `files/${fileID}`, - method: 'DELETE', - }), + method: "PUT", + body: formData, + }; + }, + }), + deleteFile: builder.mutation({ + query: (fileID) => ({ + url: `files/${fileID}`, + method: "DELETE", }), + }), }); diff --git a/src/store/endpoints/ic-endpoints.js b/src/store/endpoints/ic-endpoints.js index 438f93a9..bea8481b 100644 --- a/src/store/endpoints/ic-endpoints.js +++ b/src/store/endpoints/ic-endpoints.js @@ -28,6 +28,6 @@ export const icEndpoints = (builder) => ({ }), getICbyId: builder.query({ - query: (icID) => `/dashboards/${icID}`, + query: (icID) => `/ic/${icID}`, }), }); diff --git a/src/store/endpoints/websocket-endpoints.js b/src/store/endpoints/websocket-endpoints.js deleted file mode 100644 index cb191c52..00000000 --- a/src/store/endpoints/websocket-endpoints.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * 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 . - ******************************************************************************/ - - -function setupWebSocket(WS_URL, onMessage, protocol) { - const socket = new WebSocket(WS_URL, protocol); // Include the protocol here - socket.binaryType = 'arraybuffer'; // Set binary type - socket.onmessage = (event) => { - onMessage(event.data); - }; - return socket; -} - -const sendMessage = (message) => { - if (socket && socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify(message)); - } -}; - -export const websocketEndpoints = (builder) => ({ - getIcData: builder.query({ - query: () => ({data: []}), - async onCacheEntryAdded( - arg, - { updateCachedData, cacheDataLoaded, cacheEntryRemoved } - ) { - // create a websocket connection when the cache subscription starts - const socket = new WebSocket('wss://villas.k8s.eonerc.rwth-aachen.de/ws/ws_sig', 'live'); - socket.binaryType = 'arraybuffer'; - try { - // wait for the initial query to resolve before proceeding - await cacheDataLoaded; - - // when data is received from the socket connection to the server, - // if it is a message and for the appropriate channel, - // update our query result with the received message - const listener = (event) => { - console.log(event.data) - } - - socket.addEventListener('message', listener) - } catch { - // no-op in case `cacheEntryRemoved` resolves before `cacheDataLoaded`, - // in which case `cacheDataLoaded` will throw - } - // cacheEntryRemoved will resolve when the cache subscription is no longer active - await cacheEntryRemoved - // perform cleanup steps once the `cacheEntryRemoved` promise resolves - ws.close() - }, - }), -}); - -function bufferToMessageArray(blob) { - let offset = 0; - const msgs = []; - - while (offset < blob.byteLength) { - const msg = bufferToMessage(new DataView(blob, offset)); - if (msg !== undefined) { - msgs.push(msg); - offset += msg.blob.byteLength; - } - } - - return msgs; -} - -function bufferToMessage(data) { - if (data.byteLength === 0) { - return null; - } - - const source_index = data.getUint8(1); - const bits = data.getUint8(0); - const length = data.getUint16(0x02, 1); - const bytes = length * 4 + 16; - - return { - version: (bits >> OFFSET_VERSION) & 0xF, - type: (bits >> OFFSET_TYPE) & 0x3, - source_index: source_index, - length: length, - sequence: data.getUint32(0x04, 1), - timestamp: data.getUint32(0x08, 1) * 1e3 + data.getUint32(0x0C, 1) * 1e-6, - values: new Float32Array(data.buffer, data.byteOffset + 0x10, length), - blob: new DataView(data.buffer, data.byteOffset + 0x00, bytes), - }; -} \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js index 71f8893b..9ac5b717 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -16,21 +16,21 @@ ******************************************************************************/ import { configureStore } from "@reduxjs/toolkit"; -import icReducer from './icSlice'; -import configReducer from './configSlice' +import icReducer from "./icSlice"; +import configReducer from "./configSlice"; import { apiSlice } from "./apiSlice"; -import authReducer from './authSlice'; -import websocketReducer from './websocketSlice'; +import authReducer from "./authSlice"; +import websocketReducer from "./websocketSlice"; export const store = configureStore({ - reducer: { - auth: authReducer, - infrastructure: icReducer, - config: configReducer, - websocket: websocketReducer, - [apiSlice.reducerPath]: apiSlice.reducer, - }, - middleware: (getDefaultMiddleware) => + reducer: { + auth: authReducer, + infrastructure: icReducer, + config: configReducer, + websocket: websocketReducer, + [apiSlice.reducerPath]: apiSlice.reducer, + }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(apiSlice.middleware), - devTools: true, -}) + devTools: true, +}); diff --git a/src/store/websocketSlice.js b/src/store/websocketSlice.js index d052311c..3769d158 100644 --- a/src/store/websocketSlice.js +++ b/src/store/websocketSlice.js @@ -20,113 +20,109 @@ import { wsManager } from "../common/api/websocket-api"; import { current } from "@reduxjs/toolkit"; export const connectWebSocket = createAsyncThunk( - 'websocket/connect', - async ({ url, id, length }, { dispatch, getState }) => { - - console.log('Want to connect to', url); - - //check if we are already connected to this socket - if(getState().websocket.activeSocketURLs.includes(url)) return; - - return new Promise((resolve, reject) => { - dispatch(addActiveSocket({parameters: {id: id, url: url, length: length}})); - wsManager.connect( - id, - url, - (msgs, id) => { - const icdata = { - input: { - sequence: -1, - length: length, - version: 2, - type: 0, - timestamp: Date.now(), - values: new Array(length).fill(0), - }, - output: { - values: [], - }, - }; - - const MAX_VALUES = 10000; - - if (msgs.length > 0) { - for (let j = 0; j < msgs.length; j++) { - let smp = msgs[j]; - - if (smp.source_index !== 0) { - for (let i = 0; i < smp.length; i++) { - while (icdata.input.values.length < i + 1) { - icdata.input.values.push([]); - } - - icdata.input.values[i] = smp.values[i]; - - if (icdata.input.values[i].length > MAX_VALUES) { - const pos = icdata.input.values[i].length - MAX_VALUES; - icdata.input.values[i].splice(0, pos); - } - } - - icdata.input.timestamp = smp.timestamp; - icdata.input.sequence = smp.sequence; - } else { - for (let i = 0; i < smp.length; i++) { - while (icdata.output.values.length < i + 1) { - icdata.output.values.push([]); - } - - icdata.output.values[i].push({ x: smp.timestamp, y: smp.values[i] }); - - if (icdata.output.values[i].length > MAX_VALUES) { - const pos = icdata.output.values[i].length - MAX_VALUES; - icdata.output.values[i].splice(0, pos); - } - } - - icdata.output.timestamp = smp.timestamp; - icdata.output.sequence = smp.sequence; + "websocket/connect", + async ({ url, id, inputValues }, { dispatch, getState }) => { + //check if we are already connected to this socket + if (getState().websocket.activeSocketURLs.includes(url)) return; + + return new Promise((resolve, reject) => { + dispatch( + addActiveSocket({ + parameters: { id: id, url: url, inputValues: inputValues }, + }) + ); + wsManager.connect( + id, + url, + (msgs, id) => { + const icdata = { + input: { + version: 2, + type: 0, + sequence: -1, + length: inputValues.length, + timestamp: Date.now(), + values: inputValues, + }, + output: { + values: [], + }, + }; + + const MAX_VALUES = 10000; + + if (msgs.length > 0) { + for (let j = 0; j < msgs.length; j++) { + let smp = msgs[j]; + + for (let i = 0; i < smp.length; i++) { + while (icdata.output.values.length < i + 1) { + icdata.output.values.push([]); } + + icdata.output.values[i].push({ + x: smp.timestamp, + y: smp.values[i], + }); + + if (icdata.output.values[i].length > MAX_VALUES) { + const pos = icdata.output.values[i].length - MAX_VALUES; + icdata.output.values[i].splice(0, pos); + } + + icdata.output.timestamp = smp.timestamp; + icdata.output.sequence = smp.sequence; } - - // Dispatch the action to update the Redux state - dispatch(updateIcData({ id, newIcData: icdata })); } - }, - () => { - console.log('WebSocket connected to:', url); - resolve(); // Resolve the promise on successful connection - }, - () => { - console.log('WebSocket disconnected from:', url); - dispatch(disconnect()); - reject(); // Reject the promise if the connection is closed + + // Dispatch the action to update the Redux state + dispatch(updateIcData({ id, newIcData: icdata })); } - ); - }); - } + }, + () => { + wsManager.send(id, { + version: 2, + type: 0, + sequence: 0, + timestamp: Date.now(), + source_index: 1, + length: inputValues.length, + values: inputValues, + }); + resolve(); + }, + () => { + console.log("WebSocket disconnected from:", url); + dispatch(disconnect()); + reject(); + } + ); + }); + } ); const websocketSlice = createSlice({ - name: 'websocket', + name: "websocket", initialState: { icdata: {}, activeSocketURLs: [], - values:[] }, reducers: { addActiveSocket: (state, action) => { - const {url, id, length} = action.payload.parameters; - const currentSockets = current(state.activeSocketURLs); - state.activeSocketURLs = [...currentSockets, url]; - state.icdata[id] = {input: { - sequence: -1, - length: length, - version: 2, - type: 0, - timestamp: Date.now(), - values: new Array(length).fill(0) - }, output: {}}; + const { url, id, inputValues } = action.payload.parameters; + const currentSockets = current(state.activeSocketURLs); + state.activeSocketURLs = [...currentSockets, url]; + state.icdata[id] = { + input: { + sequence: -1, + length: inputValues.length, + version: 2, + type: 0, + timestamp: Date.now(), + values: inputValues, + }, + output: {}, + }; }, reportLength:(state,action)=>{ return { @@ -139,63 +135,70 @@ const websocketSlice = createSlice({ state.values.splice(idx,1,initVal) }, disconnect: (state, action) => { - if(action.payload){ + if (action.payload) { wsManager.disconnect(action.payload.id); // Ensure the WebSocket is disconnected } }, updateIcData: (state, action) => { const { id, newIcData } = action.payload; const currentICdata = current(state.icdata); - if(currentICdata[id].output.values){ - const {values, ...rest} = newIcData.output; + if (currentICdata[id].output.values) { + const { values, ...rest } = newIcData.output; let oldValues = [...currentICdata[id].output.values]; - for(let i = 0; i < newIcData.output.values.length; i++){ - oldValues[i] = [...oldValues[i], ...values[i]] + for (let i = 0; i < newIcData.output.values.length; i++) { + oldValues[i] = [...oldValues[i], ...values[i]]; } state.icdata[id] = { - input: newIcData.input, - output: { - ...rest, - values: oldValues - } - } + input: newIcData.input, + output: { + ...rest, + values: oldValues, + }, + }; } else { state.icdata[id] = { ...newIcData, }; } }, + //widget changes a value in input values array and they are sent to the websocket sendMessageToWebSocket: (state, action) => { - const { ic, signalID, signalIndex, data} = action.payload.message; - const currentICdata = current(state.icdata); - state.values[signalIndex] = data - const values = current(state.values); - if (!(ic == null || currentICdata[ic].input == null)) { - const inputAction = JSON.parse(JSON.stringify(currentICdata[ic].input)); - // update message properties - inputAction.timestamp = Date.now(); - inputAction.sequence++; - inputAction.values = values; - inputAction.length = inputAction.values.length; - inputAction.source_index = signalID; - // The previous line sets the source_index field of the message to the ID of the signal - // so that upon loopback through VILLASrelay the value can be mapped to correct signal - - state.icdata[ic].input = inputAction; - let input = JSON.parse(JSON.stringify(inputAction)); - wsManager.send(ic, input); + const { ic, signalID, signalIndex, data } = action.payload.message; + + if (ic != null && state.icdata[ic] && state.icdata[ic].input) { + const inputMessage = state.icdata[ic].input; + + inputMessage.timestamp = Date.now(); + inputMessage.sequence++; + + //replace only the value of the taget signal if rest of the payload is valid + if ( + Array.isArray(inputMessage.values) && + signalIndex >= 0 && + signalIndex < inputMessage.values.length + ) { + inputMessage.values[signalIndex] = data; } - } + + inputMessage.length = inputMessage.values.length; + inputMessage.source_index = signalID; + + wsManager.send(ic, JSON.parse(JSON.stringify(inputMessage))); + } + }, }, extraReducers: (builder) => { - builder.addCase(connectWebSocket.fulfilled, (state, action) => { - // Handle the fulfilled state if needed - }); + builder.addCase(connectWebSocket.fulfilled, (state, action) => {}); builder.addCase(connectWebSocket.rejected, (state, action) => { - console.log('error', action); + console.log("error", action); }); }, }); -export const { disconnect, updateIcData, addActiveSocket, sendMessageToWebSocket,reportLength,initValue } = websocketSlice.actions; +export const { + disconnect, + updateIcData, + addActiveSocket, + sendMessageToWebSocket, +} = websocketSlice.actions; export default websocketSlice.reducer; From fb4ea522f3f88630aee1d8fcffc78efb4e47062f Mon Sep 17 00:00:00 2001 From: Andrii Podriez Date: Thu, 17 Apr 2025 12:47:28 +0200 Subject: [PATCH 10/12] Update Button Widget Implement toggle mode Adjust size of the box for the WidgetPlayer fix: missing data after merge/rebase Signed-off-by: Andrii Podriez Signed-off-by: SystemsPurge --- package.json | 2 +- src/app.js | 10 ++ src/common/api/websocket-api.js | 3 +- src/pages/dashboards/dashboard.jsx | 7 +- .../dashboards/hooks/use-dashboard-data.js | 1 - .../hooks/use-websocket-connection.js | 8 +- .../dashboards/widget/widget-factory.jsx | 4 +- src/pages/dashboards/widget/widget.js | 28 ---- .../dashboards/widget/widgets/button.jsx | 101 ++++--------- src/pages/dashboards/widget/widgets/input.jsx | 6 +- .../ic-pages/kubernetes-ic-page.js | 2 +- src/pages/infrastructure/infrastructure.js | 2 +- .../scenarios/dialogs/edit-signal-mapping.js | 3 +- src/pages/scenarios/tables/configs-table.js | 2 +- src/pages/users/users.js | 134 ++++++++++++++---- src/store/apiSlice.js | 1 + src/store/authSlice.js | 1 + src/store/websocketSlice.js | 33 +++-- 18 files changed, 188 insertions(+), 160 deletions(-) delete mode 100644 src/pages/dashboards/widget/widget.js diff --git a/package.json b/package.json index 641961bb..1e8cd18a 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, - "proxy": "https://slew.k8s.eonerc.rwth-aachen.de/", + "proxy": "https://villas.k8s.eonerc.rwth-aachen.de/", "browserslist": { "production": [ ">0.2%", diff --git a/src/app.js b/src/app.js index ce3074ed..ab9c4a71 100644 --- a/src/app.js +++ b/src/app.js @@ -49,6 +49,16 @@ const App = () => { return decodedToken.exp < timeNow; }; + const notificationSystem = useRef(null); + + useEffect(() => { + NotificationsDataManager.setSystem(notificationSystem.current); + + return () => { + NotificationsDataManager.setSystem(null); + }; + }, []); + const { isAuthenticated, token, user } = useSelector((state) => state.auth); if (!isAuthenticated || isTokenExpired(token)) { diff --git a/src/common/api/websocket-api.js b/src/common/api/websocket-api.js index a985d70d..6c33b642 100644 --- a/src/common/api/websocket-api.js +++ b/src/common/api/websocket-api.js @@ -54,13 +54,12 @@ class WebSocketManager { } send(id, message) { - console.log("MESSAGE", message); const socket = this.sockets.find((s) => s.id === id); if (socket == null) { return false; } const data = this.messageToBuffer(message); - console.log("📤 Sending binary buffer to server:", new Uint8Array(data)); + console.log("📤 Sending binary buffer to server:", message.values); socket.socket.send(data); return true; diff --git a/src/pages/dashboards/dashboard.jsx b/src/pages/dashboards/dashboard.jsx index b4a81a3d..8daafbf1 100644 --- a/src/pages/dashboards/dashboard.jsx +++ b/src/pages/dashboards/dashboard.jsx @@ -36,7 +36,6 @@ import useWebSocketConnection from "./hooks/use-websocket-connection.js"; import { useGetDashboardQuery, - useGetICSQuery, useLazyGetWidgetsQuery, useLazyGetFilesQuery, useAddWidgetMutation, @@ -48,9 +47,7 @@ import { useAddFileMutation, useUpdateFileMutation, } from "../../store/apiSlice"; -import { useState } from "react"; -import DashboardLayout from "./dashboard-layout"; -import ErrorBoundary from "./dashboard-error-boundry"; +import { Spinner } from "react-bootstrap"; const startUpdaterWidgets = new Set(["Slider", "Button", "NumberInput"]); @@ -612,4 +609,4 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { ); }; -export default Dashboard; +export default Fullscreenable()(Dashboard); diff --git a/src/pages/dashboards/hooks/use-dashboard-data.js b/src/pages/dashboards/hooks/use-dashboard-data.js index 4639eaf1..6734dd0c 100644 --- a/src/pages/dashboards/hooks/use-dashboard-data.js +++ b/src/pages/dashboards/hooks/use-dashboard-data.js @@ -52,7 +52,6 @@ export const useDashboardData = (scenarioID) => { // Fetch configs and signals const configsRes = await triggerGetConfigs(scenarioID).unwrap(); - console.log("GOT CONFIGS", configsRes); if (configsRes?.configs) { setConfigs(configsRes.configs); diff --git a/src/pages/dashboards/hooks/use-websocket-connection.js b/src/pages/dashboards/hooks/use-websocket-connection.js index 96f7f818..30a9c357 100644 --- a/src/pages/dashboards/hooks/use-websocket-connection.js +++ b/src/pages/dashboards/hooks/use-websocket-connection.js @@ -51,12 +51,14 @@ const useWebSocketConnection = (activeICS, signals, widgets) => { ); if ( matchingSignal && - typeof matchingSignal.index === "number" && + !isNaN(matchingSignal.index) && matchingSignal.index < inputValues.length ) { if (widget.type == "Button") { - inputValues[matchingSignal.index] = - widget.customProperties.off_value; + inputValues[matchingSignal.index] = widget.customProperties + .pressed + ? widget.customProperties.on_value + : widget.customProperties.off_value; } else { inputValues[matchingSignal.index] = widget.customProperties.value; diff --git a/src/pages/dashboards/widget/widget-factory.jsx b/src/pages/dashboards/widget/widget-factory.jsx index 7118161a..a3bb5947 100644 --- a/src/pages/dashboards/widget/widget-factory.jsx +++ b/src/pages/dashboards/widget/widget-factory.jsx @@ -204,8 +204,8 @@ const widgetsMap = { Player: { minWidth: 144, minHeight: 226, - width: 400, - height: 606, + width: 144, + height: 226, customProperties: { configIDs: [], uploadResults: false, diff --git a/src/pages/dashboards/widget/widget.js b/src/pages/dashboards/widget/widget.js deleted file mode 100644 index d3c74c52..00000000 --- a/src/pages/dashboards/widget/widget.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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 WidgetButton from "./widgets/button"; - -const Widget = ({ widget, editing }) => { - const widgetTypeMap = { - Button: , - }; - - return widgetTypeMap["Button"]; -}; - -export default Widget; diff --git a/src/pages/dashboards/widget/widgets/button.jsx b/src/pages/dashboards/widget/widgets/button.jsx index 0fc56180..bf822b77 100644 --- a/src/pages/dashboards/widget/widgets/button.jsx +++ b/src/pages/dashboards/widget/widgets/button.jsx @@ -15,73 +15,26 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Button } from "react-bootstrap"; - -const WidgetButton = ({ widget, editing }) => { +import { useUpdateWidgetMutation } from "../../../../store/apiSlice"; +const WidgetButton = ({ widget, editing, onInputChanged }) => { const [pressed, setPressed] = useState(widget.customProperties.pressed); + const [toggle, setToggle] = useState(widget.customProperties.toggle); + const [updateWidget] = useUpdateWidgetMutation(); - // useEffect(() => { - // let widget = props.widget; - // widget.customProperties.simStartedSendValue = false; - // widget.customProperties.pressed = false; - - // // AppDispatcher.dispatch({ - // // type: 'widgets/start-edit', - // // token: props.token, - // // data: widget - // // }); - - // // Effect cleanup - // return () => { - // // Clean up if needed - // }; - // }, [props.token, props.widget]); - - // useEffect(() => { - // if (props.widget.customProperties.simStartedSendValue) { - // let widget = props.widget; - // widget.customProperties.simStartedSendValue = false; - // widget.customProperties.pressed = false; - // AppDispatcher.dispatch({ - // type: 'widgets/start-edit', - // token: props.token, - // data: widget - // }); - - // props.onInputChanged(widget.customProperties.off_value, '', false, false); - // } - // }, [props, setPressed]); - - // useEffect(() => { - // setPressed(props.widget.customProperties.pressed); - // }, [props.widget.customProperties.pressed]); + //this ref is used for capturing last value of pressed so that it can be saved and sent on unmount + const pressedRef = useRef(pressed); - // const onPress = (e) => { - // if (e.button === 0 && !props.widget.customProperties.toggle) { - // valueChanged(props.widget.customProperties.on_value, true); - // } - // }; - - // const onRelease = (e) => { - // if (e.button === 0) { - // let nextState = false; - // if (props.widget.customProperties.toggle) { - // nextState = !pressed; - // } - // valueChanged(nextState ? props.widget.customProperties.on_value : props.widget.customProperties.off_value, nextState); - // } - // }; - - // const valueChanged = (newValue, newPressed) => { - // if (props.onInputChanged) { - // props.onInputChanged(newValue, 'pressed', newPressed, true); - // } - // setPressed(newPressed); - // }; + useEffect(() => { + return () => { + //if button is in toggle-mode, we want to save its pressed state for future reloads of dashboard + if (toggle) updateSimStartedAndPressedValues(false, pressedRef.current); + }; + }, []); useEffect(() => { - updateSimStartedAndPressedValues(false, false); + setToggle(widget.customProperties.toggle); }, [widget]); const updateSimStartedAndPressedValues = async (isSimStarted, isPressed) => { @@ -105,13 +58,16 @@ const WidgetButton = ({ widget, editing }) => { }; useEffect(() => { - if (widget.customProperties.simStartedSendValue) { - let widgetCopy = { ...widget }; - widgetCopy.customProperties.simStartedSendValue = false; - widgetCopy.customProperties.pressed = false; - - onInputChanged(widget.customProperties.off_value, "", false, false); - } + pressedRef.current = pressed; + + onInputChanged( + pressed + ? widget.customProperties.on_value + : widget.customProperties.off_value, + "", + false, + false + ); }, [pressed]); let opacity = widget.customProperties.background_color_opacity; @@ -129,8 +85,13 @@ const WidgetButton = ({ widget, editing }) => { style={buttonStyle} active={pressed} disabled={editing} - onMouseDown={(e) => setPressed(true)} - onMouseUp={(e) => setPressed(false)} + onMouseDown={(e) => { + if (!toggle) setPressed(true); + else setPressed(!pressed); + }} + onMouseUp={(e) => { + if (!toggle) setPressed(false); + }} > {widget.name} diff --git a/src/pages/dashboards/widget/widgets/input.jsx b/src/pages/dashboards/widget/widgets/input.jsx index f9eb4a4b..d19fe776 100644 --- a/src/pages/dashboards/widget/widgets/input.jsx +++ b/src/pages/dashboards/widget/widgets/input.jsx @@ -33,11 +33,9 @@ const WidgetInput = ({ signals, widget, editing, onInputChanged }) => { useEffect(() => { if (widget.customProperties.simStartedSendValue) { widget.customProperties.simStartedSendValue = false; - if(props.onInputChanged && props.signals && props.signals.length > 0){ - props.onInputChanged(widget.customProperties.value, "", "", false); + if(onInputChanged && signals && signals.length > 0){ + onInputChanged(widget.customProperties.value, "", "", false); } - }, [props.widget]); - updateWidgetSimStatus(false); onInputChanged(Number(value), "", false, false); diff --git a/src/pages/infrastructure/ic-pages/kubernetes-ic-page.js b/src/pages/infrastructure/ic-pages/kubernetes-ic-page.js index 96e7b1ea..0180755b 100644 --- a/src/pages/infrastructure/ic-pages/kubernetes-ic-page.js +++ b/src/pages/infrastructure/ic-pages/kubernetes-ic-page.js @@ -15,7 +15,7 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import { Col, Row } from "react-bootstrap"; +import { Col, Row, Table} from "react-bootstrap"; import IconButton from "../../../common/buttons/icon-button"; import RawDataTable from "../../../common/rawDataTable"; import { useDispatch, useSelector } from "react-redux"; diff --git a/src/pages/infrastructure/infrastructure.js b/src/pages/infrastructure/infrastructure.js index 62080812..03ee44eb 100644 --- a/src/pages/infrastructure/infrastructure.js +++ b/src/pages/infrastructure/infrastructure.js @@ -166,7 +166,7 @@ const Infrastructure = () => { - {currentUser.role === "Admin" ? : null} + {currentUser.role === "Admin" ? : null}
{ const signalToUpdate = { ...updatedSignals[row] }; switch (column) { case 1: - signalToUpdate.index = e.target.value; + signalToUpdate.index = Number(e.target.value); break; case 2: signalToUpdate.name = e.target.value; @@ -114,6 +114,7 @@ const EditSignalMappingDialog = ({ show, direction, onClose, configID }) => { ); if (signalToUpdate) { + console.log(signalToUpdate) await updateSignal({ signalID: id, updatedSignal: signalToUpdate, diff --git a/src/pages/scenarios/tables/configs-table.js b/src/pages/scenarios/tables/configs-table.js index 7e9c63f1..e8699b39 100644 --- a/src/pages/scenarios/tables/configs-table.js +++ b/src/pages/scenarios/tables/configs-table.js @@ -470,7 +470,7 @@ const ConfigsTable = ({scenario, ics}) => { onClose={(c) => deleteConfig(c)} /> setIsEditSignalMappingModalOpened(false)} configID={signalMappingConfigID} diff --git a/src/pages/users/users.js b/src/pages/users/users.js index b5890dea..6e9c5e90 100644 --- a/src/pages/users/users.js +++ b/src/pages/users/users.js @@ -1,6 +1,6 @@ import { useState } from "react"; import { useSelector } from "react-redux"; -import { Dropdown, DropdownButton } from 'react-bootstrap'; +import { Dropdown, DropdownButton, Spinner, Row, Col } from 'react-bootstrap'; import { Table, ButtonColumn, CheckboxColumn, DataColumn } from "../../common/table"; import Icon from "../../common/icon"; import IconButton from "../../common/buttons/icon-button"; @@ -11,6 +11,7 @@ import DeleteDialog from "../../common/dialogs/delete-dialog"; import { buttonStyle, iconStyle } from "./styles"; import NotificationsFactory from "../../common/data-managers/notifications-factory"; import notificationsDataManager from "../../common/data-managers/notifications-data-manager"; +import Usergroups from "../usergroups/usergroups"; import { useGetUsersQuery, useAddUserMutation, @@ -18,26 +19,31 @@ import { useDeleteUserMutation, useGetScenariosQuery, useAddUserToScenarioMutation, + useGetUsergroupsQuery, + useAddUserToUsergroupMutation } from "../../store/apiSlice"; -const Users = ({}) => { +const Users = () => { const { user: currentUser, token: sessionToken } = useSelector((state) => state.auth); const {data: fetchedUsers, refetch: refetchUsers} = useGetUsersQuery(); const users = fetchedUsers ? fetchedUsers.users : []; - const { data: fetchedScenarios } = useGetScenariosQuery(); - const scenarios = fetchedScenarios ? fetchedScenarios.scenarios : []; + const { data: {scenarios} = [], isLoading: isLoadingScenarios } = useGetScenariosQuery(); + const {data: {usergroups} = [], isLoading: isLoadingUsergroups } = useGetUsergroupsQuery(); const [checkedUsersIDs, setCheckedUsersIDs] = useState([]); const [addUserMutation] = useAddUserMutation(); const [updateUserMutation] = useUpdateUserMutation(); const [deleteUserMutation] = useDeleteUserMutation(); const [addUserToScenarioMutation] = useAddUserToScenarioMutation(); + const [addUserToUsergroup] = useAddUserToUsergroupMutation(); const [isNewModalOpened, setIsNewModalOpened] = useState(false); const [isEditModalOpened, setIsEditModalOpened] = useState(false); const [isDeleteModalOpened, setIsDeleteModalOpened] = useState(false); const [scenario, setScenario] = useState({name: ''}); + const [usergroup, setUsergroup] = useState({name: ''}); const [isUsersToScenarioModalOpened, setUsersToScenarioModalOpened] = useState(false); + const [isUsersToUsegroupModalOpened, setUsersToUsegroupModalOpened] = useState(false); const [userToEdit, setUserToEdit] = useState({}); const [userToDelete, setUserToDelete] = useState({}); const [areAllUsersChecked, setAreAllUsersChecked] = useState(false); @@ -62,6 +68,10 @@ const Users = ({}) => { setIsNewModalOpened(false); } + const getIconForActiveColumn = (active) => { + return + } + const handleEditUser = async (data) => { if(data){ try { @@ -106,7 +116,6 @@ const Users = ({}) => { await addUserToScenarioMutation({ scenarioID: scenario.id, username: users.find(u => u.id === checkedUsersIDs[i]).username }).unwrap(); } } catch (error) { - console.log('ERROR', error) if(error.data){ notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(error.data.message)); } else { @@ -121,6 +130,28 @@ const Users = ({}) => { setAreAllUsersChecked(false); } + const handleAddUsersToUsergroup = async (isCanceled) => { + if(!isCanceled){ + try { + for(let i = 0; i < checkedUsersIDs.length; i++){ + await addUserToUsergroup({ usergroupID: usergroup.id, username: users.find(u => u.id === checkedUsersIDs[i]).username }).unwrap(); + } + } catch (error) { + if(error.data){ + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(error.data.message)); + } else { + console.log("error", error) + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR("Unknown error")); + } + } + } + + setUsersToUsegroupModalOpened(false); + setCheckedUsersIDs([]); + setUsergroup({name: ''}); + setAreAllUsersChecked(false); + } + const toggleCheckAllUsers = () => { if(checkedUsersIDs.length === users.length){ setCheckedUsersIDs([]); @@ -188,7 +219,7 @@ const Users = ({}) => { - {}}/> + getIconForActiveColumn(active)}/> { }} /> - - { - let scenario; - if(scenarios.length > 0) { - scenario = scenarios.find(s => s.id == id); - } - setScenario(scenario); - setUsersToScenarioModalOpened(true); - }} - > - {scenarios.map(scenario => ( - {scenario.name} - ))} - - - - checkedUsersIDs.includes(user.id))} - scenario={scenario.name} - onClose={(canceled) => handleAddUserToScenario(canceled)} - /> + + + + {isLoadingScenarios? : <> + + { + let scenario; + if(scenarios.length > 0) { + scenario = scenarios.find(s => s.id == id); + } + setScenario(scenario); + setUsersToScenarioModalOpened(true); + }} + > + {scenarios.map(scenario => ( + {scenario.name} + ))} + + + + + checkedUsersIDs.includes(user.id))} + scenario={scenario.name} + onClose={(canceled) => handleAddUserToScenario(canceled)} + /> + } + + + + {isLoadingUsergroups? : <> + + { + let usergroup; + if(usergroups.length > 0) { + usergroup = usergroups.find(s => s.id == id); + } + setUsergroup(usergroup); + setUsersToUsegroupModalOpened(true); + }} + > + {usergroups.map(usergroup => ( + {usergroup.name} + ))} + + + + {/* re-using same modal to implement adding suers to usergroup */} + checkedUsersIDs.includes(user.id))} + scenario={usergroup.name} + onClose={(canceled) => handleAddUsersToUsergroup(canceled)} + /> + } + + + handleAddNewUser(data)} @@ -243,6 +313,10 @@ const Users = ({}) => { show={isDeleteModalOpened} onClose={(e) => handleDeleteUser(e)} /> + +
+ +
) } diff --git a/src/store/apiSlice.js b/src/store/apiSlice.js index cd48ffeb..afd56e1a 100644 --- a/src/store/apiSlice.js +++ b/src/store/apiSlice.js @@ -114,4 +114,5 @@ export const { useDeleteUserFromUsergroupMutation, useUpdateUsergroupMutation, useGetWidgetsQuery, + useLazyGetICbyIdQuery, } = apiSlice; diff --git a/src/store/authSlice.js b/src/store/authSlice.js index b0cf1f1a..5e408394 100644 --- a/src/store/authSlice.js +++ b/src/store/authSlice.js @@ -66,6 +66,7 @@ const authSlice = createSlice({ ); }, }); +export const selectToken = (state) => state.auth.token; export const { setUser, deleteUser } = authSlice.actions; diff --git a/src/store/websocketSlice.js b/src/store/websocketSlice.js index 3769d158..2312a093 100644 --- a/src/store/websocketSlice.js +++ b/src/store/websocketSlice.js @@ -148,17 +148,30 @@ const websocketSlice = createSlice({ for (let i = 0; i < newIcData.output.values.length; i++) { oldValues[i] = [...oldValues[i], ...values[i]]; } - state.icdata[id] = { - input: newIcData.input, - output: { - ...rest, - values: oldValues, - }, - }; + return { + ...state, + icdata:{ + ...state.icdata, + [id]:{ + ...state.icdata[id], + output:{ + ...rest, + values:oldValues + } + } + } + } } else { - state.icdata[id] = { - ...newIcData, - }; + console.log(newIcData) + return { + ...state, + icdata:{ + ...state.icdata, + [id]:{ + ...newIcData + } + } + } } }, //widget changes a value in input values array and they are sent to the websocket From aa24c1fb84de29682c1154fc219a0cba5eeb8ceb Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Thu, 6 Nov 2025 09:45:14 +0100 Subject: [PATCH 11/12] fix: replace hard coded URL, consider dev case (proxy != window.location) Signed-off-by: SystemsPurge --- src/pages/dashboards/widget/widgets/player.jsx | 4 ++-- src/pages/scenarios/dialogs/result-python-dialog.js | 3 ++- src/pages/scenarios/tables/config-action-board.js | 4 ++-- src/url.js | 4 ++++ 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 src/url.js diff --git a/src/pages/dashboards/widget/widgets/player.jsx b/src/pages/dashboards/widget/widgets/player.jsx index d257bb72..177ead07 100644 --- a/src/pages/dashboards/widget/widgets/player.jsx +++ b/src/pages/dashboards/widget/widgets/player.jsx @@ -34,9 +34,9 @@ import { } from "../../../../store/apiSlice"; import notificationsDataManager from "../../../../common/data-managers/notifications-data-manager"; import NotificationsFactory from "../../../../common/data-managers/notifications-factory"; -import { start } from "xstate/lib/actions"; import FileSaver from "file-saver"; import { useDispatch } from "react-redux"; +import __origin from "../../../../url"; const WidgetPlayer = ({ widget, @@ -172,7 +172,7 @@ const WidgetPlayer = ({ }) .then((v) => { pld.results = { - url: `https://slew.k8s.eonerc.rwth-aachen.de/api/v2/results/${v.data.result.id}/file`, + url: `https://${__origin}/api/v2/results/${v.data.result.id}/file`, type: "url", token: sessionToken, }; diff --git a/src/pages/scenarios/dialogs/result-python-dialog.js b/src/pages/scenarios/dialogs/result-python-dialog.js index f86b53a0..cd1bc069 100644 --- a/src/pages/scenarios/dialogs/result-python-dialog.js +++ b/src/pages/scenarios/dialogs/result-python-dialog.js @@ -22,6 +22,7 @@ import Dialog from '../../../common/dialogs/dialog'; import {CopyToClipboard} from 'react-copy-to-clipboard'; import SyntaxHighlighter from 'react-syntax-highlighter'; import { github } from 'react-syntax-highlighter/dist/esm/styles/hljs'; +import __origin from '../../../url'; class ResultPythonDialog extends React.Component { villasDataProcessingUrl = 'https://pypi.org/project/villas-dataprocessing/'; @@ -105,7 +106,7 @@ class ResultPythonDialog extends React.Component { code_snippets.push(code_imports) /* Result object */ - code_snippets.push(`r = Result(${result.id}, '${token}', endpoint='https://slew.k8s.eonerc.rwth-aachen.de')`); + code_snippets.push(`r = Result(${result.id}, '${token}', endpoint='https://${__origin}')`); /* Examples */ code_snippets.push(`# Get result metadata diff --git a/src/pages/scenarios/tables/config-action-board.js b/src/pages/scenarios/tables/config-action-board.js index 38170a24..42db6065 100644 --- a/src/pages/scenarios/tables/config-action-board.js +++ b/src/pages/scenarios/tables/config-action-board.js @@ -24,6 +24,7 @@ import { sessionToken } from '../../../localStorage'; import { useSendActionMutation, useAddResultMutation, useLazyGetSignalsQuery, useGetResultsQuery } from '../../../store/apiSlice'; import NotificationsFactory from "../../../common/data-managers/notifications-factory"; import notificationsDataManager from "../../../common/data-managers/notifications-data-manager"; +import __origin from '../../../url'; const ConfigActionBoard = ({selectedConfigs, scenarioID}) => { let pickedTime = new Date(); @@ -82,9 +83,8 @@ const ConfigActionBoard = ({selectedConfigs, scenarioID}) => { const res = await addResult({result: newResult}).unwrap(); if(!isErrorAddingResult){ - const url = window.location.origin; action.results = { - url: `https://slew.k8s.eonerc.rwth-aachen.de/api/v2/results/${res.result.id}/file`, + url: `https://${__origin}/api/v2/results/${res.result.id}/file`, type: "url", token: sessionToken } diff --git a/src/url.js b/src/url.js new file mode 100644 index 00000000..554ddebf --- /dev/null +++ b/src/url.js @@ -0,0 +1,4 @@ + +var dev = true +const __origin = dev ? "slew.k8s.eonerc.rwth-aachen.de" : window.location.origin +export default __origin; \ No newline at end of file From 847e7a9f593647109d2c7cebb2e9c1e1b248ef9a Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Thu, 6 Nov 2025 09:51:06 +0100 Subject: [PATCH 12/12] fix: remove old unused code Signed-off-by: SystemsPurge --- src/pages/dashboards/dashboard-old.js | 618 ---------------------- src/pages/dashboards/widget/widget-old.js | 290 ---------- src/url.js | 2 +- 3 files changed, 1 insertion(+), 909 deletions(-) delete mode 100644 src/pages/dashboards/dashboard-old.js delete mode 100644 src/pages/dashboards/widget/widget-old.js diff --git a/src/pages/dashboards/dashboard-old.js b/src/pages/dashboards/dashboard-old.js deleted file mode 100644 index 50910304..00000000 --- a/src/pages/dashboards/dashboard-old.js +++ /dev/null @@ -1,618 +0,0 @@ -/** - * 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, { useState, useEffect, useCallback, useRef, act } from "react"; -import { useDispatch, useSelector } from "react-redux"; -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 EditSignalMappingDialog from "../scenarios/dialogs/edit-signal-mapping"; -import WidgetToolbox from "./widget/widget-toolbox"; -import WidgetArea from "./grid/widget-area"; -import DashboardButtonGroup from "./grid/dashboard-button-group"; -import IconToggleButton from "../../common/buttons/icon-toggle-button"; -import WidgetContainer from "./widget/widget-container"; -import Widget from "./widget/widget-old"; - -import { connectWebSocket, disconnect } from "../../store/websocketSlice"; - -import { - useGetDashboardQuery, - useLazyGetWidgetsQuery, - useLazyGetConfigsQuery, - useAddWidgetMutation, - useUpdateWidgetMutation, - useDeleteWidgetMutation, - useLazyGetFilesQuery, - useUpdateDashboardMutation, - useGetICSQuery, - useLazyGetSignalsQuery, -} from "../../store/apiSlice"; - -const startUpdaterWidgets = new Set(["Slider", "Button", "NumberInput"]); - -const Dashboard = ({ isFullscreen, toggleFullscreen }) => { - const dispatch = useDispatch(); - const params = useParams(); - const { - data: dashboardRes, - error: dashboardError, - isLoading: isDashboardLoading, - } = useGetDashboardQuery(params.dashboard); - const dashboard = dashboardRes ? dashboardRes.dashboard : {}; - const { data: icsRes } = useGetICSQuery(); - const ics = icsRes ? icsRes.ics : []; - - const [triggerGetWidgets] = useLazyGetWidgetsQuery(); - const [triggerGetConfigs] = useLazyGetConfigsQuery(); - const [triggerGetFiles] = useLazyGetFilesQuery(); - const [triggerGetSignals] = useLazyGetSignalsQuery(); - const [addWidget] = useAddWidgetMutation(); - const [updateWidget] = useUpdateWidgetMutation(); - const [deleteWidgetMutation] = useDeleteWidgetMutation(); - const [updateDashboard] = useUpdateDashboardMutation(); - - const [widgets, setWidgets] = useState([]); - const [widgetsToUpdate, setWidgetsToUpdate] = useState([]); - const [configs, setConfigs] = useState([]); - const [signals, setSignals] = useState([]); - const [sessionToken, setSessionToken] = useState( - localStorage.getItem("token") - ); - const [files, setFiles] = 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); - - const [height, setHeight] = useState(10); - const [grid, setGrid] = useState(50); - const [newHeightValue, setNewHeightValue] = useState(0); - - //ics that are included in configurations - const [activeICS, setActiveICS] = useState([]); - - useEffect(() => { - let usedICS = []; - for (const config of configs) { - usedICS.push(config.icID); - } - setActiveICS(ics.filter((i) => usedICS.includes(i.id))); - }, [configs]); - - const activeSocketURLs = useSelector( - (state) => state.websocket.activeSocketURLs - ); - - //connect to websockets - useEffect(() => { - activeICS.forEach((i) => { - if (i.websocketurl) { - if (!activeSocketURLs.includes(i.websocketurl)) - dispatch(connectWebSocket({ url: i.websocketurl, id: i.id })); - } - }); - - return () => { - activeICS.forEach((i) => { - dispatch(disconnect({ id: i.id })); - }); - }; - }, [activeICS]); - - //as soon as dashboard is loaded, load widgets, configs, signals and files for this dashboard - useEffect(() => { - if (dashboard.id) { - fetchWidgets(dashboard.id); - fetchWidgetData(dashboard.scenarioID); - setHeight(dashboard.height); - setGrid(dashboard.grid); - } - }, [dashboard]); - - const fetchWidgets = async (dashboardID) => { - try { - const widgetsRes = await triggerGetWidgets(dashboardID).unwrap(); - if (widgetsRes.widgets) { - setWidgets(widgetsRes.widgets); - } - } catch (err) { - console.log("error fetching data", err); - } - }; - - const fetchWidgetData = async (scenarioID) => { - try { - const filesRes = await triggerGetFiles(scenarioID).unwrap(); - if (filesRes.files) { - setFiles(filesRes.files); - } - const configsRes = await triggerGetConfigs(scenarioID).unwrap(); - if (configsRes.configs) { - setConfigs(configsRes.configs); - //load signals if there are any configs - - if (configsRes.configs.length > 0) { - for (const config of configsRes.configs) { - const signalsInRes = await triggerGetSignals({ - configID: config.id, - direction: "in", - }).unwrap(); - const signalsOutRes = await triggerGetSignals({ - configID: config.id, - direction: "out", - }).unwrap(); - setSignals((prevState) => [ - ...signalsInRes.signals, - ...signalsOutRes.signals, - ...prevState, - ]); - } - } - } - } catch (err) { - console.log("error fetching data", 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) => { - setWidgetsToUpdate((prevWidgetsToUpdate) => [ - ...prevWidgetsToUpdate, - widget.id, - ]); - setWidgets((prevWidgets) => - prevWidgets.map((w) => (w.id === widget.id ? { ...widget } : w)) - ); - - // 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: 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 = async () => { - // 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); - // } - // }); - - if (height !== dashboard.height || grid !== dashboard.grid) { - try { - const { height: oldHeight, grid: oldGrid, ...rest } = dashboard; - await updateDashboard({ - dashboardID: dashboard.id, - dashboard: { height: height, grid: grid, ...rest }, - }).unwrap(); - } catch (err) { - console.log("error", err); - } - } - - if (widgetsToUpdate.length > 0) { - try { - for (const index in widgetsToUpdate) { - await updateWidget({ - widgetID: widgetsToUpdate[index], - updatedWidget: { - widget: { - ...widgets.find((w) => w.id == widgetsToUpdate[index]), - }, - }, - }).unwrap(); - } - fetchWidgets(dashboard.id); - } 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); - // } - // } - // }); - fetchWidgets(dashboard.id); - setEditing(false); - setWidgetChangeData([]); - setHeight(dashboard.height); - setGrid(dashboard.grid); - }; - - const updateGrid = (value) => { - setGrid(value); - }; - - const updateHeight = (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) { - setHeight((prevState) => prevState - 50); - } - } else { - setHeight((prevState) => prevState + 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 boxClasses = classNames("section", "box", { - "fullscreen-padding": isFullscreen, - }); - - 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} - > - onSimulationStarted()} - /> - -
- ))} -
- - - - {/* */} - - -
-
- ); -}; - -export default Fullscreenable()(Dashboard); diff --git a/src/pages/dashboards/widget/widget-old.js b/src/pages/dashboards/widget/widget-old.js deleted file mode 100644 index f29fe830..00000000 --- a/src/pages/dashboards/widget/widget-old.js +++ /dev/null @@ -1,290 +0,0 @@ -/** - * 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 { useState, useEffect } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import WidgetLabel from "./widgets/label.jsx"; -import WidgetLine from "./widgets/line.jsx"; -import WidgetBox from "./widgets/box.jsx"; -import WidgetImage from "./widgets/image.jsx"; -import WidgetPlot from "./widgets/plot.jsx"; -import WidgetTable from "./widgets/table.jsx"; -import WidgetValue from "./widgets/value.jsx"; -import WidgetLamp from "./widgets/lamp.jsx"; -import WidgetGauge from "./widgets/gauge.jsx"; -import WidgetTimeOffset from "./widgets/time-offset.jsx"; -import WidgetICstatus from "./widgets/icstatus.jsx"; -// import WidgetCustomAction from './widgets/custom-action'; -// import WidgetAction from './widgets/action'; -import WidgetButton from "./widgets/button.jsx"; -import WidgetInput from "./widgets/input.jsx"; -import WidgetSlider from "./widgets/slider.jsx"; -// import WidgetTopology from './widgets/topology'; -import WidgetPlayer from "./widgets/player.jsx"; -//import WidgetHTML from './widgets/html'; -import "../../../styles/widgets.css"; -import { useUpdateWidgetMutation } from "../../../store/apiSlice.js"; -import { sendMessageToWebSocket } from "../../../store/websocketSlice.js"; -import { useGetResultsQuery } from "../../../store/apiSlice.js"; - -const Widget = ({ - widget, - editing, - files, - configs, - signals, - paused, - ics, - scenarioID, - onSimulationStarted, -}) => { - const dispatch = useDispatch(); - const { token: sessionToken } = useSelector((state) => state.auth); - const { data, refetch: refetchResults } = useGetResultsQuery(scenarioID); - const results = data ? data.results : []; - const [icIDs, setICIDs] = useState([]); - - const icdata = useSelector((state) => state.websocket.icdata); - - const [websockets, setWebsockets] = useState([]); - const activeSocketURLs = useSelector( - (state) => state.websocket.activeSocketURLs - ); - const [update] = useUpdateWidgetMutation(); - - useEffect(() => { - if (activeSocketURLs.length > 0) { - activeSocketURLs.forEach((url) => { - setWebsockets((prevState) => [ - ...prevState, - { url: url.replace(/^wss:\/\//, "https://"), connected: true }, - ]); - }); - } - }, [activeSocketURLs]); - - useEffect(() => { - if (signals.length > 0) { - let ids = []; - - for (let id of widget.signalIDs) { - let signal = signals.find((s) => s.id === id); - if (signal !== undefined) { - let config = configs.find((m) => m.id === signal.configID); - if (config !== undefined) { - ids[signal.id] = config.icID; - } - } - } - - setICIDs(ids); - } - }, [signals]); - - const inputDataChanged = ( - widget, - data, - controlID, - controlValue, - isFinalChange - ) => { - if (controlID !== "" && isFinalChange) { - let updatedWidget = JSON.parse(JSON.stringify(widget)); - updatedWidget.customProperties[controlID] = controlValue; - - updateWidget(updatedWidget); - } - - let signalID = widget.signalIDs[0]; - let signal = signals.filter((s) => s.id === signalID); - if (signal.length === 0) { - console.warn( - "Unable to send signal for signal ID", - signalID, - ". Signal not found." - ); - return; - } - // determine ID of infrastructure component related to signal[0] - // Remark: there is only one selected signal for an input type widget - let icID = icIDs[signal[0].id]; - dispatch( - sendMessageToWebSocket({ - message: { - ic: icID, - signalID: signal[0].id, - signalIndex: signal[0].index, - data: signal[0].scalingFactor * data, - }, - }) - ); - }; - - const updateWidget = async (updatedWidget) => { - try { - await update({ - widgetID: widget.id, - updatedWidget: { widget: updatedWidget }, - }).unwrap(); - } catch (err) { - console.log("error", err); - } - }; - - if (widget.type === "Line") { - return ; - } else if (widget.type === "Box") { - return ; - } else if (widget.type === "Label") { - return ; - } else if (widget.type === "Image") { - return ; - } else if (widget.type === "Plot") { - return ( - - ); - } else if (widget.type === "Table") { - return ( - - ); - } else if (widget.type === "Value") { - return ( - - ); - } else if (widget.type === "Lamp") { - return ( - - ); - } else if (widget.type === "Gauge") { - return ( - - ); - } else if (widget.type === "TimeOffset") { - return ( - - ); - } else if (widget.type === "ICstatus") { - return ; - } else if (widget.type === "Button") { - return ( - - inputDataChanged( - widget, - value, - controlID, - controlValue, - isFinalChange - ) - } - signals={signals} - token={sessionToken} - /> - ); - } else if (widget.type === "NumberInput") { - return ( - - inputDataChanged( - widget, - value, - controlID, - controlValue, - isFinalChange - ) - } - signals={signals} - token={sessionToken} - /> - ); - } else if (widget.type === "Slider") { - return ( - - inputDataChanged( - widget, - value, - controlID, - controlValue, - isFinalChange - ) - } - signals={signals} - token={sessionToken} - /> - ); - } else if (widget.type === "Player") { - return ( - - ); - } else { - console.log("Unknown widget type", widget.type); - return
Error: Widget not found!
; - } -}; - -export default Widget; diff --git a/src/url.js b/src/url.js index 554ddebf..8e65f9ad 100644 --- a/src/url.js +++ b/src/url.js @@ -1,4 +1,4 @@ -var dev = true +var dev = false; const __origin = dev ? "slew.k8s.eonerc.rwth-aachen.de" : window.location.origin export default __origin; \ No newline at end of file