diff --git a/src/pages/scenarios/dialogs/edit-config.js b/src/pages/scenarios/dialogs/edit-config.js new file mode 100644 index 0000000..70bc2cc --- /dev/null +++ b/src/pages/scenarios/dialogs/edit-config.js @@ -0,0 +1,244 @@ +/** + * 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 from "@rjsf/core"; + +import { Form as BForm } from 'react-bootstrap'; +import { Multiselect } from 'multiselect-react-dropdown' +import Dialog from '../../../common/dialogs/dialog'; +import ParametersEditor from '../../../common/parameters-editor'; + + +class EditConfigDialog extends React.Component { + valid = false; + + constructor(props) { + super(props); + this.state = { + name: '', + icID: '', + startParameters: {}, + formData: {}, + startparamTemplate: null, + selectedFiles: [] // list of selected files {name, id}, this is not the fileIDs list of the config! + }; + } + + onClose(canceled) { + if (canceled === false) { + if (this.valid) { + let data = this.props.config; + if (this.state.name !== '' && this.props.config.name !== this.state.name) { + data.name = this.state.name; + } + if (this.state.icID !== '' && this.props.config.icID !== parseInt(this.state.icID)) { + data.icID = parseInt(this.state.icID, 10); + } + if (Object.keys(this.state.startParameters).length === 0 && this.state.startParameters.constructor === Object && + JSON.stringify(this.props.config.startParameters) !== JSON.stringify(this.state.startParameters)) { + data.startParameters = this.state.startParameters; + } + + let IDs = [] + for (let e of this.state.selectedFiles) { + IDs.push(e.id) + } + if (this.props.config.fileIDs !== null && this.props.config.fileIDs !== undefined) { + if (JSON.stringify(IDs) !== JSON.stringify(this.props.config.fileIDs)) { + data.fileIDs = IDs; + } + } + else { + data.fileIDs = IDs + } + + //forward modified config to callback function + this.props.onClose(data) + } + } else { + this.props.onClose(); + } + + this.setState({ startparamTemplate: null }) + this.valid = false + } + + handleChange(e) { + this.setState({ [e.target.id]: e.target.value }); + this.valid = this.isValid() + } + + changeIC(id) { + let schema = null; + if (this.props.ics) { + let currentIC = this.props.ics.find(ic => ic.id === parseInt(id, 10)); + if (currentIC) { + if (currentIC.startparameterschema !== null && currentIC.startparameterschema.hasOwnProperty('type')) { + schema = currentIC.startparameterschema; + } + } + } + + this.setState({ + icID: id, + startparamTemplate: schema, + }); + + this.valid = this.isValid() + } + + handleParameterChange(data) { + if (data) { + this.setState({ startParameters: data }); + } + this.valid = this.isValid() + } + + onFileChange(selectedList, changedItem) { + this.setState({ + selectedFiles: selectedList + }) + this.valid = this.isValid() + } + + + isValid() { + // input is valid if at least one element has changed from its initial value + return this.state.name !== '' + || this.state.icID !== '' + || Object.keys(this.state.startParameters).length === 0 && this.state.startParameters.constructor === Object + } + + resetState() { + + // determine list of selected files incl id and filename + let selectedFiles = [] + if (this.props.config.fileIDs !== null && this.props.config.fileIDs !== undefined) { + for (let selectedFileID of this.props.config.fileIDs) { + for (let file of this.props.files) { + if (file.id === selectedFileID) { + selectedFiles.push({ name: file.name, id: file.id }) + } + } + } + } + + let schema = null; + if (this.props.ics && this.props.config.icID) { + let currentIC = this.props.ics.find(ic => ic.id === parseInt(this.props.config.icID, 10)); + if (currentIC) { + if (currentIC.startparameterschema !== null && currentIC.startparameterschema.hasOwnProperty('type')) { + schema = currentIC.startparameterschema; + } + } + } + + this.setState({ + name: this.props.config.name, + icID: this.props.config.icID, + startParameters: this.props.config.startParameters, + selectedFiles: selectedFiles, + startparamTemplate: schema, + }); + } + + handleFormChange({formData}) { + this.setState({formData: formData, startParameters: formData}) + this.valid = this.isValid() + } + + render() { + const ICOptions = this.props.ics.map(s => + + ); + + let configFileOptions = []; + for (let file of this.props.files) { + configFileOptions.push( + { name: file.name, id: file.id } + ); + } + + return ( + this.onClose(c)} + onReset={() => this.resetState()} + valid={this.valid} + > + + + Name + this.handleChange(e)} + /> + + + + + Infrastructure Component + this.changeIC(e.target.value)} + > + {ICOptions} + + + + this.onFileChange(selectedList, selectedItem)} + onRemove={(selectedList, removedItem) => this.onFileChange(selectedList, removedItem)} + displayValue={'name'} + placeholder={'Select file(s)...'} + /> + +
+ Start Parameters + + {!this.state.startparamTemplate ? + this.handleParameterChange(data)} + /> + : <>} +
+ {this.state.startparamTemplate ? +
this.handleFormChange({formData})} + children={true} // hides submit button + /> + : <> } +
+ ); + } +} + +export default EditConfigDialog; diff --git a/src/pages/scenarios/dialogs/edit-dashboard.js b/src/pages/scenarios/dialogs/edit-dashboard.js new file mode 100644 index 0000000..070576d --- /dev/null +++ b/src/pages/scenarios/dialogs/edit-dashboard.js @@ -0,0 +1,94 @@ +/** + * 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 } from 'react-bootstrap'; + +import Dialog from '../../../common/dialogs/dialog'; + +class EditDashboardDialog extends React.Component { + valid = true; + + constructor(props) { + super(props); + + this.state = { + name: '', + id: '' + } + } + + onClose(canceled) { + if (canceled === false) { + if (this.valid) { + this.props.onClose(this.state); + } + } else { + this.props.onClose(); + } + } + + handleChange(e) { + this.setState({ [e.target.id]: e.target.value }); + } + + resetState() { + this.setState({ + name: this.props.dashboard.name, + id: this.props.dashboard.id + }); + } + + validateForm(target) { + // check all controls + var name = true; + + if (this.state.name === '') { + name = false; + } + + this.valid = name; + + // return state to control + if (target === 'name') return name ? "success" : "error"; + + return "success"; + } + + render() { + return ( + this.onClose(c)} + onReset={() => this.resetState()} + valid={this.valid} + > + + + Name + this.handleChange(e)} /> + + + + + ); + } +} + +export default EditDashboardDialog; diff --git a/src/pages/scenarios/dialogs/edit-scenario.js b/src/pages/scenarios/dialogs/edit-scenario.js new file mode 100644 index 0000000..0d55165 --- /dev/null +++ b/src/pages/scenarios/dialogs/edit-scenario.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 React from 'react'; +import { Form, Col } from 'react-bootstrap'; + +import Dialog from '../../../common/dialogs/dialog'; +import ParametersEditor from '../../../common/parameters-editor'; + +class EditScenarioDialog extends React.Component { + valid = true; + + constructor(props) { + super(props); + + this.state = { + name: '', + id: '', + running: false, + startParameters: {} + }; + } + + onClose = canceled => { + if (canceled) { + if (this.props.onClose != null) { + this.props.onClose(); + } + + return; + } + + if (this.valid && this.props.onClose != null) { + this.props.onClose(this.state); + } + }; + + handleChange = event => { + this.setState({ [event.target.id]: event.target.value }); + + let name = true; + if (this.state.name === '') { + name = false; + } + + this.valid = name; + }; + + resetState = () => { + this.setState({ + name: this.props.scenario.name, + id: this.props.scenario.id, + running: this.props.scenario.running, + startParameters: this.props.scenario.startParameters || {} + }); + }; + + handleStartParametersChange = startParameters => { + this.setState({ startParameters }); + }; + + render() { + return +
+ + Name + + + + + + Start Parameters + + +
+
; + } +} + +export default EditScenarioDialog; diff --git a/src/pages/scenarios/dialogs/edit-signal-mapping.js b/src/pages/scenarios/dialogs/edit-signal-mapping.js new file mode 100644 index 0000000..c416190 --- /dev/null +++ b/src/pages/scenarios/dialogs/edit-signal-mapping.js @@ -0,0 +1,205 @@ +import { useState } 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 { useGetSignalsQuery, useAddSignalMutation, useDeleteSignalMutation } from "../../../store/apiSlice"; +import { Collapse } from 'react-collapse'; + +const ExportSignalMappingDialog = ({isShown, 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 signals = data ? data.signals : []; + + const handleMappingChange = (e, row, column) => { + console.log(e.target.value, row, column); + } + + const handleAdd = async () => { + + let largestIndex = -1; + signals.forEach(signal => { + if(signal.index > largestIndex){ + largestIndex = signal.index; + } + }) + + const newSignal = { + configID: configID, + direction: direction, + name: "PlaceholderName", + unit: "PlaceholderUnit", + index: largestIndex + 1, + scalingFactor: 1.0 + }; + + try { + await addSignalToConfig(newSignal).unwrap(); + } catch (err) { + console.log(err); + } + + refetchSignals(); + console.log(signals) + } + + const handleDelete = async (signalID) => { + try { + await deleteSignal(signalID).unwrap(); + } catch (err) { + console.log(err); + } + + refetchSignals(); + } + + 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 = ( + 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={signals}> + 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 isShown ? DialogWindow : <> +} + +export default ExportSignalMappingDialog; diff --git a/src/pages/scenarios/dialogs/import-config.js b/src/pages/scenarios/dialogs/import-config.js new file mode 100644 index 0000000..5b1808c --- /dev/null +++ b/src/pages/scenarios/dialogs/import-config.js @@ -0,0 +1,129 @@ +/** + * 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 } from 'react-bootstrap'; + +import Dialog from '../../../common/dialogs/dialog'; + +class ImportConfigDialog extends React.Component { + imported = false; + valid = false; + + constructor(props) { + super(props); + + this.state = { + config: {}, + name: '', + }; + } + + onClose(canceled){ + if (canceled) { + this.props.onClose(); + + return; + } + + this.props.onClose(this.state); + } + + resetState = () => { + this.setState({ + config: {}, + name: '' + }); + + this.imported = false; + } + + loadFile = event => { + // get file + const file = event.target.files[0]; + if (!file.type.match('application/json')) { + return; + } + + // create file reader + let reader = new FileReader(); + let self = this; + + reader.onload = event => { + const config = JSON.parse(event.target.result); + + self.imported = true; + self.valid = true; + this.setState({name: config.name, config: config }); + }; + + reader.readAsText(file); + } + + handleChange(e, index) { + this.setState({ [e.target.id]: e.target.value }); + } + + validateForm(target) { + // check all controls + let name = true; + + if (this.state.name === '') { + name = false; + } + this.valid = name; + + // return state to control + if (target === 'name'){ + return name; + } + } + + render() { + return ( + this.onClose(c)} + onReset={() => this.resetState()} + valid={this.valid} > +
+ + Component Configuration File + + + + + Name + this.handleChange(e)} + /> + + +
+
+ ); + } +} + +export default ImportConfigDialog; diff --git a/src/pages/scenarios/dialogs/import-dashboard.js b/src/pages/scenarios/dialogs/import-dashboard.js new file mode 100644 index 0000000..60d8edb --- /dev/null +++ b/src/pages/scenarios/dialogs/import-dashboard.js @@ -0,0 +1,127 @@ +/** + * 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 } from 'react-bootstrap'; + +import Dialog from '../../../common/dialogs/dialog'; + +class ImportDashboardDialog extends React.Component { + valid = false; + imported = false; + + constructor(props) { + super(props); + + this.state = { + name: '', + widgets: [], + grid: 0 + }; + } + + onClose(canceled) { + if (canceled === false) { + this.props.onClose(this.state); + } else { + this.props.onClose(); + } + } + + handleChange(e, index) { + this.setState({ [e.target.id]: e.target.value }); + } + + resetState() { + this.setState({ name: '', widgets: [], grid: 0 }); + + this.imported = false; + } + + loadFile(fileList) { + // get file + const file = fileList[0]; + if (!file.type.match('application/json')) { + return; + } + + // create file reader + var reader = new FileReader(); + var self = this; + + reader.onload = function(event) { + // read IC + const dashboard = JSON.parse(event.target.result); + + self.imported = true; + self.valid = true; + self.setState({ name: dashboard.name, widgets: dashboard.widgets, grid: dashboard.grid }); + }; + + reader.readAsText(file); + } + + validateForm(target) { + // check all controls + let name = true; + + if (this.state.name === '') { + name = false; + } + + this.valid = name; + + // return state to control + if (target === 'name'){ + return name; + } + } + + render() { + return ( + this.onClose(c)} + onReset={() => this.resetState()} + valid={this.valid}> +
+ + Dashboard File + this.loadFile(e.target.files)} /> + + + + Name + this.handleChange(e)} + /> + + +
+
+ ); + } +} + +export default ImportDashboardDialog; diff --git a/src/pages/scenarios/dialogs/import-scenario.js b/src/pages/scenarios/dialogs/import-scenario.js new file mode 100644 index 0000000..2f20272 --- /dev/null +++ b/src/pages/scenarios/dialogs/import-scenario.js @@ -0,0 +1,126 @@ +/** + * 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, Col } from 'react-bootstrap'; + +import Dialog from '../../../common/dialogs/dialog'; +import ParametersEditor from '../../../common/parameters-editor'; + +class ImportScenarioDialog extends React.Component { + valid = false; + imported = false; + + constructor(props) { + super(props); + + this.state = { + name: '', + running: '', + configs: [], + dashboards: [], + startParameters: {} + }; + } + + onClose = canceled => { + if (canceled) { + if (this.props.onClose != null) { + this.props.onClose(); + } + + return; + } + + if (this.valid && this.props.onClose != null) { + this.props.onClose(this.state); + } + } + + handleChange(e, index) { + this.setState({ [e.target.id]: e.target.value }); + + // check all controls + let name = true; + + if (this.state.name === '') { + name = false; + } + + this.valid = name; + } + + resetState = () => { + this.setState({ name: '', configs: [], startParameters: {} }); + + this.imported = false; + } + + loadFile = event => { + const file = event.target.files[0]; + + if (!file.type.match('application/json')) { + return; + } + + // create file reader + const reader = new FileReader(); + const self = this; + + reader.onload = onloadEvent => { + const scenario = JSON.parse(onloadEvent.target.result); + + self.imported = true; + self.valid = true; + self.setState({ name: scenario.name, configs: scenario.configs, dashboards: scenario.dashboards, startParameters: scenario.startParameters, running: scenario.running }); + }; + + reader.readAsText(file); + } + + render() { + return +
+ + Scenario File + + + + + Name + this.handleChange(e)} /> + + + + + Start Parameters + + + +
+
; + } +} + +export default ImportScenarioDialog; diff --git a/src/pages/scenarios/dialogs/new-scenario.js b/src/pages/scenarios/dialogs/new-scenario.js new file mode 100644 index 0000000..47d1a19 --- /dev/null +++ b/src/pages/scenarios/dialogs/new-scenario.js @@ -0,0 +1,97 @@ +/** + * 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, Col} from 'react-bootstrap'; + +import Dialog from '../../../common/dialogs/dialog'; +import ParametersEditor from '../../../common/parameters-editor'; + +class NewScenarioDialog extends React.Component { + valid = false; + + constructor(props) { + super(props); + + this.state = { + name: '', + startParameters: {}, + running: false + }; + } + + onClose = canceled => { + if (canceled) { + if (this.props.onClose != null) { + this.props.onClose(); + } + + return; + } + + if (this.valid && this.props.onClose != null) { + this.props.onClose(this.state); + } + } + + handleChange = event => { + this.setState({ [event.target.id]: event.target.value }); + + // check all controls + let name = true; + + if (this.state.name === '') { + name = false; + } + + this.valid = name; + } + + resetState = () => { + this.setState({ name: '', startParameters: {} }); + } + + handleStartParametersChange = startParameters => { + this.setState({ startParameters }); + } + + render() { + return +
+ + Name + + + + + + Start Parameters + + + +
+
; + } +} + +export default NewScenarioDialog; diff --git a/src/pages/scenarios/dialogs/result-configs-dialog.js b/src/pages/scenarios/dialogs/result-configs-dialog.js new file mode 100644 index 0000000..0e62b13 --- /dev/null +++ b/src/pages/scenarios/dialogs/result-configs-dialog.js @@ -0,0 +1,65 @@ +/** + * 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 } from 'react-bootstrap'; +import Dialog from '../../../common/dialogs/dialog'; +import ReactJson from 'react-json-view'; + + +class ResultConfigDialog extends React.Component { + valid = true; + + constructor(props) { + super(props); + + this.state = { + confirmCommand: false, + command: '', + }; + } + + onClose(canceled) { + this.props.onClose(); + } + + render() { + return ( + this.onClose(c)} + valid={true} + size="lg" + blendOutCancel={true} + > +
+ + +
+ ); + } +} + +export default ResultConfigDialog; diff --git a/src/pages/scenarios/dialogs/result-python-dialog.js b/src/pages/scenarios/dialogs/result-python-dialog.js new file mode 100644 index 0000000..3042ffd --- /dev/null +++ b/src/pages/scenarios/dialogs/result-python-dialog.js @@ -0,0 +1,286 @@ +/** + * 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 { Button } from 'react-bootstrap'; +import Icon from '../../../common/icon'; +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'; + +class ResultPythonDialog extends React.Component { + villasDataProcessingUrl = 'https://pypi.org/project/villas-dataprocessing/'; + + constructor(props) { + super(props); + + this.state = {}; + } + + componentDidUpdate(prevProps) { + if (this.props.results && this.props.resultId !== prevProps.resultId) { + const result = this.props.results[this.props.resultId]; + if (result) { + const output = this.getJupyterNotebook(result); + const blob = new Blob([JSON.stringify(output)], { + 'type': 'application/x-ipynb+json' + }); + const url = URL.createObjectURL(blob); + + this.setState({ fileDownloadUrl: url }) + } + } + } + + downloadJupyterNotebook() { + const result = this.props.results[this.props.resultId]; + const output = this.getJupyterNotebook(result); + const blob = new Blob([JSON.stringify(output)], { + 'type': 'application/x-ipynb+json' + }); + var url = window.URL.createObjectURL(blob); + + var a = document.createElement('a'); + a.style = 'display: none'; + a.href = url; + a.download = `villas_web_result_${result.id}.ipynb`; + document.body.appendChild(a); + + a.click(); + + setTimeout(function(){ + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 100); + } + + getPythonDependencies(notebook) { + let code = ''; + if (notebook) + code += `import sys +!{sys.executable} -m `; + + code += `pip install villas-dataprocessing`; + + return code; + } + + getPythonSnippets(notebook, result) { + let token = localStorage.getItem('token'); + + let files = []; + for (let file of this.props.files) { + if (result.resultFileIDs != null) { + if (result.resultFileIDs.includes(file.id)) { + files.push(file); + } + } + } + + let code_snippets = []; + + /* Imports */ + let code_imports = ''; + if (notebook) + code_imports += 'from IPython.display import display\n' + + code_imports += `from villas.web.result import Result\n` + code_imports += `from pprint import pprint` + + code_snippets.push(code_imports) + + /* Result object */ + code_snippets.push(`r = Result(${result.id}, '${token}')`); + + /* Examples */ + code_snippets.push(`# Get result metadata +print(r) # result details +pprint(r.files) # list of files of this result set`); + + code_snippets.push(`f = r.files[0] # first file +# f = r.get_files_by_type('text/csv')[0] # first CSV file +# f = r.get_file_by_name('result.csv') # by filename`); + + code_snippets.push(`# Get file metadata +print('Name: %s' % f.name) +print('Size: %d Bytes' % f.size) +print('Type: ' + f.type) +print('Created at: %s' % f.created_at) +print('Updated at: %s' % f.updated_at)`); + + code_snippets.push(`# Open file as fileobj +# with f.open() as fh: +# contents = fh.read() + +# Load and parse file contents (supports xls, mat, json, h5) +contents = f.load()`); + + for (let file of files) { + let code = `# Get file by name +f${file.id} = r.get_file_by_name('${file.name}')`; + + if (notebook) + code += `\n\n# Display contents in Jupyter +display(f${file.id})\n`; + + switch (file.type) { + case 'application/zip': + code += `\n# Open a file within the zipped results +with f${file.id}.open_zip('file_in_zip.csv') as f: + f${file.id} = pandas.read_csv(f)`; + break; + + case 'text/csv': + case 'application/vnd.ms-excel': + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + case 'application/x-hdf5': + case 'application/x-matlab-data': + code += `\n# Load tables as Pandas dataframe +f${file.id} = f${file.id}.load()`; + break; + + case 'application/json': + code += `\n# Load JSON file as Python dictionary +f${file.id} = f${file.id}.load()`; + break; + + default: + code += `\n# Load files as bytes +f${file.id} = f${file.id}.load()`; + break; + } + + code_snippets.push(code); + } + + return code_snippets; + } + + /* Generate random cell ids + * + * See: https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html + */ + getCellId() { + var result = []; + var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + var charactersLength = characters.length; + + for ( var i = 0; i < 8; i++ ) + result.push(characters.charAt(Math.floor(Math.random() * charactersLength))); + + return result.join(''); + } + + getJupyterNotebook(result) { + let ipynb_cells = []; + let cells = [ this.getPythonDependencies(true) ]; + cells = cells.concat(this.getPythonSnippets(true, result)); + + for (let cell of cells) { + let lines = cell.split('\n'); + + for (let i = 0; i < lines.length -1; i++) + lines[i] += '\n' + + ipynb_cells.push({ + cell_type: 'code', + execution_count: null, + id: this.getCellId(), + metadata: {}, + outputs: [], + source: lines + }) + } + + return { + cells: ipynb_cells, + metadata: { + kernelspec: { + display_name: 'Python 3', + language: 'python', + name: 'python3' + }, + language_info: { + codemirror_mode: { + name: 'ipython', + version: 3 + }, + file_extension: '.py', + mimetype: 'text/x-python', + name: 'python', + nbconvert_exporter: 'python', + pygments_lexer: 'ipython3', + version: '3.9.5' + } + }, + nbformat: 4, + nbformat_minor: 5 + } + } + + render() { + let result = this.props.results[this.props.resultId]; + + if (!result) + return null; + + let snippets = this.getPythonSnippets(true, result); + let code = snippets.join('\n\n'); + return ( + this.props.onClose()} + valid={true} + size='lg' + blendOutCancel={true} + > +

Use the following Python code-snippet to fetch and load your results as a Pandas dataframe.

+ +

1) Please install the villas-controller Python package:

