1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/web/ synced 2025-03-09 00:00:01 +01:00

updated scenarios page

Signed-off-by: Andrii Podriez <andrey5577990@gmail.com>
This commit is contained in:
Andrii Podriez 2024-07-25 13:49:35 +02:00 committed by al3xa23
parent 7d11f3e5d9
commit 7ae0deee87
18 changed files with 3168 additions and 0 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 =>
<option key={s.id} value={s.id}>{s.name}</option>
);
let configFileOptions = [];
for (let file of this.props.files) {
configFileOptions.push(
{ name: file.name, id: file.id }
);
}
return (
<Dialog
show={this.props.show}
title="Edit Component Configuration"
buttonTitle="Save"
onClose={(c) => this.onClose(c)}
onReset={() => this.resetState()}
valid={this.valid}
>
<BForm>
<BForm.Group controlId="name" style={{marginBottom: '15px'}}>
<BForm.Label column={false}>Name</BForm.Label>
<BForm.Control
type="text"
placeholder={this.props.config.name}
value={this.state.name}
onChange={(e) => this.handleChange(e)}
/>
<BForm.Control.Feedback />
</BForm.Group>
<BForm.Group controlId="icID" style={{marginBottom: '15px'}}>
<BForm.Label column={false}> Infrastructure Component </BForm.Label>
<BForm.Control
as="select"
placeholder='Select infrastructure component'
value={this.state.icID}
onChange={(e) => this.changeIC(e.target.value)}
>
{ICOptions}
</BForm.Control>
</BForm.Group>
<Multiselect
options={configFileOptions}
showCheckbox={true}
selectedValues={this.state.selectedFiles}
onSelect={(selectedList, selectedItem) => this.onFileChange(selectedList, selectedItem)}
onRemove={(selectedList, removedItem) => this.onFileChange(selectedList, removedItem)}
displayValue={'name'}
placeholder={'Select file(s)...'}
/>
<hr/>
<BForm.Label><b>Start Parameters</b></BForm.Label>
{!this.state.startparamTemplate ?
<ParametersEditor
content={this.state.startParameters}
onChange={(data) => this.handleParameterChange(data)}
/>
: <></>}
</BForm>
{this.state.startparamTemplate ?
<Form
schema={this.state.startparamTemplate}
formData={this.state.formData}
id='jsonFormData'
onChange={({formData}) => this.handleFormChange({formData})}
children={true} // hides submit button
/>
: <></> }
</Dialog>
);
}
}
export default EditConfigDialog;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 (
<Dialog
show={this.props.show}
title="Edit Dashboard"
buttonTitle="Save"
onClose={(c) => this.onClose(c)}
onReset={() => this.resetState()}
valid={this.valid}
>
<Form>
<Form.Group controlId="name" valid={this.validateForm('name')}>
<Form.Label>Name</Form.Label>
<Form.Control type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<Form.Control.Feedback />
</Form.Group>
</Form>
</Dialog>
);
}
}
export default EditDashboardDialog;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 <Dialog
show={this.props.show}
title='Edit Scenario'
buttonTitle='Save'
onClose={this.onClose}
onReset={this.resetState}
valid={true}
>
<Form>
<Form.Group as={Col} controlId='name' style={{marginBottom: '15px'}}>
<Form.Label column={false}>Name</Form.Label>
<Form.Control type='text' placeholder='Enter name' value={this.state.name} onChange={this.handleChange} />
<Form.Control.Feedback />
</Form.Group>
<Form.Group as={Col} controlId='startParameters'>
<Form.Label column={false}>Start Parameters</Form.Label>
<ParametersEditor content={this.state.startParameters} onChange={this.handleStartParametersChange} />
</Form.Group>
</Form>
</Dialog>;
}
}
export default EditScenarioDialog;

View file

