diff --git a/package.json b/package.json index 6954861..f7ba094 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@fortawesome/fontawesome-svg-core": "^1.2.34", "@fortawesome/free-solid-svg-icons": "^5.15.2", "@fortawesome/react-fontawesome": "^0.1.14", + "@rjsf/core": "^2.5.1", "babel-runtime": "^6.26.0", "bootstrap": "^4.6.0", "classnames": "^2.2.6", @@ -23,8 +24,8 @@ "gaugeJS": "^1.3.7", "handlebars": "^4.7.7", "jquery": "^3.6.0", - "jszip": "^3.6.0", "jsonwebtoken": "^8.5.1", + "jszip": "^3.6.0", "libcimsvg": "git+https://git.rwth-aachen.de/acs/public/cim/pintura-npm-package.git", "lodash": "^4.17.21", "moment": "^2.29.1", @@ -61,7 +62,7 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, - "proxy": "https://villas.k8s.eonerc.rwth-aachen.de", + "proxy": "http://localhost:4000", "browserslist": { "production": [ ">0.2%", diff --git a/src/common/icon-button.js b/src/common/icon-button.js index a905dd1..2a65b95 100644 --- a/src/common/icon-button.js +++ b/src/common/icon-button.js @@ -25,31 +25,32 @@ import Icon from '../common/icon'; class IconButton extends React.Component { render() { - const altButtonStyle = { - marginLeft: '10px', + let btn = + + let button; + if (!this.props.tooltip || this.props.hidetooltip) { + button = btn; + } else { + button = {this.props.tooltip}} > + {btn} + } - const iconStyle = { - height: '30px', - width: '30px' - } - - return {this.props.tooltip}} > - - + return button; } } diff --git a/src/common/icon-toggle-button.js b/src/common/icon-toggle-button.js new file mode 100644 index 0000000..e9adc8a --- /dev/null +++ b/src/common/icon-toggle-button.js @@ -0,0 +1,62 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; + +import { ToggleButton, ButtonGroup, Tooltip, OverlayTrigger } from 'react-bootstrap'; + +import Icon from './icon'; + + +class IconToggleButton extends React.Component { + + render() { + let tooltip = this.props.checked ? this.props.tooltipChecked : this.props.tooltipUnchecked; + + return {tooltip}} > + + + {this.props.checked ? + + : + + } + + + + } +} + +export default IconToggleButton; diff --git a/src/common/table-column.js b/src/common/table-column.js index fb9905a..042010b 100644 --- a/src/common/table-column.js +++ b/src/common/table-column.js @@ -27,7 +27,10 @@ class TableColumn extends Component { deleteButton: false, showDeleteButton: null, exportButton: false, + signalButton: false, duplicateButton: false, + isLocked: null, + locked: false, link: '/', linkKey: '', dataIndex: false, diff --git a/src/common/table.js b/src/common/table.js index dfba04f..8ea1579 100644 --- a/src/common/table.js +++ b/src/common/table.js @@ -20,6 +20,9 @@ import _ from 'lodash'; import { Table, Button, Form, Tooltip, OverlayTrigger } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import Icon from './icon'; +import IconToggleButton from './icon-toggle-button'; +import IconButton from '../common/icon-button'; + class CustomTable extends Component { constructor(props) { @@ -45,6 +48,7 @@ class CustomTable extends Component { static addCell(data, index, child) { // add data to cell let content = null; + let childkey = 0; if ('dataKeys' in child.props) { for (let key of child.props.dataKeys) { @@ -90,7 +94,7 @@ class CustomTable extends Component { onClick={() => child.props.onDownload(contentkey)} disabled={child.props.onDownload == null}> {contentkey + ' '} - + ); }); @@ -125,26 +129,26 @@ class CustomTable extends Component { cell.push(index); } + let isLocked = child.props.locked || (child.props.isLocked != null && child.props.isLocked(index)); + // add buttons - let showEditButton = child.props.showEditButton !== null && child.props.showEditButton !== undefined + let showEditButton = child.props.showEditButton !== null && child.props.showEditButton !== undefined ? child.props.showEditButton(index) : true; if (child.props.editButton && showEditButton) { cell.push( - Edit } - > - - - ); + child.props.onEdit(index)} + variant={'table-control-button'} + />) } if (child.props.checkbox) { @@ -164,89 +168,112 @@ class CustomTable extends Component { ); } + if (child.props.lockButton) { + cell.push( + child.props.onChangeLock(index)} + checked={isLocked} + checkedIcon='lock' + uncheckedIcon='lock-open' + tooltipChecked='Scenario is locked, cannot be edited' + tooltipUnchecked='Scenario is unlocked, can be edited' + disabled={false} + variant={'table-control-button'} + /> + ); + } + if (child.props.exportButton) { cell.push( - Export } - > - - - ); + child.props.onExport(index)} + variant={'table-control-button'} + />); + } + + if (child.props.signalButton) { + cell.push( + child.props.onAutoConf(index)} + variant={'table-control-button'} + />); } if (child.props.duplicateButton) { cell.push( - Duplicate } > - - - ); + child.props.onDuplicate(index)} + variant={'table-control-button'} + />); } if (child.props.addRemoveFilesButton) { cell.push( - Add/remove File(s)} > - - - ); + child.props.onAddRemove(index)} + variant={'table-control-button'} + />); } if (child.props.downloadAllButton) { cell.push( - Download All Files} > - - - ); + child.props.onDownloadAll(index)} + variant={'table-control-button'} + />); } - let showDeleteButton = child.props.showDeleteButton !== null && child.props.showDeleteButton !== undefined + let showDeleteButton = child.props.showDeleteButton !== null && child.props.showDeleteButton !== undefined ? child.props.showDeleteButton(index) : true; if (child.props.deleteButton && showDeleteButton) { cell.push( - Delete } > - - - ); + child.props.onDelete(index)} + variant={'table-control-button'} + />); } return cell; @@ -351,14 +378,14 @@ class CustomTable extends Component { value={cell} onChange={(event) => children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} ref={ref => { this.activeInput = ref; }} /> - : + : { cell.map((element, elementIndex) => {element} ) } - } + } }) } diff --git a/src/componentconfig/config-table.js b/src/componentconfig/config-table.js index f18fb6d..665850e 100644 --- a/src/componentconfig/config-table.js +++ b/src/componentconfig/config-table.js @@ -298,6 +298,15 @@ class ConfigTable extends Component { } render() { + const buttonStyle = { + marginLeft: '10px', + } + + const iconStyle = { + height: '30px', + width: '30px' + } + return (
{/*Component Configurations table*/} @@ -308,12 +317,20 @@ class ConfigTable extends Component { tooltip='Add Component Configuration' onClick={() => this.addConfig()} icon='plus' + disabled={this.props.locked} + hidetooltip={this.props.locked} + buttonStyle={buttonStyle} + iconStyle={iconStyle} /> this.setState({ importConfigModal: true })} icon='upload' + disabled={this.props.locked} + hidetooltip={this.props.locked} + buttonStyle={buttonStyle} + iconStyle={iconStyle} /> @@ -343,6 +360,7 @@ class ConfigTable extends Component { editButton onEdit={index => this.setState({ editOutputSignalsModal: true, modalConfigData: this.props.configs[index], modalConfigIndex: index })} width={150} + locked={this.props.locked} /> this.setState({ editInputSignalsModal: true, modalConfigData: this.props.configs[index], modalConfigIndex: index })} width={150} + locked={this.props.locked} /> this.signalsAutoConf(index)} - width={150} + title='Autoconfigure Signals' + signalButton + onAutoConf={(index) => this.signalsAutoConf(index)} + width={170} + locked={this.props.locked} /> this.setState({ deleteConfigModal: true, modalConfigData: this.props.configs[index], modalConfigIndex: index })} onExport={index => this.exportConfig(index)} onDuplicate={index => this.duplicateConfig(index)} + locked={this.props.locked} /> diff --git a/src/dashboard/dashboard-button-group.js b/src/dashboard/dashboard-button-group.js index be73aad..ddacfa0 100644 --- a/src/dashboard/dashboard-button-group.js +++ b/src/dashboard/dashboard-button-group.js @@ -17,114 +17,77 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button,OverlayTrigger, Tooltip } from 'react-bootstrap'; -import Icon from "../common/icon"; +import IconButton from '../common/icon-button'; + +const buttonStyle = { + marginLeft: '12px', + height: '44px', + width: '35px', +}; + +const iconStyle = { + height: '25px', + width: '25px' +} + +let buttonkey = 0; class DashboardButtonGroup extends React.Component { - render() { - const buttonStyle = { - marginLeft: '12px', - height: '44px', - width : '35px', - }; - const iconStyle = { - height: '25px', - width : '25px' + getBtn(icon, tooltip, clickFn, locked = false) { + if (locked) { + return + } else { + return } + } + render() { const buttons = []; - let key = 0; - - /*if (this.props.fullscreen) { - return null; - }*/ + buttonkey = 0; if (this.props.editing) { - buttons.push( - Save changes } > - - , - Discard changes } > - - - ); + buttons.push(this.getBtn("save", "Save changes", this.props.onSave)); + buttons.push(this.getBtn("times", "Discard changes", this.props.onCancel)); } else { if (this.props.fullscreen !== true) { - buttons.push( - Change to fullscreen view } > - - - ); + buttons.push(this.getBtn("expand", "Change to fullscreen view", this.props.onFullscreen)); } else { - buttons.push( - Back to normal view } > - - - ); + buttons.push(this.getBtn("compress", "Back to normal view", this.props.onFullscreen)); } if (this.props.paused) { - buttons.push( - Continue simulation } > - - - ); + buttons.push(this.getBtn("play", "Continue simulation", this.props.onUnpause)); } else { - buttons.push( - Pause simulation } > - - - ); + buttons.push(this.getBtn("pause", "Pause simulation", this.props.onPause)); } if (this.props.fullscreen !== true) { - buttons.push( - Add, edit or delete files of scenario }> - - - ); - - buttons.push( - Add, edit or delete input signals }> - - - ); - - buttons.push( - Add, edit or delete output signals }> - - - ); - - buttons.push( - Add widgets and edit layout }> - - - ); + let tooltip = this.props.locked ? "View files of scenario" : "Add, edit or delete files of scenario"; + buttons.push(this.getBtn("file", tooltip, this.props.onEditFiles)); + buttons.push(this.getBtn("sign-in-alt", "Add, edit or delete input signals", this.props.onEditInputSignals, this.props.locked)); + buttons.push(this.getBtn("sign-out-alt", "Add, edit or delete output signals", this.props.onEditOutputSignals, this.props.locked)); + buttons.push(this.getBtn("pen", "Add widgets and edit layout", this.props.onEdit, this.props.locked)); } } diff --git a/src/dashboard/dashboard-table.js b/src/dashboard/dashboard-table.js index cb96fdb..589366b 100644 --- a/src/dashboard/dashboard-table.js +++ b/src/dashboard/dashboard-table.js @@ -137,6 +137,14 @@ class DashboardTable extends Component { } render() { + const buttonStyle = { + marginLeft: '10px', + } + + const iconStyle = { + height: '30px', + width: '30px' + } return (
@@ -148,12 +156,20 @@ class DashboardTable extends Component { tooltip='Add Dashboard' onClick={() => this.setState({newDashboardModal: true})} icon='plus' + disabled={this.props.locked} + hidetooltip={this.props.locked} + buttonStyle={buttonStyle} + iconStyle={iconStyle} /> this.setState({importDashboardModal: true})} icon='upload' + disabled={this.props.locked} + hidetooltip={this.props.locked} + buttonStyle={buttonStyle} + iconStyle={iconStyle} /> @@ -198,6 +214,7 @@ class DashboardTable extends Component { })} onExport={index => this.exportDashboard(index)} onDuplicate={index => this.duplicateDashboard(index)} + locked={this.props.locked} /> diff --git a/src/dashboard/dashboard.js b/src/dashboard/dashboard.js index 7eebb2a..ee838f3 100644 --- a/src/dashboard/dashboard.js +++ b/src/dashboard/dashboard.js @@ -27,6 +27,7 @@ import WidgetContextMenu from '../widget/widget-context-menu'; import WidgetToolbox from '../widget/widget-toolbox'; import WidgetArea from '../widget/widget-area'; import DashboardButtonGroup from './dashboard-button-group'; +import IconToggleButton from '../common/icon-toggle-button'; import DashboardStore from './dashboard-store'; import SignalStore from '../signal/signal-store' @@ -35,6 +36,8 @@ import WidgetStore from '../widget/widget-store'; import ICStore from '../ic/ic-store' import ConfigStore from '../componentconfig/config-store' import AppDispatcher from '../common/app-dispatcher'; +import ScenarioStore from '../scenario/scenario-store'; + import 'react-contexify/dist/ReactContexify.min.css'; import WidgetContainer from '../widget/widget-container'; @@ -45,7 +48,7 @@ class Dashboard extends Component { static lastWidgetKey = 0; static webSocketsOpened = false; static getStores() { - return [DashboardStore, FileStore, WidgetStore, SignalStore, ConfigStore, ICStore]; + return [DashboardStore, FileStore, WidgetStore, SignalStore, ConfigStore, ICStore, ScenarioStore]; } static calculateState(prevState, props) { @@ -80,9 +83,14 @@ class Dashboard extends Component { // filter component configurations to the ones that belong to this scenario let configs = []; let files = []; + let locked = false; if (dashboard !== undefined) { configs = ConfigStore.getState().filter(config => config.scenarioID === dashboard.scenarioID); files = FileStore.getState().filter(file => file.scenarioID === dashboard.scenarioID); + let scenario = ScenarioStore.getState().find(s => s.id === dashboard.scenarioID); + if (scenario) { + locked = scenario.isLocked; + } if (dashboard.height === 0) { dashboard.height = 400; } @@ -144,6 +152,7 @@ class Dashboard extends Component { widgetOrigIDs: prevState.widgetOrigIDs || [], maxWidgetHeight: maxHeight || null, + locked, }; } @@ -216,6 +225,13 @@ class Dashboard extends Component { param: '?scenarioID=' + this.state.dashboard.scenarioID, token: this.state.sessionToken }); + + // load scenario for 'isLocked' value + AppDispatcher.dispatch({ + type: 'scenarios/start-load', + data: this.state.dashboard.scenarioID, + token: this.state.sessionToken + }); } } @@ -482,6 +498,15 @@ class Dashboard extends Component { return
{"Loading Dashboard..."}
} + const buttonStyle = { + marginLeft: '10px', + } + + const iconStyle = { + height: '25px', + width: '25px' + } + const grid = this.state.dashboard.grid; const boxClasses = classNames('section', 'box', { 'fullscreen-padding': this.props.isFullscreen }); let draggable = this.state.editing; @@ -489,10 +514,26 @@ class Dashboard extends Component { return (
-

{this.state.dashboard.name}

+

+ {this.state.dashboard.name} + + + +

this.onClose()} blendOutCancel = {true} @@ -139,6 +141,7 @@ class EditFilesDialog extends React.Component { onDelete={(index) => this.deleteFile(index)} editButton onEdit={index => this.setState({ editModal: true, modalFile: this.props.files[index] })} + locked={this.props.locked} /> @@ -146,13 +149,17 @@ class EditFilesDialog extends React.Component {
Add file
- this.selectUploadFile(event)} /> + this.selectUploadFile(event)} + disabled={this.props.locked} + /> diff --git a/src/result/result-table.js b/src/result/result-table.js index 92b8b62..a38d160 100644 --- a/src/result/result-table.js +++ b/src/result/result-table.js @@ -154,19 +154,31 @@ class ResultTable extends Component { } render() { + const buttonStyle = { + marginLeft: '10px', + } + + const iconStyle = { + height: '30px', + width: '30px' + } return (
{/*Result table*/}

Results - + this.setState({ newResultModal: true })} icon='plus' + disabled={this.props.locked} + hidetooltip={this.props.locked} + buttonStyle={buttonStyle} + iconStyle={iconStyle} /> - +

@@ -208,6 +220,7 @@ class ResultTable extends Component { onEdit={index => this.setState({ editResultsModal: true, modalResultsIndex: index })} onDownloadAll={(index) => this.downloadResultData(this.props.results[index])} onDelete={(index) => this.setState({ deleteResultsModal: true, modalResultsData: this.props.results[index], modalResultsIndex: index })} + locked={this.props.locked} />
diff --git a/src/scenario/scenario-users-table.js b/src/scenario/scenario-users-table.js index c93b356..26eb06f 100644 --- a/src/scenario/scenario-users-table.js +++ b/src/scenario/scenario-users-table.js @@ -16,11 +16,11 @@ ******************************************************************************/ import React, {Component} from "react"; -import {Button, Form, InputGroup} from "react-bootstrap"; +import { Form, InputGroup} from "react-bootstrap"; import {Redirect} from "react-router-dom"; import Table from "../common/table"; import TableColumn from "../common/table-column"; -import Icon from "../common/icon"; +import IconButton from "../common/icon-button"; import DeleteDialog from "../common/dialogs/delete-dialog"; import AppDispatcher from "../common/app-dispatcher"; @@ -82,15 +82,6 @@ class ScenarioUsersTable extends Component { return (); } - const altButtonStyle = { - marginLeft: '10px', - } - - const iconStyle = { - height: '30px', - width: '30px' - } - return (
{/*Scenario Users table*/} @@ -124,6 +115,7 @@ class ScenarioUsersTable extends Component { deleteUserName: this.props.scenario.users[index].username, modalUserIndex: index })} + locked={this.props.locked} /> @@ -141,13 +133,14 @@ class ScenarioUsersTable extends Component { /> - + this.addUser()} + icon='plus' + disabled={this.props.locked} + hidetooltip={this.props.locked} + /> diff --git a/src/scenario/scenario.js b/src/scenario/scenario.js index 1e1033c..ac618d7 100644 --- a/src/scenario/scenario.js +++ b/src/scenario/scenario.js @@ -20,6 +20,7 @@ import { Container } from 'flux/utils'; import AppDispatcher from '../common/app-dispatcher'; import IconButton from '../common/icon-button'; +import IconToggleButton from '../common/icon-toggle-button'; import ScenarioStore from './scenario-store'; import ICStore from '../ic/ic-store'; @@ -70,26 +71,24 @@ class Scenario extends React.Component { } componentDidMount() { - - let token = localStorage.getItem("token") let scenarioID = parseInt(this.props.match.params.scenario, 10) //load selected scenario AppDispatcher.dispatch({ type: 'scenarios/start-load', data: scenarioID, - token: token + token: this.state.sessionToken }); AppDispatcher.dispatch({ type: 'scenarios/start-load-users', data: scenarioID, - token: token + token: this.state.sessionToken }); // load ICs to enable that component configs and dashboards work with them AppDispatcher.dispatch({ type: 'ics/start-load', - token: token + token: this.state.sessionToken }); } @@ -112,11 +111,35 @@ class Scenario extends React.Component { this.setState({ filesEditModal: false }); } + /* ############################################## + * Change locked state of scenario + ############################################## */ + + onChangeLock() { + let data = {}; + data.id = this.state.scenario.id; + data.isLocked = !this.state.scenario.isLocked; + + AppDispatcher.dispatch({ + type: 'scenarios/start-edit', + data, + token: this.state.sessionToken + }); + } + /* ############################################## * Render method ############################################## */ render() { + const buttonStyle = { + marginLeft: '10px', + } + + const iconStyle = { + height: '30px', + width: '30px' + } const tableHeadingStyle = { paddingTop: '30px' @@ -126,16 +149,36 @@ class Scenario extends React.Component { return

Loading Scenario...

; } + let tooltip = this.state.scenario.isLocked ? "View files of scenario" : "Add, edit or delete files of scenario"; + return
-

{this.state.scenario.name}

+

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

diff --git a/src/scenario/scenarios.js b/src/scenario/scenarios.js index 9d81a74..89b52d9 100644 --- a/src/scenario/scenarios.js +++ b/src/scenario/scenarios.js @@ -73,7 +73,7 @@ class Scenarios extends Component { } closeNewModal(data) { - if(data) { + if (data) { AppDispatcher.dispatch({ type: 'scenarios/start-add', data: data, @@ -218,7 +218,7 @@ class Scenarios extends Component { jsonObj["configs"] = this.getConfigs(scenario.id); jsonObj["dashboards"] = this.getDashboards(scenario.id); - if(jsonObj) { + if (jsonObj) { AppDispatcher.dispatch({ type: 'scenarios/start-add', data: jsonObj, @@ -227,68 +227,99 @@ class Scenarios extends Component { } } - modifyRunningColumn(running){ - return + isLocked(index) { + return this.state.scenarios[index].isLocked; + } + + onLock(index) { + let data = {}; + data.id = this.state.scenarios[index].id; + data.isLocked = !this.state.scenarios[index].isLocked; + + AppDispatcher.dispatch({ + type: 'scenarios/start-edit', + data, + token: this.state.sessionToken + }); } render() { + const buttonStyle = { + marginLeft: '10px', + } + + const iconStyle = { + height: '30px', + width: '30px' + } + return
-

Scenarios +

Scenarios - this.setState({ newModal: true })} - icon='plus' - /> - this.setState({ importModal: true })} - icon='upload' - /> - -

- - - {this.state.currentUser.role === "Admin" ? - - : <> - } - this.setState({ newModal: true })} + icon='plus' + buttonStyle={buttonStyle} + iconStyle={iconStyle} /> - this.modifyRunningColumn(running)} + this.setState({ importModal: true })} + icon='upload' + buttonStyle={buttonStyle} + iconStyle={iconStyle} /> + + + +
+ {this.state.currentUser.role === "Admin" ? this.setState({ editModal: true, modalScenario: this.state.scenarios[index] })} - onDelete={index => this.setState({ deleteModal: true, modalScenario: this.state.scenarios[index] })} - onExport={index => this.exportScenario(index)} - onDuplicate={index => this.duplicateScenario(index)} + title='ID' + dataKey='id' /> -
+ : <> + } + + {this.state.currentUser.role === "Admin" ? + this.onLock(index)} + isLocked={index => this.isLocked(index)} + /> + : <> + } + this.setState({ editModal: true, modalScenario: this.state.scenarios[index] })} + onDelete={index => this.setState({ deleteModal: true, modalScenario: this.state.scenarios[index] })} + onExport={index => this.exportScenario(index)} + onDuplicate={index => this.duplicateScenario(index)} + isLocked={index => this.isLocked(index)} + /> + - this.closeNewModal(data)} /> - this.closeEditModal(data)} scenario={this.state.modalScenario} /> - this.closeImportModal(data)} nodes={this.state.nodes} /> + this.closeNewModal(data)} /> + this.closeEditModal(data)} scenario={this.state.modalScenario} /> + this.closeImportModal(data)} nodes={this.state.nodes} /> - this.closeDeleteModal(e)} /> -
; + this.closeDeleteModal(e)} /> +
; } }