+ + {this.getPythonDependencies(false)} + + +

2a) Insert the following snippet your Python code:

+ + {code} + + + + + +

2b) Or alternatively, download the following generated Jupyter notebook to get started:

+ +
+ ); + } +} + +export default ResultPythonDialog; diff --git a/src/pages/scenarios/scenario.js b/src/pages/scenarios/scenario.js new file mode 100644 index 0000000..86283e1 --- /dev/null +++ b/src/pages/scenarios/scenario.js @@ -0,0 +1,137 @@ +/** + * 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 { useParams } from "react-router-dom/cjs/react-router-dom.min"; +import { useGetScenarioByIdQuery } from "../../store/apiSlice"; +import IconButton from "../../common/buttons/icon-button"; +import { currentUser, sessionToken } from "../../localStorage"; +import IconToggleButton from "../../common/buttons/icon-toggle-button"; +import ConfigsTable from "./tables/configs-table"; +import DashboardsTable from "./tables/dashboards-table"; +import ResultsTable from "./tables/results-table"; +import { tableHeadingStyle } from "./styles"; +import UsersTable from "./tables/users-table"; +import { + useUpdateScenarioMutation, + useGetICSQuery, +} from "../../store/apiSlice"; + +const Scenario = (props) => { + const params = useParams(); + const id = params.scenario; + + const { data: fetchedScenarios, isLoading: isScenarioLoading, refetch: refetchScenario } = useGetScenarioByIdQuery(id); + const scenario = fetchedScenarios?.scenario; + + const { data: fetchedICs, isLoading: areICsLoading, error, refetch: refetchICs } = useGetICSQuery(id); + const ics = fetchedICs?.ics; + + const [updateScenario, {isLoadingUpdate}] = useUpdateScenarioMutation(); + + const buttonStyle = { + marginLeft: '10px', + } + + const iconStyle = { + height: '30px', + width: '30px' + } + + const onScenarioLock = async (index) => { + try{ + const data = {...scenario}; + data.isLocked = !data.isLocked; + await updateScenario({id: scenario.id, ...{scenario: data}}).unwrap(); + refetchScenario(); + } catch(error){ + console.log('Error locking/unlocking scenario', error) + } + } + + if(isScenarioLoading){ + return
Loading...
+ } else { + const tooltip = scenario.isLocked ? "View files of scenario" : "Add, edit or delete files of scenario"; + + return ( +
+
+ console.log("click")} + icon="file" + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> +
+

+ {scenario.name} + + onScenarioLock()} + checked={scenario.isLocked} + checkedIcon='lock' + uncheckedIcon='lock-open' + tooltipChecked='Scenario is locked, cannot be edited' + tooltipUnchecked='Scenario is unlocked, can be edited' + disabled={currentUser.role !== "Admin"} + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> + +

+ + {/* */} + + { areICsLoading? +
isLoading...
+ : + + } + + + + + + + +
+ ); + } +} + +export default Scenario; diff --git a/src/pages/scenarios/scenarios.js b/src/pages/scenarios/scenarios.js new file mode 100644 index 0000000..51d138c --- /dev/null +++ b/src/pages/scenarios/scenarios.js @@ -0,0 +1,238 @@ +/** + * 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 } from "react"; +import { useDispatch } from "react-redux"; +import IconButton from "../../common/buttons/icon-button"; +import { Table, ButtonColumn, DataColumn, LinkColumn } from "../../common/table"; +import { buttonStyle, iconStyle } from "./styles"; +import { currentUser } from "../../localStorage"; +import NewScenarioDialog from "./dialogs/new-scenario"; +import ImportScenarioDialog from "./dialogs/import-scenario"; +import DeleteDialog from "../../common/dialogs/delete-dialog"; +import EditScenarioDialog from "./dialogs/edit-scenario"; +import FileSaver from 'file-saver'; +import { + useGetScenariosQuery, + useAddScenarioMutation, + useDeleteScenarioMutation, + useUpdateScenarioMutation, + useGetConfigsQuery, + useGetDashboardsQuery, +} from "../../store/apiSlice"; + +const Scenarios = (props) => { + + const { data , error, refetch: refetchScenarios } = useGetScenariosQuery(); + const scenarios = data?.scenarios; + const dispatch = useDispatch(); + + const [modalScenario, setModalScenario] = useState({name: 'error'}); + const [isNewModalOpened, setIsNewModalOpened] = useState(false); + const [isEditModalOpened, setIsEditModalOpened] = useState(false); + const [isDeleteModalOpened, setIsDeleteModalOpened] = useState(false); + const [selectedScenarioID, setSelectedScenarioID] = useState(null); + + const { data: configs } = useGetConfigsQuery(selectedScenarioID, { + skip: !selectedScenarioID, + }); + + const { data: dashboards } = useGetDashboardsQuery(selectedScenarioID, { + skip: !selectedScenarioID, + }); + + const [postScenario, {isLoadingPost}] = useAddScenarioMutation(); + const [deleteScenario, {isLoadingDelete}] = useDeleteScenarioMutation(); + const [updateScenario, {isLoadingUpdate}] = useUpdateScenarioMutation(); + + const onAddScenario = async (data) => { + //if a new scenario is to be added + try { + await postScenario({scenario: data}).unwrap(); + refetchScenarios(); + } catch (error) { + console.log('Error adding scenario', error) + } + + setIsNewModalOpened(false); + } + + const onDeleteScenario = async (e) => { + if(e){ + try{ + await deleteScenario(modalScenario.id); + refetchScenarios(); + } catch(error){ + console.log('Error deleting scenario', error) + } + } + + setIsDeleteModalOpened(false); + } + + const onEditScenario = async (data) => { + console.log("data: ", {scenario: data}); + if(data){ + try{ + await updateScenario({id: modalScenario.id, ...{scenario: data}}).unwrap(); + refetchScenarios(); + } catch(error){ + if(error.data){ + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(err.data.message)); + } else { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR("Unknown error")); + } + } + } + + setIsEditModalOpened(false); + } + + const onScenarioLock = async (index) => { + try{ + const data = {...scenarios[index]}; + data.isLocked = !data.isLocked; + await updateScenario({id: scenarios[index].id, ...{scenario: data}}).unwrap(); + refetchScenarios(); + } catch(error){ + console.log('Error locking/unlocking scenario', error) + } + } + + const onScenarioDuplicate = async (index) => { + try{ + let scenario = JSON.parse(JSON.stringify(scenarios[index])); + scenario.name = scenario.name + "_copy"; + let jsonObj = scenario; + setSelectedScenarioID(scenario.id); + jsonObj["configs"] = configs; + jsonObj["dashboards"] = dashboards; + await postScenario({scenario: jsonObj}).unwrap(); + refetchScenarios(); + } catch(error){ + console.log('Error duplicating scenario', error) + } + } + + const onExportScenario = (index) => { + // filter properties + let toExport = {...scenarios[index]}; + delete toExport.id; + + const fileName = toExport.name.replace(/\s+/g, '-').toLowerCase(); + + // show save dialog + const blob = new Blob([JSON.stringify(toExport, null, 2)], { type: 'application/json' }); + FileSaver.saveAs(blob, 'scenario-' + fileName + '.json'); + } + + return ( +
+

+ Scenarios + + setIsNewModalOpened(true)} + icon="plus" + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> + {}} + icon="upload" + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> + +

+ + + {currentUser.role === "Admin" ? ( + + ) : ( + <> + )} + + {currentUser.role === "Admin" ? ( + onScenarioLock(index)} + isLocked={(index) => {return scenarios[index].isLocked}} + /> + ) : ( + <> + )} + + { + setModalScenario(scenarios[index]); + setIsEditModalOpened(true); + }} + onDelete={(index) => { + setModalScenario(scenarios[index]); + setIsDeleteModalOpened(true); + }} + onExport={(index) => {onExportScenario(index)}} + onDuplicate={(index) => {onScenarioDuplicate(index)}} + isLocked={(index) => {return scenarios[index].isLocked}} + /> +
+ + onAddScenario(data)} + /> + + {onEditScenario(data)}} + scenario={modalScenario} + /> + + {}} + nodes={null} + /> + + onDeleteScenario(e)} + /> +
+ ); +} + +export default Scenarios; diff --git a/src/pages/scenarios/styles.js b/src/pages/scenarios/styles.js new file mode 100644 index 0000000..826affe --- /dev/null +++ b/src/pages/scenarios/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/scenarios/tables/config-action-board.js b/src/pages/scenarios/tables/config-action-board.js new file mode 100644 index 0000000..60379a8 --- /dev/null +++ b/src/pages/scenarios/tables/config-action-board.js @@ -0,0 +1,149 @@ +/** + * 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 { 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 { 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"; +import notificationsDataManager from "../../../common/data-managers/notifications-data-manager"; + +const ConfigActionBoard = ({selectedConfigs, scenarioID}) => { + let pickedTime = new Date(); + pickedTime.setMinutes(5 * Math.round(pickedTime.getMinutes() / 5 + 1)); + + const [triggerGetSignals] = useLazyGetSignalsQuery(); + + const [sendAction] = useSendActionMutation(); + const [addResult] = useAddResultMutation(); + //we only need to update results table in case new result being added + const { refetch: refetchResults } = useGetResultsQuery(scenarioID); + + const [time, setTime] = useState(pickedTime); + const [isResultRequested, setIsResultRequested] = useState(false); + + const handleConfigStart = async () => { + for(const config of selectedConfigs){ + try { + if(isResultRequested){ + + const signalsInRes = await triggerGetSignals({configID: config.id, direction: "in"}, ).unwrap(); + const signalsOutRes = await triggerGetSignals({configID: config.id, direction: "out"}, ).unwrap(); + + let parsedInSignals = []; + let parsedOutSignals = []; + + if(signalsInRes.signals.length > 0){ + for(let signal of signalsInRes.signals){ + parsedInSignals.push(signal); + } + } + + if(signalsOutRes.signals.length > 0){ + for(let signal of signalsOutRes.signals){ + parsedOutSignals.push(signal); + } + } + + const newResult = { + description: "Start at " + time, + scenarioID: scenarioID, + configSnapshots: { + ...config, + inputMapping: parsedInSignals, + outputMapping: parsedOutSignals, + } + } + + await addResult({result: newResult}) + refetchResults(); + } + await sendAction({ icid: config.icID, action: "start", when: Math.round(new Date(time).getTime() / 1000), parameters: {} }).unwrap(); + } catch (err) { + if(err.data){ + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(err.data.message)); + } else { + console.log('Error', err); + } + } + } + } + + const handleConfigPause = async () => { + for(const config of selectedConfigs){ + try { + await sendAction({ icid: config.icID, action: "pause", when: Math.round(new Date(time).getTime() / 1000), parameters: {} }).unwrap(); + } catch (error) { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(err.data.message)); + } + } + } + + const handleConfigStop = async () => { + for(const config of selectedConfigs){ + try { + await sendAction({ icid: config.icID, action: "stop", when: Math.round(new Date(time).getTime() / 1000), parameters: {} }).unwrap(); + } catch (error) { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(err.data.message)); + } + } + } + + return (
+ + +
+ setTime(newTime)} + value={time} + disableClock={true} + /> + + + + handleConfigStart()} + onPauseResume={() => handleConfigPause()} + onStop={() => handleConfigStop()} + onReset={null} + onShutdown={null} + onDelete={null} + onRecreate={null} + paused={false} + /> + + + + setIsResultRequested(prevState => !prevState)} + /> + + +
+ Select time for synced command execution +
); +} + +export default ConfigActionBoard; diff --git a/src/pages/scenarios/tables/configs-table.js b/src/pages/scenarios/tables/configs-table.js new file mode 100644 index 0000000..2b0806b --- /dev/null +++ b/src/pages/scenarios/tables/configs-table.js @@ -0,0 +1,440 @@ +/** + * 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 } from "react"; +import IconButton from "../../../common/buttons/icon-button"; +import { Table, ButtonColumn, CheckboxColumn, DataColumn } from "../../../common/table"; +import { tableHeadingStyle, buttonStyle, iconStyle } from "../styles"; +import { currentUser, sessionToken } from "../../../localStorage"; +import NewDialog from "../../../common/dialogs/new-dialog"; +import ImportConfigDialog from "../dialogs/import-config"; +import DeleteDialog from "../../../common/dialogs/delete-dialog"; +import NotificationsFactory from "../../../common/data-managers/notifications-factory"; +import notificationsDataManager from "../../../common/data-managers/notifications-data-manager"; +import EditSignalMappingDialog from '../dialogs/edit-signal-mapping'; +import FileSaver from "file-saver"; +import { + useGetConfigsQuery, + useAddComponentConfigMutation, + useDeleteComponentConfigMutation, + useLazyGetSignalsQuery, + useAddSignalMutation, +} from "../../../store/apiSlice"; +import ConfigActionBoard from "./config-action-board"; + + +const ConfigsTable = ({scenario, ics}) => { + const {data, refetch: refetchConfigs } = useGetConfigsQuery(scenario.id); + const [addComponentConfig] = useAddComponentConfigMutation(); + const [deleteComponentConfig] = useDeleteComponentConfigMutation(); + const [addSignalToConfig] = useAddSignalMutation(); + const [triggerGetSignals] = useLazyGetSignalsQuery(); + const configs = data ? data.configs : []; + const [signals, setSignals] = useState({}); + + const [isNewModalOpened, setIsNewModalOpened] = useState(false); + const [isImportModalOpened, setIsImportModalOpened] = useState(false); + const [isDeleteModalOpened, setIsDeleteModalOpened] = useState(false); + const [isEditSignalMappingModalOpened, setIsEditSignalMappingModalOpened] = useState(false); + const [signalMappingConfigID, setSignalMappingConfigID] = useState(0); + const [mappingModalDirection, setMappingModalDirection] = useState('in'); + const [configToDelete, setConfigToDelete] = useState({name: ''}); + const [checkedConfigsIDs, setCheckedConfigsIDs] = useState([]); + const [areAllConfigsChecked, setAreAllConfigsChecked] = useState(false); + + useEffect(() => { + if(configs.length > 0) { + configs.forEach(config => { + getBothSignals(config.id); + }) + } + }, [configs]) + + const getBothSignals = async(configID) => { + try { + const resIn = await triggerGetSignals({configID: configID, direction: 'in'}).unwrap(); + const resOut = await triggerGetSignals({configID: configID, direction: 'out'}).unwrap(); + setSignals(prevSignals => ({...prevSignals, [configID]: resIn.signals.concat(resOut.signals).length > 0 ? resIn.signals.concat(resOut.signals) : []})) + } catch (err) { + console.log('error', err); + return []; + } + } + + const newConfig = async (data) => { + if(data){ + const config = { + config: { + ScenarioID: scenario.id, + Name: data.value, + ICID: ics.length > 0 ? ics[0].id : null, + StartParameters: {}, + FileIDs: [], + } + }; + + try { + await addComponentConfig(config).unwrap(); + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(err.data.message)); + } + + refetchConfigs(); + } + + setIsNewModalOpened(false); + } + + const importConfig = async (data) => { + if(data){ + const config = { + config: { + ScenarioID: scenario.id, + Name: data.name, + ICID: data.config.icID, + StartParameters: data.config.startParameters, + FileIDs: data.config.fileIDs, + } + } + + try { + await addComponentConfig(config).unwrap(); + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(err.data.message)); + } + } + + refetchConfigs(); + setIsImportModalOpened(false); + } + + const exportConfig = (index) => { + + + // show save dialog + const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); + FileSaver.saveAs(blob, 'config-' + config.name + '.json'); + } + + const deleteConfig = async (isConfirmed) => { + if(isConfirmed){ + try { + await deleteComponentConfig(configToDelete.id).unwrap(); + setConfigToDelete({name: ''}); + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(err.data.message)); + } + } + + refetchConfigs(); + setIsDeleteModalOpened(false); + } + + const getICName = (icID) => { + for (let ic of ics) { + if (ic.id === icID) { + return ic.name || ic.uuid; + } + } + } + + const copyConfig = async (configToCopy) => { + let copiedConfig = JSON.parse(JSON.stringify(configToCopy)); + + try { + const signalsInRes = await triggerGetSignals({configID: configToCopy.id, direction: "in"}, ).unwrap(); + const signalsOutRes = await triggerGetSignals({configID: configToCopy.id, direction: "out"}, ).unwrap(); + + let parsedInSignals = []; + let parsedOutSignals = []; + + if(signalsInRes.signals.length > 0){ + for(let signal of signalsInRes.signals){ + delete signal.configID; + delete signal.id; + parsedInSignals.push(signal); + } + } + + if(signalsOutRes.signals.length > 0){ + for(let signal of signalsOutRes.signals){ + delete signal.configID; + delete signal.id; + parsedOutSignals.push(signal); + } + } + + copiedConfig["inputMapping"] = parsedInSignals; + copiedConfig["outputMapping"] = parsedOutSignals; + + delete copiedConfig.id; + delete copiedConfig.scenarioID; + + return copiedConfig; + } catch (err) { + console.log(err); + return null; + } + } + + const handleConfigExport = async (config) => { + try { + const configToExport = await copyConfig(config); + const fileName = configToExport.name.replace(/\s+/g, '-').toLowerCase(); + const blob = new Blob([JSON.stringify(configToExport, null, 2)], { type: 'application/json' }); + FileSaver.saveAs(blob, 'config-' + fileName + '.json'); + } catch (err) { + console.log(err); + } + } + + const handleDuplicateConfig = async (originalConfig) => { + try { + //in order to properly duplicate existing config, we need first to create an new + //one with same initital parameters and then add all the signal subobjects to it + const copiedConfig = await copyConfig(originalConfig); + copiedConfig["scenarioID"] = scenario.id; + copiedConfig.name = `${originalConfig.name}_copy`; + + const signalsIn = copiedConfig.inputMapping; + const signalsOut = copiedConfig.outputMapping; + + delete copiedConfig["inputMapping"]; + delete copiedConfig["outputMapping"]; + + const res = await addComponentConfig({ config: copiedConfig }).unwrap(); + + if(signalsIn.length > 0){ + for(let signal of signalsIn){ + signal.configID = res.id; + await addSignalToConfig(signal).unwrap(); + } + } + + if(signalsOut.length > 0){ + for(let signal of signalsOut){ + signal.configID = res.id; + await addSignalToConfig(signal).unwrap(); + } + } + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(err.data.message)); + } + + refetchConfigs(); + } + + const getNumberOfSignalsModifier = (configID, direction) => { + if(configID in signals){ + return signals[configID].filter(s => s.direction == direction).length; + } else { + return 0; + } + } + + const handleSignalMapping = () => { + + } + + const toggleCheckAllConfigs = () => { + if(checkedConfigsIDs.length === configs.length){ + setCheckedConfigsIDs([]); + setAreAllConfigsChecked(false); + } else { + configs.forEach(config => { + if(!checkedConfigsIDs.includes(config.id)){ + setCheckedConfigsIDs(prevState => ([...prevState, config.id])); + } + }) + setAreAllConfigsChecked(true); + } + } + + const isConfigChecked = (config) => { + return checkedConfigsIDs.includes(config.id); + } + + const handleConfigCheck = (config, event) => { + if(!checkedConfigsIDs.includes(config.id)){ + setCheckedConfigsIDs(prevState => ([...prevState, config.id])); + } else { + const index = checkedConfigsIDs.indexOf(config.id); + setCheckedConfigsIDs(prevState => prevState.filter((_, i) => i !== index)); + } + } + + const usesExternalIC = (index) => { + for (let ic of ics) { + if (ic.id === configs[index].icID) { + if (ic.managedexternally === true) { + return true + } + } + } + return false + } + + return ( +
+ {/*Component Configurations table*/} +