@ -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 = (
<Dialog
show={isShown}
title={"Edit Signal " + direction +" Mapping"}
buttonTitle="Close"
blendOutCancel = {true}
onClose={(c) => onClose(c)}
onReset={() => {}}
valid={true}
>
<Form.Group>
<Form.Label style={dialogWarningLabel}>IMPORTANT: Signal configurations that were created before January 2022 have to be fixed manually. Signal indices have to start at 0 and not 1.</Form.Label>
<Form.Label> <i>Click in table cell to edit</i></Form.Label>
<Table breakWord={true} checkbox onChecked={(signal) => onSignalChecked(signal)} data={signals}>
<CheckboxColumn
onChecked={(index, event) => onSignalChecked(index, event)}
checked={(signal) => isSignalChecked(signal)}
width='30'
/>
<DataColumn
title='Index'
dataKey='index'
inlineEditable
inputType='number'
onInlineChange={(e, row, column) => handleMappingChange(e, row, column)}
/>
<DataColumn
title='Name'
dataKey='name'
inlineEditable
inputType='text'
onInlineChange={(e, row, column) => handleMappingChange(e, row, column)}
/>
<DataColumn
title='Unit'
dataKey='unit'
inlineEditable
inputType='text'
onInlineChange={(e, row, column) => handleMappingChange(e, row, column)}
/>
<DataColumn
title='Scaling Factor'
dataKey='scalingFactor'
inlineEditable
inputType='number'
onInlineChange={(e, row, column) => handleMappingChange(e, row, column)}
/>
<ButtonColumn
title='Remove'
deleteButton
onDelete={(index) => handleDelete(signals[index].id)}
/>
</Table>
<div className='solid-button' style={signalDialogCheckButton}>
<OverlayTrigger
key={0}
placement='left'
overlay={<Tooltip id={`tooltip-${"check"}`}> Check/Uncheck All </Tooltip>}
>
<Button
variant='secondary'
key={50}
onClick={() => toggleCheckAll()}
style={buttonStyle}
>
<Icon icon="check" />
</Button>
</OverlayTrigger>
<Button
variant='secondary'
key={51}
onClick={() => deleteCheckedSignals()} style={buttonStyle}>
<Icon icon="minus" /> Remove
</Button>
<Button
variant='secondary'
key={52}
onClick={() => handleAdd()} style={buttonStyle}>
<Icon icon="plus" /> Signal
</Button>
</div>
<div>
<Collapse isOpened={isCollapseOpened}>
<h6>Choose a Component Configuration to add the signal to: </h6>
<div className='solid-button'>
{typeof configs !== "undefined" && configs.map(config => (
<Button
variant='secondary'
key={config.id}
onClick={() => handleAdd(config.id)}
style={buttonStyle}
>
{config.name}
</Button>
))}
</div>
</Collapse>
</div>
</Form.Group>
</Dialog>
)
return isShown ? DialogWindow : <></>
}
export default ExportSignalMappingDialog;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 (
<Dialog
show={this.props.show}
title="Import Component Configuration"
buttonTitle="Import"
onClose={(c) => this.onClose(c)}
onReset={() => this.resetState()}
valid={this.valid} >
<Form>
<Form.Group controlId='file' style={{marginBottom: '15px'}}>
<Form.Label>Component Configuration File</Form.Label>
<Form.Control type='file' onChange={this.loadFile} />
</Form.Group>
<Form.Group controlId="name" >
<Form.Label>Name</Form.Label>
<Form.Control
readOnly={!this.imported}
isValid={this.validateForm('name')}
type="text"
placeholder="Enter name"
value={this.state.name}
onChange={(e) => this.handleChange(e)}
/>
<Form.Control.Feedback />
</Form.Group>
</Form>
</Dialog>
);
}
}
export default ImportConfigDialog;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 (
<Dialog
show={this.props.show}
title="Import Dashboard"
buttonTitle="Import"
onClose={(c) => this.onClose(c)}
onReset={() => this.resetState()}
valid={this.valid}>
<Form>
<Form.Group controlId="file">
<Form.Label>Dashboard File</Form.Label>
<Form.Control type="file" onChange={(e) => this.loadFile(e.target.files)} />
</Form.Group>
<Form.Group controlId="name" >
<Form.Label>Name</Form.Label>
<Form.Control
readOnly={!this.imported}
isValid={this.validateForm('name')}
type="text"
placeholder="Enter name"
value={this.state.name}
onChange={(e) => this.handleChange(e)}
/>
<Form.Control.Feedback />
</Form.Group>
</Form>
</Dialog>
);
}
}
export default ImportDashboardDialog;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 <Dialog
show={this.props.show}
title="Import Scenario"
buttonTitle="Import"
onClose={this.onClose}
onReset={this.resetState}
valid={this.valid}
>
<Form>
<Form.Group as={Col} controlId="file" style={{marginBottom: '15px'}}>
<Form.Label>Scenario File</Form.Label>
<Form.Control type="file" onChange={this.loadFile} />
</Form.Group>
<Form.Group as={Col} controlId="name" style={{marginBottom: '15px'}}>
<Form.Label>Name</Form.Label>
<Form.Control readOnly={this.imported === false} type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<Form.Control.Feedback />
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Start Parameters</Form.Label>
<ParametersEditor content={this.state.startParameters} onChange={this.handleStartParametersChange} disabled={this.imported === false} />
</Form.Group>
</Form>
</Dialog>;
}
}
export default ImportScenarioDialog;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 <Dialog
show={this.props.show}
title="New Scenario"
buttonTitle="Add"
onClose={this.onClose}
onReset={this.resetState}
valid={this.valid}>
<Form>
<Form.Group as={Col} controlId="name" style={{marginBottom: '15px'}}>
<Form.Label>Name</Form.Label>
<Form.Control type="text" placeholder="Enter name" value={this.state.name} onChange={this.handleChange} />
<Form.Control.Feedback />
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Start Parameters</Form.Label>
<ParametersEditor content={this.state.startParameters} onChange={this.handleStartParametersChange} />
</Form.Group>
</Form>
</Dialog>;
}
}
export default NewScenarioDialog;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 (
<Dialog
show={this.props.show}
title={"Component Configurations for Result No. " + this.props.resultNo}
buttonTitle="Close"
onClose={(c) => this.onClose(c)}
valid={true}
size="lg"
blendOutCancel={true}
>
<Form>
<ReactJson
src={this.props.configs}
name={false}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard={false}
collapsed={false}
/>
</Form>
</Dialog>
);
}
}
export default ResultConfigDialog;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 (
<Dialog
show={this.props.show}
title={'Use Result ' + result.id + ' in Jupyter Notebooks'}
buttonTitle='Close'
onClose={(cancelled) => this.props.onClose()}
valid={true}
size='lg'
blendOutCancel={true}
>
<p>Use the following Python code-snippet to fetch and load your results as a Pandas dataframe.</p>
<p><b>1)</b> Please install the <a href={this.villasDataProcessingUrl}>villas-controller</a> Python package:</p>
<SyntaxHighlighter
language="bash"
style={github}>
{this.getPythonDependencies(false)}
</SyntaxHighlighter>
<p><b>2a)</b> Insert the following snippet your Python code:</p>
<SyntaxHighlighter
language="python"
style={github}>
{code}
</SyntaxHighlighter>
<CopyToClipboard text={code}>
<Button>
<Icon style={{color: 'white'}} icon='clipboard'/>&nbsp;
Copy to Clipboard
</Button>
</CopyToClipboard>
<p style={{marginTop: '2em'}}><b>2b)</b> Or alternatively, download the following generated Jupyter notebook to get started:</p>
<Button onClick={this.downloadJupyterNotebook.bind(this)}>
<Icon style={{color: 'white'}} icon='download'/>&nbsp;
Download Jupyter Notebook
</Button>
</Dialog>
);
}
}
export default ResultPythonDialog;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 <div>Loading...</div>
} else {
const tooltip = scenario.isLocked ? "View files of scenario" : "Add, edit or delete files of scenario";
return (
<div className='section'>
<div className='section-buttons-group-right'>
<IconButton
childKey="0"
tooltip={tooltip}
onClick={() => console.log("click")}
icon="file"
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</div>
<h1>
{scenario.name}
<span className='icon-button'>
<IconToggleButton
childKey={0}
index={scenario.id}
onChange={() => 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}
/>
</span>
</h1>
{/* <EditFilesDialog
sessionToken={sessionToken}
show={false}
onClose={null}
signals={null}
files={[]}
scenarioID={scenario.id}
locked={scenario.isLocked}
/> */}
{ areICsLoading?
<div>isLoading...</div>
:
<ConfigsTable
ics={ics}
scenario={scenario}
/>
}
<DashboardsTable
scenario={scenario}
/>
<ResultsTable
scenario={scenario}
/>
<UsersTable
scenario={scenario}
/>
</div>
);
}
}
export default Scenario;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 (
<div className="section">
<h1>
Scenarios
<span className="icon-button">
<IconButton
childKey={0}
tooltip="Add Scenario"
onClick={() => setIsNewModalOpened(true)}
icon="plus"
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
<IconButton
childKey={1}
tooltip="Import Scenario"
onClick={() => {}}
icon="upload"
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h1>
<Table data={scenarios}>
{currentUser.role === "Admin" ? (
<DataColumn title="ID" dataKey="id" />
) : (
<></>
)}
<LinkColumn
title="Name"
dataKey="name"
link="/scenarios/"
linkKey="id"
/>
{currentUser.role === "Admin" ? (
<ButtonColumn
title="Locked"
lockButton
onChangeLock={(index, event) => onScenarioLock(index)}
isLocked={(index) => {return scenarios[index].isLocked}}
/>
) : (
<></>
)}
<ButtonColumn
width="200"
align="right"
editButton
deleteButton
exportButton
duplicateButton
onEdit={(index) =>{
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}}
/>
</Table>
<NewScenarioDialog
show={isNewModalOpened}
onClose={(data) => onAddScenario(data)}
/>
<EditScenarioDialog
show={isEditModalOpened}
onClose={(data) => {onEditScenario(data)}}
scenario={modalScenario}
/>
<ImportScenarioDialog
show={false}
onClose={(data) => {}}
nodes={null}
/>
<DeleteDialog
title="scenario"
name={modalScenario.name}
show={isDeleteModalOpened}
onClose={(e) => onDeleteScenario(e)}
/>
</div>
);
}
export default Scenarios;

