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:
parent
7d11f3e5d9
commit
7ae0deee87
18 changed files with 3168 additions and 0 deletions
244
src/pages/scenarios/dialogs/edit-config.js
Normal file
244
src/pages/scenarios/dialogs/edit-config.js
Normal 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;
|
94
src/pages/scenarios/dialogs/edit-dashboard.js
Normal file
94
src/pages/scenarios/dialogs/edit-dashboard.js
Normal 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;
|
101
src/pages/scenarios/dialogs/edit-scenario.js
Normal file
101
src/pages/scenarios/dialogs/edit-scenario.js
Normal 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;
|
205
src/pages/scenarios/dialogs/edit-signal-mapping.js
Normal file
205
src/pages/scenarios/dialogs/edit-signal-mapping.js
Normal 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;
|
129
src/pages/scenarios/dialogs/import-config.js
Normal file
129
src/pages/scenarios/dialogs/import-config.js
Normal 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;
|
127
src/pages/scenarios/dialogs/import-dashboard.js
Normal file
127
src/pages/scenarios/dialogs/import-dashboard.js
Normal 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;
|
126
src/pages/scenarios/dialogs/import-scenario.js
Normal file
126
src/pages/scenarios/dialogs/import-scenario.js
Normal 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;
|
97
src/pages/scenarios/dialogs/new-scenario.js
Normal file
97
src/pages/scenarios/dialogs/new-scenario.js
Normal 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;
|
65
src/pages/scenarios/dialogs/result-configs-dialog.js
Normal file
65
src/pages/scenarios/dialogs/result-configs-dialog.js
Normal 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;
|
286
src/pages/scenarios/dialogs/result-python-dialog.js
Normal file
286
src/pages/scenarios/dialogs/result-python-dialog.js
Normal 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'/>
|
||||
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'/>
|
||||
Download Jupyter Notebook
|
||||
</Button>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ResultPythonDialog;
|
137
src/pages/scenarios/scenario.js
Normal file
137
src/pages/scenarios/scenario.js
Normal 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;
|
238
src/pages/scenarios/scenarios.js
Normal file
238
src/pages/scenarios/scenarios.js
Normal 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;
|
21
src/pages/scenarios/styles.js
Normal file
21
src/pages/scenarios/styles.js
Normal 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'
|
||||
}
|
149
src/pages/scenarios/tables/config-action-board.js
Normal file
149
src/pages/scenarios/tables/config-action-board.js
Normal 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;
|
440
src/pages/scenarios/tables/configs-table.js
Normal file
440
src/pages/scenarios/tables/configs-table.js
Normal 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;
|
306
src/pages/scenarios/tables/dashboards-table.js
Normal file
306
src/pages/scenarios/tables/dashboards-table.js
Normal 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;
|
259
src/pages/scenarios/tables/results-table.js
Normal file
259
src/pages/scenarios/tables/results-table.js
Normal 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;
|
144
src/pages/scenarios/tables/users-table.js
Normal file
144
src/pages/scenarios/tables/users-table.js
Normal 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;
|
Loading…
Add table
Reference in a new issue