Component Configurations + + setIsNewModalOpened(true)} + icon='plus' + disabled={scenario.isLocked} + hidetooltip={scenario.isLocked} + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> + setIsImportModalOpened(true)} + icon='upload' + disabled={scenario.isLocked} + hidetooltip={scenario.isLocked} + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> + +

+ + toggleCheckAllConfigs()} + allChecked={areAllConfigsChecked} + checked={(config) => isConfigChecked(config)} + checkboxDisabled={(index) => !usesExternalIC(index)} + onChecked={(config, event) => handleConfigCheck(config, event)} + width={20} + /> + {currentUser.role === "Admin" ? + + : <> + } + + { + setMappingModalDirection('out'); + setSignalMappingConfigID(configs[index].id); + setIsEditSignalMappingModalOpened(true); + }} + width={150} + locked={scenario.isLocked} + modifier={(configId) => {getNumberOfSignalsModifier(configId, 'out')}} + /> + { + setMappingModalDirection('in'); + setSignalMappingConfigID(configs[index].id); + setIsEditSignalMappingModalOpened(true); + }} + width={150} + locked={scenario.isLocked} + modifier={(configId) => {getNumberOfSignalsModifier(configId, 'in')}} + /> + {}} + width={170} + locked={scenario.isLocked} + /> + getICName(icID)} + width={200} + /> + {}} + onDelete={(index) => { + setConfigToDelete(configs[index]) + setIsDeleteModalOpened(true); + }} + onExport={index => { + handleConfigExport(configs[index]); + }} + onDuplicate={index => { + handleDuplicateConfig(configs[index]); + }} + locked={scenario.isLocked} + /> +
+ + isConfigChecked(c))} + scenarioID={scenario.id} + /> + + newConfig(data)} + /> + importConfig(data)} + ics={ics} + /> + deleteConfig(c)} + /> + setIsEditSignalMappingModalOpened(false)} + configID={signalMappingConfigID} + /> + +
+ ) +} + +export default ConfigsTable; diff --git a/src/pages/scenarios/tables/dashboards-table.js b/src/pages/scenarios/tables/dashboards-table.js new file mode 100644 index 0000000..f6ac81b --- /dev/null +++ b/src/pages/scenarios/tables/dashboards-table.js @@ -0,0 +1,306 @@ +/** + * 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 } from "react"; +import IconButton from "../../../common/buttons/icon-button"; +import { Table, ButtonColumn, LinkColumn, DataColumn } from "../../../common/table"; +import { buttonStyle, tableHeadingStyle, iconStyle } from "../styles"; +import { currentUser, sessionToken } from "../../../localStorage"; +import { InputGroup, Form } from "react-bootstrap"; +import { useGetDashboardsQuery } from "../../../store/apiSlice"; +import {Button} from "react-bootstrap"; +import NewDialog from "../../../common/dialogs/new-dialog"; +import DeleteDialog from "../../../common/dialogs/delete-dialog"; +import ImportDashboardDialog from "../dialogs/import-dashboard"; +import EditDashboardDialog from "../dialogs/edit-dashboard"; +import NotificationsFactory from "../../../common/data-managers/notifications-factory"; +import notificationsDataManager from "../../../common/data-managers/notifications-data-manager"; +import FileSaver from "file-saver"; +import { + useAddDashboardMutation, + useDeleteDashboardMutation, + useUpdateDashboardMutation, + useLazyGetWidgetsQuery, + useAddWidgetMutation, +} from "../../../store/apiSlice"; +import { rest } from "lodash"; + +const DashboardsTable = ({scenario}) => { + const {data: fetchedDashboards, refetch: refetchDashboards } = useGetDashboardsQuery(scenario.id); + const [addDashboard] = useAddDashboardMutation(); + const [deleteDashboard] = useDeleteDashboardMutation(); + const [updateDashboard] = useUpdateDashboardMutation(); + const [addWidgetToDashboard] = useAddWidgetMutation(); + + const [triggerGetWidgets, { isLoading: isWidgetsLoading, data: widgets, error: widgetsError }] = useLazyGetWidgetsQuery(); + + const dashboards = fetchedDashboards ? fetchedDashboards.dashboards : []; + + const [isNewModalOpened, setIsNewModalOpened] = useState(false); + const [isDeleteModalOpened, setIsDeleteModalOpened] = useState(false); + const [isEditModalOpened, setIsEditModalOpened] = useState(false); + const [isImportModalOpened, setIsImportModalOpened] = useState(false); + + const [dashboardToDelete, setDashboardToDelete] = useState({}); + const [dashboardToEdit, setDashboardToEdit] = useState({}); + + const handleNewDashboard = async(data) => { + if(data){ + const newDashboard = { + Grid: 15, + Height: 0, + Name: data.value, + ScenarioID: scenario.id, + }; + + try { + await addDashboard({ dashboard: newDashboard }).unwrap(); + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(err.data.message)); + } + } + + refetchDashboards(); + setIsNewModalOpened(false); + } + + const handleImportDashboard = async(data) => { + if(data){ + const toImportDashboard = { + Grid: data.grid ? data.grid : 15, + Height: data.height? data.height : 0, + Name: data.name, + ScenarioID: scenario.id, + } + try{ + const res = await addDashboard({ dashboard: toImportDashboard }).unwrap(); + if(data.widgets.length > 0){ + for(let widget of data.widgets){ + widget.scenarioID = scenario.id; + widget.dashboardID = res.dashboard.id; + await addWidgetToDashboard(widget).unwrap(); + } + } + } catch (err) { + console.log(err) + } + } + + refetchDashboards(); + setIsImportModalOpened(false); + } + + const handleEditDashboard = async(editedDashboard) => { + if(editedDashboard){ + //as for now modal only allows to edit the name + const {name, ...rest} = dashboardToEdit; + const updatedDashboard = {name: editedDashboard.name, ...rest}; + try { + await updateDashboard({ dashboardID: updatedDashboard.id, dashboard: updatedDashboard }).unwrap(); + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(err.data.message)); + } + } + + refetchDashboards(); + setIsEditModalOpened(false); + } + + const handleDeleteDashboard = async(isConfirmed) => { + if(isConfirmed){ + try { + await deleteDashboard(dashboardToDelete.id).unwrap(); + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(err.data.message)); + } + } + + refetchDashboards(); + setIsDeleteModalOpened(false); + } + + const copyDashboard = async (dashboardToCopy) => { + let copiedDashboard = JSON.parse(JSON.stringify(dashboardToCopy)); + + try { + const widgetsResponse = await triggerGetWidgets(dashboardToCopy.id).unwrap(); + let parsedWidgets = []; + if(widgetsResponse.widgets.length > 0){ + parsedWidgets = JSON.parse(JSON.stringify(widgetsResponse.widgets.filter(w => w.dashboardID === parseInt(dashboardToCopy.id, 10)))); + parsedWidgets.forEach((widget) => { + delete widget.dashboardID; + delete widget.id; + }) + } + + copiedDashboard["widgets"] = parsedWidgets; + delete copiedDashboard.scenarioID; + delete copiedDashboard.id; + + return copiedDashboard; + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(err.data.message)); + return null; + } + } + + const duplicateDashboard = async (originalDashboard) => { + try { + //in order to properly duplicate existing dashboard, we need first to create an new + //one with same initital parameters and then add all the widget subobjects to it + const duplicatedDashboard = await copyDashboard(originalDashboard); + duplicatedDashboard.scenarioID = scenario.id; + duplicatedDashboard.name = `${originalDashboard.name}_copy`; + const widgets = duplicatedDashboard.widgets; + + const res = await addDashboard({ dashboard: duplicatedDashboard }).unwrap(); + + if(widgets.length > 0){ + for(let widget of widgets){ + widget.scenarioID = scenario.id; + widget.dashboardID = res.dashboard.id; + await addWidgetToDashboard(widget).unwrap(); + } + } + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(err.data.message)); + } + + refetchDashboards(); + } + + const exportDashboard = async (dashboard) => { + try { + //remove unnecessary fields and get widgets + const dashboardToExport = await copyDashboard(dashboard); + const fileName = dashboardToExport.name.replace(/\s+/g, '-').toLowerCase(); + const blob = new Blob([JSON.stringify(dashboardToExport, null, 2)], { type: 'application/json' }); + FileSaver.saveAs(blob, 'dashboard-' + fileName + '.json'); + } catch (err) { + console.log(err); + } + } + + return ( +
+ {/*Dashboard table*/} +