View file

@ -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'
}

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 (<div className={classNames('section', 'box')}>
<Row className='align-items-center'>
<Col style={{padding: '10px'}} md='auto' lg='auto'>
<Form>
<DateTimePicker
onChange={(newTime) => setTime(newTime)}
value={time}
disableClock={true}
/>
</Form>
</Col>
<Col style={{padding: '20px'}} md='auto' lg='auto'>
<ActionBoardButtonGroup
disabled={selectedConfigs.length == 0}
onStart={() => handleConfigStart()}
onPauseResume={() => handleConfigPause()}
onStop={() => handleConfigStop()}
onReset={null}
onShutdown={null}
onDelete={null}
onRecreate={null}
paused={false}
/>
</Col>
<Col style={{padding: '20px'}} md='auto' lg='auto'>
<Form.Group controlId="resultCheck">
<Form.Check
type="checkbox"
label="Create Result"
checked={isResultRequested}
onChange={() => setIsResultRequested(prevState => !prevState)}
/>
</Form.Group>
</Col>
</Row>
<small className="text-muted">Select time for synced command execution</small>
</div>);
}
export default ConfigActionBoard;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 (
<div>
{/*Component Configurations table*/}
<h2 style={tableHeadingStyle}>Component Configurations
<span className='icon-button'>
<IconButton
childKey={0}
tooltip='Add Component Configuration'
onClick={() => setIsNewModalOpened(true)}
icon='plus'
disabled={scenario.isLocked}
hidetooltip={scenario.isLocked}
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
<IconButton
childKey={1}
tooltip='Import Component Configuration'
onClick={() => setIsImportModalOpened(true)}
icon='upload'
disabled={scenario.isLocked}
hidetooltip={scenario.isLocked}
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h2>
<Table
data={configs}
allRowsChecked={false}
>
<CheckboxColumn
enableCheckAll
onCheckAll={() => toggleCheckAllConfigs()}
allChecked={areAllConfigsChecked}
checked={(config) => isConfigChecked(config)}
checkboxDisabled={(index) => !usesExternalIC(index)}
onChecked={(config, event) => handleConfigCheck(config, event)}
width={20}
/>
{currentUser.role === "Admin" ?
<DataColumn
title='ID'
dataKey='id'
width={70}
/>
: <></>
}
<DataColumn
title='Name'
dataKey='name'
width={250}
/>
<ButtonColumn
title='# Output Signals'
dataKey='id'
editButton
onEdit={index => {
setMappingModalDirection('out');
setSignalMappingConfigID(configs[index].id);
setIsEditSignalMappingModalOpened(true);
}}
width={150}
locked={scenario.isLocked}
modifier={(configId) => <span>{getNumberOfSignalsModifier(configId, 'out')}</span>}
/>
<ButtonColumn
title='# Input Signals'
dataKey='id'
editButton
onEdit={index => {
setMappingModalDirection('in');
setSignalMappingConfigID(configs[index].id);
setIsEditSignalMappingModalOpened(true);
}}
width={150}
locked={scenario.isLocked}
modifier={(configId) => <span>{getNumberOfSignalsModifier(configId, 'in')}</span>}
/>
<ButtonColumn
title='Autoconfigure Signals'
signalButton
onAutoConf={(index) => {}}
width={170}
locked={scenario.isLocked}
/>
<DataColumn
title='Infrastructure Component'
dataKey='icID'
modifier={(icID) => getICName(icID)}
width={200}
/>
<ButtonColumn
title=''
width={200}
align='right'
editButton
deleteButton
exportButton
duplicateButton
onEdit={index => {}}
onDelete={(index) => {
setConfigToDelete(configs[index])
setIsDeleteModalOpened(true);
}}
onExport={index => {
handleConfigExport(configs[index]);
}}
onDuplicate={index => {
handleDuplicateConfig(configs[index]);
}}
locked={scenario.isLocked}
/>
</Table>
<ConfigActionBoard
selectedConfigs={configs.filter(c => isConfigChecked(c))}
scenarioID={scenario.id}
/>
<NewDialog
show={isNewModalOpened}
title="New Component Configuration"
inputLabel="Name"
placeholder="Enter name"
onClose={data => newConfig(data)}
/>
<ImportConfigDialog
show={isImportModalOpened}
onClose={data => importConfig(data)}
ics={ics}
/>
<DeleteDialog
title="component configuration"
name={configToDelete.name}
show={isDeleteModalOpened}
onClose={(c) => deleteConfig(c)}
/>
<EditSignalMappingDialog
isShown={isEditSignalMappingModalOpened}
direction={mappingModalDirection}
onClose={() => setIsEditSignalMappingModalOpened(false)}
configID={signalMappingConfigID}
/>
</div>
)
}
export default ConfigsTable;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 (
<div>
{/*Dashboard table*/}
<h2 style={tableHeadingStyle}>Dashboards
<span className='icon-button'>
<IconButton
childKey={0}
tooltip='Add Dashboard'
onClick={() => {
setIsNewModalOpened(true);
}}
icon='plus'
disabled={scenario.isLocked}
hidetooltip={scenario.isLocked}
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
<IconButton
childKey={1}
tooltip='Import Dashboard'
onClick={() => {
setIsImportModalOpened(true);
}}
icon='upload'
disabled={scenario.isLocked}
hidetooltip={scenario.isLocked}
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h2>
<Table data={dashboards}>
{currentUser.role === "Admin" ?
<DataColumn
title='ID'
dataKey='id'
width={70}
/>
: <></>
}
<LinkColumn
title='Name'
dataKey='name'
link='/dashboards/'
linkKey='id'
width={300}
/>
<DataColumn
title='Grid'
dataKey='grid'
width={100}
/>
<ButtonColumn
title=''
width={200}
align='right'
editButton
deleteButton
exportButton
duplicateButton
onEdit={index => {
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}
/>
</Table>
<NewDialog
show={isNewModalOpened}
title="New Dashboard"
inputLabel="Name"
placeholder="Enter name"
onClose={data => handleNewDashboard(data)}
/>
<EditDashboardDialog
show={isEditModalOpened}
dashboard={dashboardToEdit}
onClose={data => handleEditDashboard(data)}
/>
<ImportDashboardDialog
show={isImportModalOpened}
onClose={data => handleImportDashboard(data)}
/>
<DeleteDialog
title="dashboard"
name={dashboardToDelete.name}
show={isDeleteModalOpened}
onClose={(isConfirmed) => handleDeleteDashboard(isConfirmed)}
/>
</div>
)
}
export default DashboardsTable;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 <Button variant="link" style={{ color: '#047cab' }} onClick={() => openResultConfigSnapshots(result)}>{id}</Button>
}
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 (
<div>
<h2 style={tableHeadingStyle}>Results
<span className='icon-button'>
<IconButton
childKey={1}
tooltip='Add Result'
onClick={() => setIsNewDialogOpened(true)}
icon='plus'
disabled={scenario.isLocked}
hidetooltip={scenario.isLocked}
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h2>
<Table data={results}>
<DataColumn
title='ID'
dataKey='id'
width={70}
modifier={(id, result) => modifyResultNoColumn(id, result)}
/>
<DataColumn
title='Description'
dataKey='description'
width={300}
/>
<DataColumn
title='Created at'
dataKey='createdAt'
width={200}
/>
<DataColumn
title='Last update'
dataKey='updatedAt'
width={200}
/>
<LinkbuttonColumn
title='Files'
dataKey='resultFileIDs'
linkKey='filebuttons'
data={files}
onDownload={(fileID) => handleDownloadFile(fileID)}
width={300}
/>
<ButtonColumn
width={200}
align='right'
editButton
pythonResultsButton
downloadAllButton
deleteButton
onPythonResults={(index) => {
setIsPythonDialogOpened(true);
setModalResultsIndex(index);
}}
onEdit={(index) => {}}
onDownloadAll={(index) => handleDownloadAllFiles(index)}
onDelete={(index) => {
setIsDeleteModalOpened(true);
setResultToDelete(results[index]);
}}
locked={scenario.isLocked}
/>
</Table>
{/* <EditResultDialog
sessionToken={sessionToken}
show={false}
files={[]}
results={results}
resultId={0}
scenarioID={scenario.id}
onClose={() => {}}
/> */}
<DeleteDialog
title="result"
name={resultToDelete.id}
show={isDeleteModalOpened}
onClose={(e) => handleDeleteResult(e)}
/>
<ResultConfigDialog
show={isResultConfigDialogOpened}
configs={modalResultConfigs}
resultNo={modalResultConfigsIndex}
onClose={() => setIsResultConfigDialogOpened(false)}
/>
<ResultPythonDialog
show={isPythonDialogOpened}
files={files}
results={results}
resultId={modalResultsIndex}
onClose={() => setIsPythonDialogOpened(false)}
/>
<NewDialog
show={isNewDialogOpened}
title="New Result"
inputLabel="Description"
placeholder="Enter description"
onClose={data => handleNewResult(data)}
/>
</div>
)
}
export default ResultsTable;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 (
<div>
{/*Scenario Users table*/}
<h2 style={tableHeadingStyle}>Users sharing this scenario</h2>
<Table data={users}>
{currentUser.role === "Admin" ?
<DataColumn
title='ID'
dataKey='id'
width={70}
/>
: <></>
}
<DataColumn
title='Name'
dataKey='username'
width={300}
/>
<DataColumn
title='Role'
dataKey='role'
width={100}
/>
<ButtonColumn
title=''
width={30}
align='right'
deleteButton
onDelete={(index) => {
setIsDeleteUserModalOpened(true);
setUsernameToDelete(users[index].username);
}}
locked={scenario.isLocked}
/>
</Table>
<InputGroup
style={{
width: 400,
float: 'right'
}}
>
<Form.Control
placeholder="Username"
onChange={(e) => {setUsernameToAdd(e.target.value)}}
value={usernameToAdd}
type="text"
/>
<span className='icon-button'>
<IconButton
childKey={1}
tooltip='Add User to Scenario'
onClick={() => {
addUser()
}}
icon='plus'
disabled={false}
hidetooltip={false}
buttonStyle={buttonStyle}
/>
</span>
</InputGroup>
<br />
<br />
<DeleteDialog
title="Delete user from scenario"
name={usernameToDelete}
show={isDeleteUserModalOpened}
onClose={(c) => {removeUser(c)}}
/>
</div>
)
}
export default UsersTable;