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 =>
+ {s.name}
+ );
+
+ 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 ?
+
+ );
+ }
+}
+
+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 }
+ >
+ toggleCheckAll()}
+ style={buttonStyle}
+ >
+
+
+
+ deleteCheckedSignals()} style={buttonStyle}>
+ Remove
+
+ handleAdd()} style={buttonStyle}>
+ Signal
+
+
+
+
+ Choose a Component Configuration to add the signal to:
+
+ {typeof configs !== "undefined" && configs.map(config => (
+ handleAdd(config.id)}
+ style={buttonStyle}
+ >
+ {config.name}
+
+ ))}
+
+
+
+
+
+ )
+
+ 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}
+
+
+
+
+
+ Copy to Clipboard
+
+
+ 2b) Or alternatively, download the following generated Jupyter notebook to get started:
+
+
+ Download Jupyter Notebook
+
+
+ );
+ }
+}
+
+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 (
+
+
+
+
+
+ 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 openResultConfigSnapshots(result)}>{id}
+ }
+
+ 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;