Dashboards + + { + setIsNewModalOpened(true); + }} + icon='plus' + disabled={scenario.isLocked} + hidetooltip={scenario.isLocked} + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> + { + setIsImportModalOpened(true); + }} + icon='upload' + disabled={scenario.isLocked} + hidetooltip={scenario.isLocked} + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> + +

+ + {currentUser.role === "Admin" ? + + : <> + } + + + + { + setDashboardToEdit(dashboards[index]); + setIsEditModalOpened(true); + }} + onDelete={(index) => { + setDashboardToDelete(dashboards[index]); + setIsDeleteModalOpened(true); + }} + onExport={index => { + exportDashboard(dashboards[index]); + }} + onDuplicate={index => { + duplicateDashboard(dashboards[index]); + }} + locked={scenario.isLocked} + /> +
+ + handleNewDashboard(data)} + /> + + handleEditDashboard(data)} + /> + + handleImportDashboard(data)} + /> + + handleDeleteDashboard(isConfirmed)} + /> +
+ ) +} + +export default DashboardsTable; diff --git a/src/pages/scenarios/tables/results-table.js b/src/pages/scenarios/tables/results-table.js new file mode 100644 index 0000000..02e43e3 --- /dev/null +++ b/src/pages/scenarios/tables/results-table.js @@ -0,0 +1,259 @@ +/** + * 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 } from "react"; +import IconButton from "../../../common/buttons/icon-button"; +import { Table, ButtonColumn, DataColumn, LinkbuttonColumn } from "../../../common/table"; +import { buttonStyle, tableHeadingStyle, iconStyle } from "../styles"; +import { currentUser, sessionToken } from "../../../localStorage"; +import { InputGroup, Form } from "react-bootstrap"; +import DeleteDialog from "../../../common/dialogs/delete-dialog"; +import ResultConfigDialog from "../dialogs/result-configs-dialog"; +import ResultPythonDialog from "../dialogs/result-python-dialog"; +import NewDialog from "../../../common/dialogs/new-dialog"; +import {Button} from "react-bootstrap"; +import NotificationsFactory from "../../../common/data-managers/notifications-factory"; +import notificationsDataManager from "../../../common/data-managers/notifications-data-manager"; +import FileSaver from "file-saver"; +import { + useGetResultsQuery, + useAddResultMutation, + useDeleteResultMutation, + useGetFilesQuery, + useAddFileMutation, + useLazyDownloadFileQuery, + useUpdateFileMutation, + useDeleteFileMutation, + } from "../../../store/apiSlice"; +import { set } from "lodash"; +import JSZip from 'jszip'; + +const ResultsTable = (props) => { + const scenario = props.scenario; + const {data, refetch: refetchResults } = useGetResultsQuery(scenario.id); + const results = data ? data.results : []; + const [addResult] = useAddResultMutation(); + const [deleteResult] = useDeleteResultMutation(); + const {data: filesData } = useGetFilesQuery(scenario.id); + const files = filesData ? filesData.files : []; + + const [isNewDialogOpened, setIsNewDialogOpened] = useState(false); + const [isDeleteModalOpened, setIsDeleteModalOpened] = useState(false); + const [isResultConfigDialogOpened, setIsResultConfigDialogOpened] = useState(false); + const [isPythonDialogOpened, setIsPythonDialogOpened] = useState(false); + const [modalResultConfigs, setModalResultConfigs] = useState([]); + const [modalResultConfigsIndex, setModalResultConfigsIndex] = useState(-1); + const [modalResultsIndex, setModalResultsIndex] = useState(0); + const [resultToDelete, setResultToDelete] = useState({}); + + const [triggerDownloadFile] = useLazyDownloadFileQuery(); + + const handleDownloadAllFiles = async (resultIndex) => { + const result = results[resultIndex]; + if (result && result.resultFileIDs && result.resultFileIDs.length > 0) { + const zip = new JSZip(); + for (const fileID of result.resultFileIDs) { + 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(`Failed to download file with ID ${fileID}`)); + console.error(`Failed to download file with ID ${fileID}`, error); + } + } + zip.generateAsync({ type: 'blob' }) + .then((content) => { + FileSaver.saveAs(content, `result-${result.id}.zip`); + }) + .catch((err) => { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR('Failed to create ZIP archive')); + console.error('Failed to create ZIP archive', err); + }); + } else { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR('No files to download for the selected result')); + } + }; + + const handleDownloadFile = async (fileID) => { + try { + const res = await triggerDownloadFile(fileID); + const file = files.find(f => f.id === fileID); + const blob = new Blob([res.data], { type: file.type }); + FileSaver.saveAs(blob, file.name); + } catch (error) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(`Failed to download file with ID ${fileID}`)); + console.error(`Failed to download file with ID ${fileID}`, error); + } + } + + const modifyResultNoColumn = (id, result) => { + return + } + + const openResultConfigSnapshots = (result) => { + if (result.configSnapshots === null || result.configSnapshots === undefined) { + setModalResultConfigs({"configs": []}) + setModalResultConfigsIndex(result.id); + setIsResultConfigDialogOpened(true); + } else { + setModalResultConfigs(result.configSnapshots) + setModalResultConfigsIndex(result.id); + setIsResultConfigDialogOpened(true); + } + } + + const handleNewResult = async (data) => { + if(data) { + const result = { + scenarioId: scenario.id, + description: data.value + } + try { + await addResult({ result: result }).unwrap(); + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(err.data.message)); + } + } + + refetchResults(); + setIsNewDialogOpened(false); + } + + const handleDeleteResult = async (isConfirmed) => { + if(isConfirmed) { + try { + await deleteResult(resultToDelete.id).unwrap(); + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(err.data.message)); + } + } + + refetchResults(); + setIsDeleteModalOpened(false); + setResultToDelete({}); + } + + return ( +
+

Results + + setIsNewDialogOpened(true)} + icon='plus' + disabled={scenario.isLocked} + hidetooltip={scenario.isLocked} + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> + +

+ + + modifyResultNoColumn(id, result)} + /> + + + + handleDownloadFile(fileID)} + width={300} + /> + { + setIsPythonDialogOpened(true); + setModalResultsIndex(index); + }} + onEdit={(index) => {}} + onDownloadAll={(index) => handleDownloadAllFiles(index)} + onDelete={(index) => { + setIsDeleteModalOpened(true); + setResultToDelete(results[index]); + }} + locked={scenario.isLocked} + /> +
+ + {/* {}} + /> */} + handleDeleteResult(e)} + /> + setIsResultConfigDialogOpened(false)} + /> + setIsPythonDialogOpened(false)} + /> + handleNewResult(data)} + /> +
+ ) +} + +export default ResultsTable; diff --git a/src/pages/scenarios/tables/users-table.js b/src/pages/scenarios/tables/users-table.js new file mode 100644 index 0000000..dda72a0 --- /dev/null +++ b/src/pages/scenarios/tables/users-table.js @@ -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 { useState } from "react"; +import IconButton from "../../../common/buttons/icon-button"; +import { Table, ButtonColumn, DataColumn } from "../../../common/table"; +import { buttonStyle, tableHeadingStyle } from "../styles"; +import { currentUser } from "../../../localStorage"; +import { InputGroup, Form } from "react-bootstrap"; +import DeleteDialog from "../../../common/dialogs/delete-dialog"; +import NotificationsFactory from "../../../common/data-managers/notifications-factory"; +import notificationsDataManager from "../../../common/data-managers/notifications-data-manager"; +import { useGetUsersOfScenarioQuery, useAddUserToScenarioMutation, useRemoveUserFromScenarioMutation } from "../../../store/apiSlice"; + +const UsersTable = (props) => { + const scenario = props.scenario; + const {data, refetch: refetchUsers } = useGetUsersOfScenarioQuery(scenario.id); + const [addUserToScenario, { isSuccess: isUserAdded }] = useAddUserToScenarioMutation(); + const [removeUserFromScenario, { isSuccess: isUserRemoved }] = useRemoveUserFromScenarioMutation(); + const users = data ? data.users : []; + const [isDeleteUserModalOpened, setIsDeleteUserModalOpened] = useState(false); + const [usernameToAdd, setUsernameToAdd] = useState(""); + const [usernameToDelete, setUsernameToDelete] = useState(""); + + const addUser = async () => { + if(usernameToAdd.trim() === ''){ + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR('Please, enter correct username')); + } else { + try { + await addUserToScenario({ scenarioID: props.scenario.id, username: usernameToAdd }).unwrap(); + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(err.data.message)); + } + + refetchUsers(); + } + + setUsernameToAdd(''); + }; + + const removeUser = async (confirmed) => { + if(confirmed){ + try { + await removeUserFromScenario({ scenarioID: props.scenario.id, username: usernameToDelete }).unwrap(); + } catch (err) { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(err.data.message)); + } + } + + refetchUsers(); + setUsernameToDelete(''); + setIsDeleteUserModalOpened(false); + } + + return ( +
+ {/*Scenario Users table*/} +

Users sharing this scenario

+ + {currentUser.role === "Admin" ? + + : <> + } + + + { + setIsDeleteUserModalOpened(true); + setUsernameToDelete(users[index].username); + }} + locked={scenario.isLocked} + /> +
+ + + {setUsernameToAdd(e.target.value)}} + value={usernameToAdd} + type="text" + /> + + { + addUser() + }} + icon='plus' + disabled={false} + hidetooltip={false} + buttonStyle={buttonStyle} + /> + + +
+
+ + {removeUser(c)}} + /> +
+ ) +} + +export default UsersTable;