diff --git a/doc/Requirements.md b/doc/Requirements.md new file mode 100644 index 0000000..c53871b --- /dev/null +++ b/doc/Requirements.md @@ -0,0 +1,16 @@ +# Requirements {#web-requirements} + +## Services + - NodeJS: Runs VILLASweb frontend + - Go: Runs VILLASweb backend + - PostgreSQL database (min version 11): Backend database + - [swag](https://github.com/swaggo/swag): For automated API documentation creation + - NGinX: Webserver and reverse proxy for backends (only for production) + - Docker: Container management system + +## Installed on your local computer + - NodeJS with npm + - Go (at least version 1.11) + - [swag](https://github.com/swaggo/swag) + - Docker + diff --git a/doc/Structure.md b/doc/Structure.md new file mode 100644 index 0000000..4399f0c --- /dev/null +++ b/doc/Structure.md @@ -0,0 +1,79 @@ +# VILLASweb data structure {#web-datastructure} + +This document describes how data (scenarios, infrastructure components, users etc., not only live data) is structured in VILLASweb. + +## Data model + +![Datamodel](../src/img/datamodel.png) + +VILLASweb features the following data classes: + + - Users + - Infrastructure Components + - Scenarios + * Component Configurations and Signals + * Dashboards and Widgets + * Files + +### Users +- You need a username and a password to authenticate in VILLASweb +- There exist three categories of users: Guest, User, and Admin +- Guests have only read access and cannot modify anything +- Users are normal users, they have access to their scenarios, can see available infrastructure components, and modify their accounts (except for their role) +- Admin users have full access to everything, they are the only users that can create new users or change the role of existing users. Only admin users can add or modify infrastructure components. + +### Infrastructure Components +- Components of research infrastructure +- Category: for example simulator, gateway, amplifier, database, etc. +- Type: for example RTDS, OpalRT, VILLASnode, Cassandra +- Can only be added/ modified by admin users + +### Scenarios +- A collection of component configurations, dashboards, and files for a specific experiment +- Users can have access to multiple scenarios +- Users can be added to and removed from scenarios + +### Component Configurations and Signals +- Configure an infrastructure component for the use in a specific scenario +- Input signals: Signals that can be modified in VILLASweb +- Output signals: Signals that can be visualized on dashboards of VILLASweb +- Parameters: Additional configuration parameters of the infrastructure component +- Signals are the actual live data that is displayed or modified through VILLASweb dashboards + +### Dashboards and Widgets +- Visualize ongoing experiments in real-time +- Interact with ongoing experiments in real-time +- Use widgets to design the dashboard according to the needs + +### Files +- Files can be added to scenarios optionally +- Can be images, model files, CIM xml files +- Can be used in widgets or component configurations + +## Setup strategy + +The easiest way to start from scratch is the following (assuming the infrastructure components are already configured by an admin user, see below): + +1. Create a new scenario. +2. Create and configure a new component configuration and link it with an available infrastructure component. +3. Configure the input and output signals of the component configuration according to the signals provided by the selected infrastructure component. The number of signals and their order (index starting at 1) must match. +4. Create a new dashboard and add widgets as desired. Configure the widgets by right-clicking to open the edit menu +5. If needed, files can be added to the scenario and used by component configurations or widgets (models, images, CIM-files, etc.) +6. For collaboration with other users, users can be added to a scenario + +### Setup of infrastructure components + +In the "Infrastructure Components" menu point admin users can create and edit components to be used in experiments. Normal uses can view the available components, but not edit them. +The components are global at any time and are shared among all users of VILLASweb. + +To create a new infrastructure component, you need to provide: +- Name +- Category (see above for examples) +- Type (see above for examples) +- Location +- Host (network address of the component) + +At the moment, you need to know the input and output signals of the infrastructure component a priori to be able to create compatible component configurations by hand. +An auto-detection mechanism for signals is planned for future releases. + +> Hint: At least one infrastructure component is required to receive data in VILLASweb. diff --git a/doc/development.md b/doc/development.md new file mode 100644 index 0000000..259fb19 --- /dev/null +++ b/doc/development.md @@ -0,0 +1,69 @@ +# Development {#web-development} + +- @subpage web-datastructure + +In order to get started with VILLASweb, you might also want to check our our [demo project](https://git.rwth-aachen.de/acs/public/villas/Demo) which is simple to setup using Docker Compose. + +## Frontend + +### Description + +The website itself based on the React JavaScript framework. + +### Required + + - NodeJS with npm + +### Setup + + - `git clone git@git.rwth-aachen.de/acs/public/villas/web.git` to copy the project on your computer + - `cd VILLASweb` + - `npm install` + +### Running + + - `npm start` + +This runs the development server for the website on your local computer at port 3000. +The backend must be running to make the website work. + +## Backend + +### Description + +The backend of VILLASweb uses the programming language Go and a PostgreSQL data base. + +### Required + + - Go (min version 1.11) + - Running PostgreSQL data base (min version 11) + - [swag](https://github.com/swaggo/swag) + +### Setup and Running + + - `git clone git@git.rwth-aachen.de/acs/public/villas/web-backend-go.git` to copy the project on your computer + - `cd VILLASweb-backend-go` + - `go mod tidy` + - `go run start.go [params]` + +To obtain a list of available parameters use `go run start.go --help`. +To run the tests use `go test $(go list ./... ) -p 1` in the top-level folder of the repo. + +Running the backend will only work if the PostgreSQL database is setup properly. Otherwise, you will get error messages. + +### Auto-generate the API documentation + +The documentation of the VILLASweb API in the OpenAPI format can be auto-generated from the source code documentation using the tool swag. +To do this run the following in the top-level folder of the repo: + +- `go mod tidy` +- `go install github.com/swaggo/swag/cmd/swag` +- `swag init -p pascalcase -g "start.go" -o "./doc/api/"` + +The `.yaml` and `.json` files in OpenAPI swagger format are created in the output folder `doc/api`. + +### PostgreSQL database setup + +Please check the [Readme file in the backend repository](https://git.rwth-aachen.de/acs/public/villas/web-backend-go) for some useful hints on the local setup of the PostreSQL database. + + diff --git a/src/__tests__/widget/edit-widget-control-creator.js b/src/__tests__/widget/edit-widget-control-creator.js index 73efcd9..7f1dded 100644 --- a/src/__tests__/widget/edit-widget-control-creator.js +++ b/src/__tests__/widget/edit-widget-control-creator.js @@ -30,7 +30,6 @@ describe('edit widget control creator', () => { { args: { widgetType: 'Table' }, result: { controlNumber: 2, controlTypes: [EditWidgetCheckboxControl] } }, { args: { widgetType: 'Image' }, result: { controlNumber: 2, controlTypes: [EditFileWidgetControl, EditWidgetAspectControl] } }, { args: { widgetType: 'Gauge' }, result: { controlNumber: 6, controlTypes: [EditWidgetTextControl, EditWidgetSignalControl, EditWidgetCheckboxControl, EditWidgetColorZonesControl, EditWidgetMinMaxControl] } }, - { args: { widgetType: 'PlotTable' }, result: { controlNumber: 5, controlTypes: [EditWidgetSignalsControl, EditWidgetTextControl, EditWidgetTimeControl, EditWidgetMinMaxControl] } }, { args: { widgetType: 'Slider' }, result: { controlNumber: 9, controlTypes: [EditWidgetTextControl, EditWidgetOrientation, EditWidgetSignalControl, EditWidgetCheckboxControl, EditWidgetCheckboxControl, EditWidgetMinMaxControl, EditWidgetNumberControl, EditWidgetNumberControl] } }, { args: { widgetType: 'Button' }, result: { controlNumber: 6, controlTypes: [EditWidgetTextControl, EditWidgetSignalControl, EditWidgetCheckboxControl, EditWidgetNumberControl, EditWidgetNumberControl] } }, { args: { widgetType: 'Box' }, result: { controlNumber: 2, controlTypes: [EditWidgetColorControl, EditWidgetColorControl] } }, diff --git a/src/common/dialogs/dialog.js b/src/common/dialogs/dialog.js index cc387d9..9b3a2c8 100644 --- a/src/common/dialogs/dialog.js +++ b/src/common/dialogs/dialog.js @@ -56,7 +56,7 @@ class Dialog extends React.Component { - + {this.props.blendOutCancel?
: }
diff --git a/src/common/table.js b/src/common/table.js index 020f9bd..b4edea3 100644 --- a/src/common/table.js +++ b/src/common/table.js @@ -63,7 +63,7 @@ class CustomTable extends Component { let cell = []; if (content != null) { - content = content.toString(); + //content = content.toString(); // check if cell should be a link const linkKey = child.props.linkKey; @@ -79,21 +79,24 @@ class CustomTable extends Component { // add label to content const labelKey = child.props.labelKey; if (labelKey && data[labelKey] != null) { - var labelContent = data[labelKey]; + let labelContent = data[labelKey]; if (child.props.labelModifier) { labelContent = child.props.labelModifier(labelContent, data); } + let labelStyle = child.props.labelStyle(data[labelKey], data) + cell.push(   - - {labelContent.toString()} + + {labelContent} ); } + if (child.props.dataIndex) { cell.push(index); } diff --git a/src/dashboard/dashboard-button-group.js b/src/dashboard/dashboard-button-group.js index 9fda299..8baeece 100644 --- a/src/dashboard/dashboard-button-group.js +++ b/src/dashboard/dashboard-button-group.js @@ -65,11 +65,18 @@ class DashboardButtonGroup extends React.Component { ); } + buttons.push( + + ); + buttons.push( ); + } return
diff --git a/src/dashboard/dashboard.js b/src/dashboard/dashboard.js index 489f366..874eb10 100644 --- a/src/dashboard/dashboard.js +++ b/src/dashboard/dashboard.js @@ -22,6 +22,7 @@ import classNames from 'classnames'; import Widget from '../widget/widget'; import EditWidget from '../widget/edit-widget/edit-widget'; +import EditFiles from '../file/edit-files' import WidgetContextMenu from '../widget/widget-context-menu'; import WidgetToolbox from '../widget/widget-toolbox'; import WidgetArea from '../widget/widget-area'; @@ -121,10 +122,12 @@ class Dashboard extends Component { paused: prevState.paused || false, editModal: false, + filesEditModal: prevState.filesEditModal || false, + filesEditSaveState: prevState.filesEditSaveState || [], modalData: null, modalIndex: null, widgetChangeData: [], - widgetAddData:prevState.widgetAddData || [], + widgetOrigIDs: prevState.widgetOrigIDs || [], maxWidgetHeight: maxHeight || null, dropZoneHeight: maxHeight +80 || null, @@ -211,10 +214,6 @@ class Dashboard extends Component { handleDrop(widget) { widget.dashboardID = this.state.dashboard.id; - let tempChanges = this.state.widgetAddData; - tempChanges.push(widget); - - this.setState({ widgetAddData: tempChanges}) AppDispatcher.dispatch({ type: 'widgets/start-add', @@ -262,14 +261,20 @@ class Dashboard extends Component { this.setState({ editModal: true, modalData: widget, modalIndex: index }); }; - uploadFile(data,widget){ - AppDispatcher.dispatch({ - type: 'files/start-upload', - data: data, - token: this.state.sessionToken, - scenarioID: this.state.dashboard.scenarioID, - }); + startEditFiles(){ + let tempFiles = []; + this.state.files.forEach( file => { + tempFiles.push({ + id: file.id, + name: file.name + }); + }) + this.setState({filesEditModal: true, filesEditSaveState: tempFiles}); + } + closeEditFiles(){ + this.setState({ filesEditModal: false }); + // TODO do we need this if the dispatches happen in the dialog? } closeEdit(data){ @@ -279,7 +284,7 @@ class Dashboard extends Component { AppDispatcher.dispatch({ type: 'widgets/start-load', token: this.state.sessionToken, - param: '?dashboardID=1' + param: '?dashboardID=' + this.state.dashboard.id }); this.setState({ editModal: false }); @@ -308,7 +313,9 @@ class Dashboard extends Component { startEditing(){ + let originalIDs = []; this.state.widgets.forEach( widget => { + originalIDs.push(widget.id); if(widget.type === 'Slider' || widget.type === 'NumberInput' || widget.type === 'Button'){ AppDispatcher.dispatch({ type: 'widgets/start-edit', @@ -317,7 +324,7 @@ class Dashboard extends Component { }); } }); - this.setState({ editing: true }); + this.setState({ editing: true, widgetOrigIDs: originalIDs }); }; saveEditing() { @@ -336,7 +343,7 @@ class Dashboard extends Component { data: widget }); }); - this.setState({ editing: false, widgetChangeData: [], widgetAddData: [] }); + this.setState({ editing: false, widgetChangeData: []}); }; saveChanges() { @@ -354,28 +361,23 @@ class Dashboard extends Component { cancelEditing() { //raw widget has no id -> cannot be deleted in its original form - let temp = []; - this.state.widgetAddData.forEach(rawWidget => { this.state.widgets.forEach(widget => { - if(widget.y === rawWidget.y && widget.x === rawWidget.x && widget.type === rawWidget.type){ - temp.push(widget); + let tempID = this.state.widgetOrigIDs.find(element => element === widget.id); + if(typeof tempID === 'undefined'){ + AppDispatcher.dispatch({ + type: 'widgets/start-remove', + data: widget, + token: this.state.sessionToken + }); } }) - }) - temp.forEach( widget => { - AppDispatcher.dispatch({ - type: 'widgets/start-remove', - data: widget, - token: this.state.sessionToken - }); - }); AppDispatcher.dispatch({ type: 'widgets/start-load', token: this.state.sessionToken, - param: '?dashboardID=1' + param: '?dashboardID=' + this.state.dashboard.id }); - this.setState({ editing: false, widgetChangeData: [], widgetAddData: []}); + this.setState({ editing: false, widgetChangeData: []}); }; @@ -416,6 +418,7 @@ class Dashboard extends Component { onFullscreen={this.props.toggleFullscreen} onPause={this.pauseData.bind(this)} onUnpause={this.unpauseData.bind(this)} + onEditFiles = {this.startEditFiles.bind(this)} />
@@ -467,12 +470,20 @@ class Dashboard extends Component { sessionToken={this.state.sessionToken} show={this.state.editModal} onClose={this.closeEdit.bind(this)} - onUpload = {this.uploadFile.bind(this)} widget={this.state.modalData} signals={this.state.signals} files={this.state.files} /> + + ; diff --git a/src/file/edit-files.js b/src/file/edit-files.js new file mode 100644 index 0000000..4f3c8a7 --- /dev/null +++ b/src/file/edit-files.js @@ -0,0 +1,166 @@ +/** + * 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 {FormGroup, FormControl, Button, Col, ProgressBar} from 'react-bootstrap'; +import Dialog from '../common/dialogs/dialog'; +import AppDispatcher from "../common/app-dispatcher"; +import Table from "../common/table"; +import TableColumn from "../common/table-column"; + + +class EditFilesDialog extends React.Component { + valid = true; + + + constructor(props) { + super(props); + + this.state = { + uploadFile: null, + uploadProgress: 0 + }; + } + + onClose(canceled) { + if (canceled === false) { + if (true) { + this.props.onClose(); + } + } else { + this.props.onClose(); + } + } + + selectUploadFile(event) { + this.setState({ uploadFile: event.target.files[0] }); + }; + + startFileUpload(){ + // upload file + const formData = new FormData(); + formData.append("file", this.state.uploadFile); + + AppDispatcher.dispatch({ + type: 'files/start-upload', + data: formData, + token: this.props.sessionToken, + progressCallback: this.updateUploadProgress, + finishedCallback: this.clearProgress, + scenarioID: this.props.scenarioID, + }); + + this.setState({ uploadFile: null }); + }; + + updateUploadProgress = (event) => { + this.setState({ uploadProgress: parseInt(event.percent.toFixed(), 10) }); + }; + + clearProgress = (newFileID) => { + /*if (this.props.onChange != null) { + let event = {} + event["target"] = {} + event.target["value"] = newFileID + this.props.onChange(event); + } + */ + this.setState({ uploadProgress: 0 }); + + + }; + + deleteFile(index){ + + let file = this.props.files[index] + AppDispatcher.dispatch({ + type: 'files/start-remove', + data: file, + token: this.props.sessionToken + }); + } + + + render() { + + let fileOptions = []; + if (this.props.files.length > 0){ + fileOptions.push( + + ) + fileOptions.push(this.props.files.map((file, index) => ( + + ))) + } else { + fileOptions = + } + + const progressBarStyle = { + marginLeft: '100px', + marginTop: '-40px' + }; + + return ( + this.onClose(c)} blendOutCancel = {true} valid={true}> +
+ +
+ + + + + + this.deleteFile(index)} + /> +
+
+ + + this.selectUploadFile(event)} /> + + + + + + + + + +
+
+ ); + } +} + +export default EditFilesDialog; diff --git a/src/ic/ic-action.js b/src/ic/ic-action.js index 4e787af..6a9e591 100644 --- a/src/ic/ic-action.js +++ b/src/ic/ic-action.js @@ -55,11 +55,12 @@ class ICAction extends React.Component { )); return
+ Send command to infrastructure component {actionList} - +
; diff --git a/src/ic/ic-data-data-manager.js b/src/ic/ic-data-data-manager.js index 5aebae9..2e09a7c 100644 --- a/src/ic/ic-data-data-manager.js +++ b/src/ic/ic-data-data-manager.js @@ -58,7 +58,7 @@ class IcDataDataManager { if (socket == null) { return false; } - + console.log("Sending to IC", identifier, "message: ", message); const data = this.messageToBuffer(message); socket.send(data); diff --git a/src/ic/ic-data-store.js b/src/ic/ic-data-store.js index 6de8669..bca9a95 100644 --- a/src/ic/ic-data-store.js +++ b/src/ic/ic-data-store.js @@ -109,7 +109,7 @@ class ICDataStore extends ReduceStore { // update message properties state[action.ic].input.timestamp = Date.now(); state[action.ic].input.sequence++; - state[action.ic].input.values[action.signal] = action.data; + state[action.ic].input.values[action.signal-1] = action.data; ICDataDataManager.send(state[action.ic].input, action.ic); diff --git a/src/ic/ics-data-manager.js b/src/ic/ics-data-manager.js index 73d3e91..89455c3 100644 --- a/src/ic/ics-data-manager.js +++ b/src/ic/ics-data-manager.js @@ -25,8 +25,7 @@ class IcsDataManager extends RestDataManager { } doActions(ic, action, token = null) { - // TODO: Make only infrastructure component id dependent - RestAPI.post(this.makeURL(this.url + '/' + ic.id), action, token).then(response => { + RestAPI.post(this.makeURL(this.url + '/' + ic.id + '/action'), action, token).then(response => { AppDispatcher.dispatch({ type: 'ics/action-started', data: response diff --git a/src/ic/ics.js b/src/ic/ics.js index 603e474..e24cd93 100644 --- a/src/ic/ics.js +++ b/src/ic/ics.js @@ -220,39 +220,40 @@ class InfrastructureComponents extends Component { return Date.now() - new Date(component.stateUpdatedAt) > fiveMinutes; } - static stateLabelStyle(state, component){ - var style = [ 'label' ]; + stateLabelStyle(state, component){ + + var style = [ 'badge' ]; if (InfrastructureComponents.isICOutdated(component) && state !== 'shutdown') { - style.push('label-outdated'); + style.push('badge-outdated'); } switch (state) { case 'running': - style.push('label-success'); + style.push('badge-success'); break; case 'paused': - style.push('label-info'); + style.push('badge-info'); break; case 'idle': - style.push('label-primary'); + style.push('badge-primary'); break; case 'error': - style.push('label-danger'); + style.push('badge-danger'); break; case 'shutdown': - style.push('label-warning'); + style.push('badge-warning'); break; default: - style.push('label-default'); + style.push('badge-default'); } - return style.join(' '); + return style.join(' ') } static stateUpdateModifier(updatedAt) { @@ -273,7 +274,7 @@ class InfrastructureComponents extends Component { this.onICChecked(index, event)} width='30' /> - + this.stateLabelStyle(state, component)} /> diff --git a/src/scenario/scenario.js b/src/scenario/scenario.js index 4053023..5a51cad 100644 --- a/src/scenario/scenario.js +++ b/src/scenario/scenario.js @@ -43,17 +43,20 @@ import EditSignalMapping from "../signal/edit-signal-mapping"; import FileStore from "../file/file-store" import WidgetStore from "../widget/widget-store"; - - class Scenario extends React.Component { static getStores() { - return [ScenarioStore, ConfigStore, DashboardStore, ICStore, LoginStore, SignalStore, FileStore, WidgetStore]; + return [ ScenarioStore, ConfigStore, DashboardStore, ICStore, LoginStore, SignalStore, FileStore, WidgetStore]; } static calculateState(prevState, props) { + if (prevState == null) { + prevState = {}; + } + // get selected scenario const sessionToken = LoginStore.getState().token; + const scenario = ScenarioStore.getState().find(s => s.id === parseInt(props.match.params.scenario, 10)); if (scenario == null) { AppDispatcher.dispatch({ @@ -87,12 +90,12 @@ class Scenario extends React.Component { deleteConfigModal: false, importConfigModal: false, editConfigModal: false, - modalConfigData: {}, + modalConfigData: (prevState.modalConfigData !== {} && prevState.modalConfigData !== undefined )? prevState.modalConfigData : {}, selectedConfigs: [], modalConfigIndex: 0, - editOutputSignalsModal: false, - editInputSignalsModal: false, + editOutputSignalsModal: prevState.editOutputSignalsModal || false, + editInputSignalsModal: prevState.editInputSignalsModal || false, newDashboardModal: false, deleteDashboardModal: false, @@ -252,12 +255,18 @@ class Scenario extends React.Component { this.setState({ selectedConfigs: selectedConfigs }); } - runAction = action => { + runAction(action) { + + if(action.data.action === 'none'){ + console.warn("No command selected. Nothing was sent."); + return; + } + for (let index of this.state.selectedConfigs) { // get IC for component config let ic = null; for (let component of this.state.ics) { - if (component._id === this.state.configs[index].icID) { + if (component.id === this.state.configs[index].icID) { ic = component; } } @@ -358,54 +367,14 @@ class Scenario extends React.Component { * Signal modification methods ############################################## */ - closeDeleteSignalModal(data) { - // data contains the signal to be deleted - if (data) { - - AppDispatcher.dispatch({ - type: 'signals/start-remove', - data: data, - token: this.state.sessionToken - }); - + closeEditSignalsModal(direction){ + if( direction === "in") { + this.setState({editInputSignalsModal: false}); + } else if( direction === "out"){ + this.setState({editOutputSignalsModal: false}); } } - closeNewSignalModal(data) { - //data contains the new signal incl. configID and direction - if (data) { - AppDispatcher.dispatch({ - type: 'signals/start-add', - data: data, - token: this.state.sessionToken - }); - } - } - - closeEditSignalsModal(data, direction) { - - if (direction === "in") { - this.setState({ editInputSignalsModal: false }); - } else if (direction === "out") { - this.setState({ editOutputSignalsModal: false }); - } else { - return; // no valid direction - } - - if (data) { - //data is an array of signals - for (let sig of data) { - //dispatch changes to signals - AppDispatcher.dispatch({ - type: 'signals/start-edit', - data: sig, - token: this.state.sessionToken, - }); - } - } - - } - /* ############################################## * File modification methods ############################################## */ @@ -428,11 +397,15 @@ class Scenario extends React.Component { marginLeft: '10px' }; + const tableHeadingStyle = { + paddingTop: '30px' + } + return

{this.state.scenario.name}

{/*Scenario Users table*/} -

Users

+

Users

@@ -464,7 +437,7 @@ class Scenario extends React.Component { {/*Component Configurations table*/} -

Component Configurations

+

Component Configurations

this.onConfigChecked(index, event)} width='30' /> @@ -497,8 +470,9 @@ class Scenario extends React.Component {
this.runAction(action)} actions={[ + { id: '-1', title: 'Select command', data: { action: 'none' } }, { id: '0', title: 'Start', data: { action: 'start' } }, { id: '1', title: 'Stop', data: { action: 'stop' } }, { id: '2', title: 'Pause', data: { action: 'pause' } }, @@ -527,23 +501,23 @@ class Scenario extends React.Component { this.closeEditSignalsModal(data, direction)} - onAdd={(data) => this.closeNewSignalModal(data)} - onDelete={(data) => this.closeDeleteSignalModal(data)} + onCloseEdit={(direction) => this.closeEditSignalsModal(direction)} direction="Output" signals={this.state.signals} - configID={this.state.modalConfigData.id} /> + configID={this.state.modalConfigData.id} + sessionToken={this.state.sessionToken} + /> this.closeEditSignalsModal(data, direction)} - onAdd={(data) => this.closeNewSignalModal(data)} - onDelete={(data) => this.closeDeleteSignalModal(data)} + onCloseEdit={(direction) => this.closeEditSignalsModal(direction)} direction="Input" signals={this.state.signals} - configID={this.state.modalConfigData.id} /> + configID={this.state.modalConfigData.id} + sessionToken={this.state.sessionToken} + /> {/*Dashboard table*/} -

Dashboards

+

Dashboards

diff --git a/src/scenario/scenarios.js b/src/scenario/scenarios.js index 22711a7..87aa7e5 100644 --- a/src/scenario/scenarios.js +++ b/src/scenario/scenarios.js @@ -214,6 +214,15 @@ class Scenarios extends Component { FileSaver.saveAs(blob, 'scenario - ' + scenario.name + '.json'); } + modifyRunningColumn(running){ + + if(running){ + return + } else { + return + } + + } render() { const buttonStyle = { @@ -227,7 +236,7 @@ class Scenarios extends Component {
- + this.modifyRunningColumn(running)}/> { - const signals = this.state.signals; + let sig = {} if (column === 1) { // Name change if (event.target.value !== '') { - signals[row].name = event.target.value; - this.setState({signals: signals}); - this.valid = true; + sig = this.state.signals[row]; + sig.name = event.target.value; } } else if (column === 2) { // unit change if (event.target.value !== '') { - signals[row].unit = event.target.value; - this.setState({signals: signals}); - this.valid = true; + sig = this.state.signals[row]; + sig.unit = event.target.value; } } else if (column === 0) { //index change - - signals[row].index = parseInt(event.target.value, 10); - this.setState({signals: signals}); - this.valid = true; + sig = this.state.signals[row]; + sig.index = parseInt(event.target.value, 10); } + + if (sig !== {}){ + //dispatch changes to signal + AppDispatcher.dispatch({ + type: 'signals/start-edit', + data: sig, + token: this.props.sessionToken, + }); + } + }; handleDelete = (index) => { let data = this.state.signals[index] - this.props.onDelete(data); + + AppDispatcher.dispatch({ + type: 'signals/start-remove', + data: data, + token: this.props.sessionToken + }); }; handleAdd = () => { - console.log("add signal"); let newSignal = { configID: this.props.configID, @@ -121,12 +111,15 @@ class EditSignalMapping extends React.Component { index: 999 }; - this.props.onAdd(newSignal) + AppDispatcher.dispatch({ + type: 'signals/start-add', + data: newSignal, + token: this.props.sessionToken + }); }; resetState() { - this.valid=false; let signals = this.props.signals.filter((sig) => { return (sig.configID === this.props.configID) && (sig.direction === this.state.dir); @@ -143,7 +136,14 @@ class EditSignalMapping extends React.Component { return( - this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}> + this.onClose(c)} + onReset={() => this.resetState()} + valid={true}> {this.props.direction} Mapping diff --git a/src/styles/app.css b/src/styles/app.css index 672c505..97c6548 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -272,6 +272,27 @@ body { supported by Chrome and Opera */ } +.edit-table table { + background-color: #fff; + table-layout: fixed; + word-wrap: break-word; + width: 467px; +} + +.edit-table th { + position: sticky; + top: 0; + text-align: left; +} + +.edit-table td{ + text-align: left; +} + +.edit-table td { + padding: 2px 8px !important; +} + /** * Toolbox */ @@ -379,6 +400,6 @@ body { margin-right: 0 !important; } -.label-outdated { +.badge-outdated { opacity: 0.4; } diff --git a/src/styles/widgets.css b/src/styles/widgets.css index 4e367a3..192acdd 100644 --- a/src/styles/widgets.css +++ b/src/styles/widgets.css @@ -115,65 +115,6 @@ div[class*="-widget"] .btn[disabled], .btn.disabled, div[class*="-widget"] input /* End edit menu: Colors */ -/* PlotTable widget */ -.plot-table-widget, .plot-widget, .value-widget, .image-widget, .label-widget { - width: 100%; - height: 100%; - padding: 3px 6px; -} - -.plot-table-widget { - display: -webkit-flex; - display: flex; - flex-direction: column; -} - -.plot-table-widget .content { - -webkit-flex: 1 0 auto; - flex: 1 0 auto; - display: -webkit-flex; - display: flex; - flex-direction: column; -} - -.table-plot-row { - -webkit-flex: 1 0 auto; - flex: 1 0 auto; - display: -webkit-flex; - display: flex; -} - -.plot-table-widget .widget-table { - -webkit-flex: 1 0 auto; - flex: 1 0 auto; - flex-basis: 90px; - max-width: 50%; - display: flex; - flex-direction: column; - justify-content: center; - margin-right: 10px; -} - -.plot-table-widget small { - text-align: center; -} - -.plot-table-widget .checkbox label { - height: 100%; - width: 100%; - padding: 6px 12px; - overflow-x: hidden; -} - -.plot-table-widget .btn { - padding: 0px; -} - -.plot-table-widget input[type="checkbox"] { - display: none; -} -/* End PlotTable Widget */ - /* Plot Widget */ .plot-widget { display: -webkit-flex; @@ -438,8 +379,3 @@ div[class*="-widget"] label { border: 2px solid; } /* End box widget */ - -.plot-table-widget .widget-plot { - -webkit-flex: 1 0 auto; - flex: 1 0 auto; -} diff --git a/src/user/users.js b/src/user/users.js index 7deb4b3..d97c539 100644 --- a/src/user/users.js +++ b/src/user/users.js @@ -122,6 +122,16 @@ class Users extends Component { } }; + modifyActiveColumn(active){ + + if(active){ + return + } else { + return + } + + } + render() { return ( @@ -132,8 +142,8 @@ class Users extends Component { - this.getHumanRoleName(role)} /> - + this.getHumanRoleName(role)} /> + this.modifyActiveColumn(active)} /> this.setState({ editModal: true, modalData: this.state.users[index] })} onDelete={index => this.setState({ deleteModal: true, modalData: this.state.users[index] })} />
diff --git a/src/widget/edit-widget/edit-widget-color-control.js b/src/widget/edit-widget/edit-widget-color-control.js index c031597..2158a55 100644 --- a/src/widget/edit-widget/edit-widget-color-control.js +++ b/src/widget/edit-widget/edit-widget-color-control.js @@ -75,7 +75,16 @@ class EditWidgetColorControl extends Component { 'checked': idx === (isCustomProperty ? this.state.widget[parts[0]][parts[1]] : this.state.widget[this.props.controlId]) }); - return ( this.props.handleChange({target: { id: this.props.controlId, value: idx}})} />) + return ( this.props.handleChange({target: { id: this.props.controlId, value: idx}})} />) } ) } diff --git a/src/widget/edit-widget/edit-widget-control-creator.js b/src/widget/edit-widget/edit-widget-control-creator.js index 78bff0c..2b656dc 100644 --- a/src/widget/edit-widget/edit-widget-control-creator.js +++ b/src/widget/edit-widget/edit-widget-control-creator.js @@ -33,7 +33,7 @@ import EditWidgetMinMaxControl from './edit-widget-min-max-control'; import EditWidgetHTMLContent from './edit-widget-html-content'; import EditWidgetParametersControl from './edit-widget-parameters-control'; -export default function CreateControls(widgetType = null, widget = null, sessionToken = null, files = null, signals, handleChange, onUpload) { +export default function CreateControls(widgetType = null, widget = null, sessionToken = null, files = null, signals, handleChange) { // Use a list to concatenate the controls according to the widget type var DialogControls = []; @@ -47,20 +47,20 @@ export default function CreateControls(widgetType = null, widget = null, session break; case 'Action': DialogControls.push( - handleChange(e)} />, + handleChange(e)} direction={'in'} />, ); break; case 'Value': DialogControls.push( handleChange(e)} />, - handleChange(e)} />, + handleChange(e)} direction={'out'}/>, handleChange(e)} />, handleChange(e)} /> ); break; case 'Lamp': DialogControls.push( - handleChange(e)} />, + handleChange(e)} direction={'out'}/>, handleChange(e)} />, handleChange(e)} />, handleChange(e)} />, @@ -69,14 +69,14 @@ export default function CreateControls(widgetType = null, widget = null, session case 'Plot': DialogControls.push( handleChange(e)} />, - handleChange(e)} />, + handleChange(e)} direction={'out'}/>, handleChange(e)} />, handleChange(e)} /> ); break; case 'Table': DialogControls.push( - handleChange(e)} />, + handleChange(e)} direction={'out'}/>, handleChange(e)} /> ); break; @@ -84,32 +84,24 @@ export default function CreateControls(widgetType = null, widget = null, session // Restrict to only image file types (MIME) //let imageControlFiles = files == null? [] : files.filter(file => file.type.includes('image')); DialogControls.push( - handleChange(e)} onUpload={(f,i) => onUpload(f,i)} />, + handleChange(e)} />, handleChange(e)} /> ); break; case 'Gauge': DialogControls.push( handleChange(e)} />, - handleChange(e)} />, + handleChange(e)} direction={'out'}/>, handleChange(e)} />, handleChange(e)} />, handleChange(e)} /> ); break; - case 'PlotTable': - DialogControls.push( - handleChange(e)} />, - handleChange(e)} />, - handleChange(e)} />, - handleChange(e)} /> - ); - break; case 'Slider': DialogControls.push( handleChange(e)} />, handleChange(e)} />, - handleChange(e)} />, + handleChange(e)} direction={'in'}/>, handleChange(e)} />, handleChange(e)} />, handleChange(e)} />, @@ -120,7 +112,7 @@ export default function CreateControls(widgetType = null, widget = null, session case 'Button': DialogControls.push( handleChange(e)} />, - handleChange(e)} />, + handleChange(e)} direction={'in'}/>, handleChange(e)} />, handleChange(e)} />, handleChange(e)} /> @@ -130,7 +122,7 @@ export default function CreateControls(widgetType = null, widget = null, session DialogControls.push( handleChange(e)} />, handleChange(e)} />, - handleChange(e)} /> + handleChange(e)} /> ); break; case 'Label': @@ -149,14 +141,14 @@ export default function CreateControls(widgetType = null, widget = null, session // Restrict to only xml files (MIME) //let topologyControlFiles = files == null? [] : files.filter( file => file.type.includes('xml')); DialogControls.push( - handleChange(e) } onUpload={(f,i) => onUpload(f,i)} /> + handleChange(e) } /> ); break; case 'NumberInput': DialogControls.push( handleChange(e)} />, - handleChange(e)} />, + handleChange(e)} direction={'in'}/>, handleChange(e)} /> ); break; diff --git a/src/widget/edit-widget/edit-widget-file-control.js b/src/widget/edit-widget/edit-widget-file-control.js index 298ba01..a55aa23 100644 --- a/src/widget/edit-widget/edit-widget-file-control.js +++ b/src/widget/edit-widget/edit-widget-file-control.js @@ -16,7 +16,7 @@ ******************************************************************************/ import React from 'react'; -import {FormGroup, FormControl, FormLabel, Button, ProgressBar} from 'react-bootstrap'; +import {FormGroup, FormControl, FormLabel} from 'react-bootstrap'; class EditFileWidgetControl extends React.Component { @@ -24,41 +24,16 @@ class EditFileWidgetControl extends React.Component { super(props); this.state = { - widget: { }, files: [], - fileList: null, - progress: 0 }; } static getDerivedStateFromProps(props, state){ return { - widget: props.widget, files: props.files.filter(file => file.type.includes(props.type)) }; } - startFileUpload = () => { - // get selected file - let formData = new FormData(); - - for (let key in this.state.fileList) { - if (this.state.fileList.hasOwnProperty(key) && this.state.fileList[key] instanceof File) { - formData.append("file", this.state.fileList[key]); - } - } - - this.props.onUpload(formData,this.props.widget); - } - - uploadProgress = (e) => { - this.setState({ progress: Math.round(e.percent) }); - } - - clearProgress = () => { - this.setState({ progress: 0 }); - } - handleFileChange(e){ this.props.handleChange({ target: { id: this.props.controlId, value: e.target.value } }); } @@ -88,17 +63,9 @@ class EditFileWidgetControl extends React.Component { Image this.handleFileChange(e)}>{fileOptions} - - - Upload - this.setState({ fileList: e.target.files }) } /> - - - - ; } } diff --git a/src/widget/edit-widget/edit-widget-number-control.js b/src/widget/edit-widget/edit-widget-number-control.js index 5354863..3b3101f 100644 --- a/src/widget/edit-widget/edit-widget-number-control.js +++ b/src/widget/edit-widget/edit-widget-number-control.js @@ -25,25 +25,36 @@ class EditWidgetNumberControl extends Component { this.state = { widget: { customProperties:{} - } + } }; } - + static getDerivedStateFromProps(props, state){ return{ - widget: props.widget + widget: props.widget }; - } + } render() { let step = 1; if(this.props.controlId ==='customProperties.background_color_opacity'){ step = 0.1; - } + } + + let parts = this.props.controlId.split('.'); + let isCustomProperty = true; + if (parts.length === 1){ + isCustomProperty = false; + } + return ( {this.props.label} - this.props.handleChange(e)} /> + this.props.handleChange(e)} /> ); } diff --git a/src/widget/edit-widget/edit-widget-signal-control.js b/src/widget/edit-widget/edit-widget-signal-control.js index aaa9c24..cc9135d 100644 --- a/src/widget/edit-widget/edit-widget-signal-control.js +++ b/src/widget/edit-widget/edit-widget-signal-control.js @@ -23,13 +23,15 @@ class EditWidgetSignalControl extends Component { super(props); this.state = { - widget: {} + widget: {}, + signals: [] }; } static getDerivedStateFromProps(props, state){ return { - widget: props.widget + widget: props.widget, + signals: props.signals.filter(s => s.direction === props.direction) }; } @@ -51,10 +53,10 @@ class EditWidgetSignalControl extends Component { this.handleSignalChange(e)}> { - this.props.signals.length === 0 ? ( + this.state.signals.length === 0 ? ( ) : ( - this.props.signals.map((signal, index) => ( + this.state.signals.map((signal, index) => ( )) ) diff --git a/src/widget/edit-widget/edit-widget-signals-control.js b/src/widget/edit-widget/edit-widget-signals-control.js index 109df2e..ad4658b 100644 --- a/src/widget/edit-widget/edit-widget-signals-control.js +++ b/src/widget/edit-widget/edit-widget-signals-control.js @@ -23,7 +23,17 @@ class EditWidgetSignalsControl extends Component { super(props); this.state = { + widget: {}, + signals: [], + checkedSignals: props.widget[props.controlId] + }; + } + + + static getDerivedStateFromProps(props, state){ + return { widget: props.widget, + signals: props.signals.filter(s => s.direction === props.direction), checkedSignals: props.widget[props.controlId] }; } @@ -48,10 +58,10 @@ class EditWidgetSignalsControl extends Component { Signals { - this.props.signals === 0 || !this.state.widget.hasOwnProperty(this.props.controlId)? ( + this.state.signals === 0 || !this.state.widget.hasOwnProperty(this.props.controlId)? ( No signals available ) : ( - this.props.signals.map((signal, index) => ( + this.state.signals.map((signal, index) => ( this.handleSignalChange(e.target.checked, signal.id)}> )) - ) + ) } ); diff --git a/src/widget/edit-widget/edit-widget.js b/src/widget/edit-widget/edit-widget.js index 594acf0..01cffdb 100644 --- a/src/widget/edit-widget/edit-widget.js +++ b/src/widget/edit-widget/edit-widget.js @@ -177,8 +177,7 @@ class EditWidgetDialog extends React.Component { this.props.sessionToken, this.props.files, this.props.signals, - (e) => this.handleChange(e), - (f,i) => this.props.onUpload(f,i)); + (e) => this.handleChange(e)); } return ( diff --git a/src/widget/toolbox-item.js b/src/widget/toolbox-item.js index 013c4cf..b61bdc4 100644 --- a/src/widget/toolbox-item.js +++ b/src/widget/toolbox-item.js @@ -51,7 +51,7 @@ class ToolboxItem extends React.Component { if (this.props.disabled === false) { return this.props.connectDragSource(
- + {this.props.icon && } {this.props.name} @@ -61,7 +61,7 @@ class ToolboxItem extends React.Component { else { return (
- + {this.props.icon && } {this.props.name} diff --git a/src/widget/widget-factory.js b/src/widget/widget-factory.js index 4586464..1360988 100644 --- a/src/widget/widget-factory.js +++ b/src/widget/widget-factory.js @@ -105,17 +105,6 @@ class WidgetFactory { widget.customProperties.fontColor = 0; widget.customProperties.resizeTopBottomLock = true; break; - case 'PlotTable': - widget.customProperties.ylabel = ''; - widget.minWidth = 200; - widget.minHeight = 100; - widget.width = 600; - widget.height = 300; - widget.customProperties.time = 60; - widget.customProperties.yMin = 0; - widget.customProperties.yMax = 10; - widget.customProperties.yUseMinMax = false; - break; case 'Image': widget.minWidth = 20; widget.minHeight = 20; diff --git a/src/widget/widget-toolbox.js b/src/widget/widget-toolbox.js index 9a4309b..60be4af 100644 --- a/src/widget/widget-toolbox.js +++ b/src/widget/widget-toolbox.js @@ -39,20 +39,19 @@ class WidgetToolbox extends React.Component { const topologyItemMsg = thereIsTopologyWidget? 'Currently only one is supported' : ''; return
- - - - - - - - - - - - - - + + + + + + + + + + + + +
diff --git a/src/widget/widget.js b/src/widget/widget.js index a2eb294..11fe5c2 100644 --- a/src/widget/widget.js +++ b/src/widget/widget.js @@ -35,7 +35,6 @@ import WidgetValue from './widgets/value'; import WidgetPlot from './widgets/plot'; import WidgetTable from './widgets/table'; import WidgetLabel from './widgets/label'; -import WidgetPlotTable from './widgets/plot-table'; import WidgetImage from './widgets/image'; import WidgetButton from './widgets/button'; import WidgetInput from './widgets/input'; @@ -72,8 +71,12 @@ class Widget extends React.Component { for (let id of props.data.signalIDs){ let signal = signals.find(s => s.id === id); - let config = configs.find(m => m.id === signal.configID); - icIDs[signal.id] = config.icID; + if (signal !== undefined) { + let config = configs.find(m => m.id === signal.configID); + if (config !== undefined){ + icIDs[signal.id] = config.icID; + } + } } return { @@ -81,19 +84,40 @@ class Widget extends React.Component { signals: signals, icIDs: icIDs, files: FileStore.getState(), - - sequence: prevState != null ? prevState.sequence + 1 : 0, - sessionToken: LoginStore.getState().token }; } - inputDataChanged(widget, data) { + inputDataChanged(widget, data, controlID) { + // controlID is the path to the widget customProperty that is changed (for example 'value') + + // modify the widget customProperty + if (controlID !== '') { + let updatedWidget = JSON.parse(JSON.stringify(widget)); + updatedWidget.customProperties[controlID] = data; + AppDispatcher.dispatch({ + type: 'widgets/start-edit', + token: this.state.sessionToken, + data: updatedWidget + }); + } + // The following assumes that a widget modifies/ uses exactly one signal + + // get the signal with the selected signal ID + let signalID = widget.signalIDs[0]; + let signal = this.state.signals.filter(s => s.id === signalID) + if (signal.length === 0){ + console.warn("Unable to send signal for signal ID", signalID, ". Signal not found."); + return; + } + // determine ID of infrastructure component related to signal[0] + // Remark: there is only one selected signal for an input type widget + let icID = this.state.icIDs[signal[0].id]; AppDispatcher.dispatch({ type: 'icData/inputChanged', - ic: this.state.icIDs[0], - signal: this.state.signals[0].index, + ic: icID, + signal: signal[0].index, data }); } @@ -101,37 +125,102 @@ class Widget extends React.Component { createWidget(widget) { if (widget.type === 'CustomAction') { - return + return } else if (widget.type === 'Action') { - return + return } else if (widget.type === 'Lamp') { - return + return } else if (widget.type === 'Value') { - return + return } else if (widget.type === 'Plot') { - return + return } else if (widget.type === 'Table') { - return + return } else if (widget.type === 'Label') { - return - } else if (widget.type === 'PlotTable') { - return this.props.onWidgetStatusChange(w, this.props.index)} paused={this.props.paused} /> + return } else if (widget.type === 'Image') { - return + return } else if (widget.type === 'Button') { - return this.inputDataChanged(widget, value)} signals={this.state.signals} /> + return this.inputDataChanged(widget, value, controlID)} + signals={this.state.signals} + /> } else if (widget.type === 'NumberInput') { - return this.inputDataChanged(widget, value)} signals={this.state.signals} /> + return this.inputDataChanged(widget, value, controlID)} + signals={this.state.signals} + /> } else if (widget.type === 'Slider') { - return this.props.onWidgetStatusChange(w, this.props.index) } onInputChanged={value => this.inputDataChanged(widget, value)} signals={this.state.signals}/> + return this.props.onWidgetStatusChange(w, this.props.index) } + onInputChanged={(value, controlID) => this.inputDataChanged(widget, value, controlID)} + signals={this.state.signals} + /> } else if (widget.type === 'Gauge') { - return + return } else if (widget.type === 'Box') { - return + return } else if (widget.type === 'HTML') { - return + return } else if (widget.type === 'Topology') { - return + return } return null; diff --git a/src/widget/widgets/button.js b/src/widget/widgets/button.js index 977a8ae..e3a11a0 100644 --- a/src/widget/widgets/button.js +++ b/src/widget/widgets/button.js @@ -30,32 +30,41 @@ class WidgetButton extends Component { onPress(e) { - if (!this.props.widget.customProperties.toggle) { + if (e.button === 0 && !this.props.widget.customProperties.toggle) { this.setState({ pressed: true }); this.valueChanged(this.props.widget.customProperties.on_value); } } onRelease(e) { - - let nextState = false; - if (this.props.widget.customProperties.toggle) { - nextState = !this.state.pressed; + + if (e.button === 0) { + let nextState = false; + if (this.props.widget.customProperties.toggle) { + nextState = !this.state.pressed; + } + this.props.widget.customProperties.pressed = nextState; + this.setState({pressed: nextState}); + this.valueChanged(nextState ? this.props.widget.customProperties.on_value : this.props.widget.customProperties.off_value); } - this.props.widget.customProperties.pressed = nextState; - this.setState({ pressed: nextState }); - this.valueChanged(nextState ? this.props.widget.customProperties.on_value : this.props.widget.customProperties.off_value); } valueChanged(newValue) { if (this.props.onInputChanged) - this.props.onInputChanged(newValue); + this.props.onInputChanged(newValue, 'pressed'); } render() { return (
- +
); } diff --git a/src/widget/widgets/gauge.js b/src/widget/widgets/gauge.js index cd778ad..00ffa93 100644 --- a/src/widget/widgets/gauge.js +++ b/src/widget/widgets/gauge.js @@ -29,9 +29,10 @@ class WidgetGauge extends Component { this.state = { value: 0, unit: '', + signalID: '', minValue: null, maxValue: null, - useColorZones: false, + colorZones: [], useMinMax: false, useMinMaxChange: false, }; @@ -48,13 +49,7 @@ class WidgetGauge extends Component { } componentDidUpdate(prevProps: Readonly

, prevState: Readonly, snapshot: SS): void { - if(prevState.minValue !== this.state.minValue){ - this.gauge.setMinValue(this.state.minValue); - } - if(prevState.maxValue !== this.state.maxValue){ - this.gauge.maxValue = this.state.maxValue - } - + // update gauge's value if(prevState.value !== this.state.value){ this.gauge.set(this.state.value) @@ -65,8 +60,8 @@ class WidgetGauge extends Component { } // update labels - if(prevState.minValue !== this.state.minValue || prevState.maxValue !== this.state.maxValue || prevState.useColorZones !== this.state.useColorZones - || prevState.useMinMax !== this.state.useMinMax){ + if(prevState.minValue !== this.state.minValue || prevState.maxValue !== this.state.maxValue || prevState.colorZones !== this.state.colorZones + || prevState.useMinMax !== this.state.useMinMax || prevState.signalID !== this.state.signalID){ this.gauge = new Gauge(this.gaugeCanvas).setOptions(this.computeGaugeOptions(this.props.widget)); this.gauge.maxValue = this.state.maxValue; this.gauge.setMinValue(this.state.minValue); @@ -80,30 +75,35 @@ class WidgetGauge extends Component { static getDerivedStateFromProps(props, state){ if(props.widget.signalIDs.length === 0){ - return null; + return{ value: 0, minValue: 0, maxValue: 10}; } + + // get the signal with the selected signal ID + let signalID = props.widget.signalIDs[0]; + let signal = props.signals.filter(s => s.id === signalID) + // determine ID of infrastructure component related to signal[0] (there is only one signal for a lamp widget) + let icID = props.icIDs[signal[0].id]; + let returnState = {} - returnState["useColorZones"] = props.widget.customProperties.colorZones; + returnState["colorZones"] = props.widget.customProperties.zones; + if(signalID){ + returnState["signalID"] = signalID; + } // Update unit (assuming there is exactly one signal for this widget) - let signalID = props.widget.signalIDs[0]; - let widgetSignal = props.signals.find(sig => sig.id === signalID); - if(widgetSignal !== undefined){ - returnState["unit"] = widgetSignal.unit; + if(signal !== undefined){ + returnState["unit"] = signal[0].unit; } - const ICid = props.icIDs[0]; - // update value + + // check if data available if (props.data == null - || props.data[ICid] == null - || props.data[ICid].output == null - || props.data[ICid].output.values == null - || props.data[ICid].output.values.length === 0 - || props.data[ICid].output.values[0].length === 0) { - returnState["value"] = 0; - return returnState; + || props.data[icID] == null + || props.data[icID].output == null + || props.data[icID].output.values == null) { + return{ value: 0, minValue: 0, maxValue: 10}; } // memorize if min or max value is updated @@ -112,14 +112,14 @@ class WidgetGauge extends Component { let updateMaxValue = false; // check if value has changed - const signalData = props.data[ICid].output.values[widgetSignal.index]; + const data = props.data[icID].output.values[signal[0].index-1]; // Take just 3 decimal positions // Note: Favor this method over Number.toFixed(n) in order to avoid a type conversion, since it returns a String - if (signalData != null) { - const value = Math.round(signalData[signalData.length - 1].y * 1e3) / 1e3; + if (data != null) { + const value = Math.round(data[data.length - 1].y * 1e3) / 1e3; let minValue = null; let maxValue = null; - + if ((state.value !== value && value != null) || props.widget.customProperties.valueUseMinMax || state.useMinMaxChange) { //value has changed updateValue = true; @@ -129,14 +129,14 @@ class WidgetGauge extends Component { minValue = state.minValue; maxValue = state.maxValue; - - if (minValue == null || state.useMinMaxChange) { + + if (minValue == null || (!props.widget.customProperties.valueUseMinMax && (value < minValue || signalID !== state.signalID)) ||state.useMinMaxChange) { minValue = value - 0.5; updateLabels = true; updateMinValue = true; } - if (maxValue == null || state.useMinMaxChange) { + if (maxValue == null || (!props.widget.customProperties.valueUseMinMax && (value > maxValue || signalID !== state.signalID)) || state.useMinMaxChange) { maxValue = value + 0.5; updateLabels = true; updateMaxValue = true; @@ -144,17 +144,12 @@ class WidgetGauge extends Component { } if (props.widget.customProperties.valueUseMinMax) { - if (state.minValue > props.widget.customProperties.valueMin) { minValue = props.widget.customProperties.valueMin; updateMinValue = true; - updateLabels = true; - } - - if (state.maxValue < props.widget.customProperties.valueMax) { maxValue = props.widget.customProperties.valueMax; updateMaxValue = true; updateLabels = true; - } + } if (updateLabels === false && state.gauge) { @@ -174,10 +169,7 @@ class WidgetGauge extends Component { if(props.widget.customProperties.valueUseMinMax !== state.useMinMax){ returnState["useMinMax"] = props.widget.customProperties.valueUseMinMax; } - if(props.widget.customProperties.colorZones !== state.useColorZones){ - returnState["useColorZones"] = props.widget.customProperties.colorZones; - } - + // prepare returned state if(updateValue === true){ returnState["value"] = value; @@ -209,18 +201,19 @@ class WidgetGauge extends Component { for (let i = 0; i < labelCount; i++) { labels.push(minValue + labelStep * i); } - + // calculate zones let zones = this.props.widget.customProperties.colorZones ? this.props.widget.customProperties.zones : null; if (zones != null) { // adapt range 0-100 to actual min-max const step = (maxValue - minValue) / 100; - + zones = zones.map(zone => { return Object.assign({}, zone, { min: (zone.min * step) + +minValue, max: zone.max * step + +minValue, strokeStyle: '#' + zone.strokeStyle }); }); } + if(this.state.signalID !== ''){ this.gauge.setOptions({ staticLabels: { font: '10px "Helvetica Neue"', @@ -231,6 +224,7 @@ class WidgetGauge extends Component { staticZones: zones }); } + } computeGaugeOptions(widget) { return { @@ -245,8 +239,8 @@ class WidgetGauge extends Component { colorStop: '#6EA2B0', strokeColor: '#E0E0E0', highDpiSupport: true, - limitMax: false, - limitMin: false + limitMax: widget.customProperties.valueUseMinMax || false, + limitMin: widget.customProperties.valueUseMinMax || false }; } diff --git a/src/widget/widgets/input.js b/src/widget/widgets/input.js index 58273ad..7fe7c38 100644 --- a/src/widget/widgets/input.js +++ b/src/widget/widgets/input.js @@ -31,53 +31,47 @@ class WidgetInput extends Component { static getDerivedStateFromProps(props, state){ - let returnState = {}; + let value = '' + let unit = '' - if(props.widget.customProperties.value !== ''){ - returnState["value"] = props.widget.customProperties.value; - } - - if(props.widget.signalIDs.length === 0){ - if (props.widget.customProperties.default_value && state.value === undefined && props.widget.customProperties.value === '') { - returnState["value"] = props.widget.customProperties.default_value; - } else { // if no default available - if (returnState !== {}){ - return returnState; - } - else{ - return null; - } - } - } - - // Update value - if (props.widget.customProperties.default_value && this.state.value === undefined && props.widget.customProperties.value === '') { - returnState["value"] = props.widget.customProperties.default_value; + if(props.widget.customProperties.hasOwnProperty('value') && props.widget.customProperties.value !== state.value){ + // set value to customProperties.value if this property exists and the value is different from current state + value = Number(props.widget.customProperties.value); + } else if (props.widget.customProperties.hasOwnProperty('default_value') && state.value === ''){ + // if customProperties.default_value exists and value has been assigned yet, set the value to the default_value + value = Number(props.widget.customProperties.default_value) } // Update unit (assuming there is exactly one signal for this widget) let signalID = props.widget.signalIDs[0]; let signal = props.signals.find(sig => sig.id === signalID); if(signal !== undefined){ - returnState["unit"] = signal.unit; + unit = signal.unit; } - if (returnState !== {}){ - return returnState; - } - else{ - return null; + if (unit !== '' && value !== ''){ + // unit and value have changed + return {unit: unit, value: value}; + } else if (unit !== ''){ + // only unit has changed + return {unit: unit} + } else if (value !== ''){ + // only value has changed + return {value: value} + } else{ + // nothing has changed + return null } } valueIsChanging(newValue) { - this.setState({ value: newValue }); - this.props.widget.customProperties.value = newValue; + this.setState({ value: Number(newValue) }); + this.props.widget.customProperties.value = Number(newValue); } valueChanged(newValue) { if (this.props.onInputChanged) { - this.props.onInputChanged(newValue); + this.props.onInputChanged(Number(newValue), 'value'); } } @@ -97,7 +91,16 @@ class WidgetInput extends Component { - this.handleKeyPress(e) } onBlur={ (e) => this.valueChanged(this.state.value) } onChange={ (e) => this.valueIsChanging(e.target.value) } placeholder="Enter value" value={ this.state.value } /> + this.handleKeyPress(e) } + onBlur={ (e) => this.valueChanged(this.state.value) } + onChange={ (e) => this.valueIsChanging(e.target.value) } + placeholder="Enter value" + value={ this.state.value } + /> {this.props.widget.customProperties.showUnit? ( {this.state.unit} diff --git a/src/widget/widgets/lamp.js b/src/widget/widgets/lamp.js index 7a89c04..939caed 100644 --- a/src/widget/widgets/lamp.js +++ b/src/widget/widgets/lamp.js @@ -25,7 +25,6 @@ class WidgetLamp extends Component { this.state = { value: '', - threshold: 0 }; } @@ -34,28 +33,31 @@ class WidgetLamp extends Component { return{ value: ''}; } - const ic = props.icIDs[0]; + // get the signal with the selected signal ID let signalID = props.widget.signalIDs[0]; - let widgetSignal = props.signals.find(sig => sig.id === signalID); + let signal = props.signals.filter(s => s.id === signalID) + // determine ID of infrastructure component related to signal[0] (there is only one signal for a lamp widget) + let icID = props.icIDs[signal[0].id]; - // update value + // check if data available if (props.data == null - || props.data[ic] == null - || props.data[ic].output == null - || props.data[ic].output.values == null) { + || props.data[icID] == null + || props.data[icID].output == null + || props.data[icID].output.values == null) { return{value:''}; } // check if value has changed - const signalData = props.data[ic].output.values[widgetSignal.index]; - if (signalData != null && state.value !== signalData[signalData.length - 1].y) { - return { value: signalData[signalData.length - 1].y }; + const data = props.data[icID].output.values[signal[0].index-1]; + if (data != null && Number(state.value) !== data[data.length - 1].y) { + return { value: data[data.length - 1].y }; } return null; } render() { + let colors = EditWidgetColorControl.ColorPalette; let color; diff --git a/src/widget/widgets/plot-table.js b/src/widget/widgets/plot-table.js deleted file mode 100644 index 155131e..0000000 --- a/src/widget/widgets/plot-table.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * 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, { Component } from 'react'; -import { FormGroup } from 'react-bootstrap'; -import Plot from '../widget-plot/plot'; -import PlotLegend from '../widget-plot/plot-legend'; - -class WidgetPlotTable extends Component { - constructor(props) { - super(props); - this.state = { - signals: [], - data: [] - }; - } - - static getDerivedStateFromProps(props, state){ - let intersection = [] - let data = []; - let signalID, sig; - for (signalID of props.widget.signalIDs) { - for (sig of props.signals) { - if (signalID === sig.id) { - intersection.push(sig); - - // sig is a selected signal, get data - // determine ID of infrastructure component related to signal (via config) - let icID = props.icIDs[sig.id] - - // distinguish between input and output signals - if (sig.direction === "out") { - if (props.data[icID] != null && props.data[icID].output != null && props.data[icID].output.values != null) { - if (props.data[icID].output.values[sig.index-1] !== undefined) { - data.push(props.data[icID].output.values[sig.index-1]); - } - } - } else if (sig.direction === "in") { - if (props.data[icID] != null && props.data[icID].input != null && props.data[icID].input.values != null) { - if (props.data[icID].input.values[sig.index-1] !== undefined) { - data.push(props.data[icID].input.values[sig.index-1]); - } - } - } - } // sig is selected signal - } // loop over props.signals - } // loop over selected signals - - return {signals: intersection, data: data} - } - - // updateSignalSelection(signal, checked) { - // // Update the selected signals and propagate to parent component - // var new_widget = Object.assign({}, this.props.widget, { - // checkedSignals: checked ? this.state.signals.concat(signal) : this.state.signals.filter((idx) => idx !== signal) - // }); - // this.props.onWidgetChange(new_widget); - // } - - render() { - let checkBoxes = []; - - let showLegend = false; - if (this.state.signals.length > 0) { - - showLegend = true; - - // Create checkboxes using the signal indices from component config - // checkBoxes = this.state.signals.map((signal) => { - // let checked = this.state.signals.indexOf(signal) > -1; - // let chkBxClasses = classNames({ - // 'btn': true, - // 'btn-default': true, - // 'active': checked - // }); - // return this.updateSignalSelection(signal, e.target.checked)}> {signal.name} - // }); - } - - return ( -

-
-
-
- {checkBoxes.length > 0 ? ( - - {checkBoxes} - - ) : (Use edit menu to change selected signals.) - } -
- -
- -
-
- {showLegend ? ( - ) : (
) - } -
-
- ); - } -} -export default WidgetPlotTable; diff --git a/src/widget/widgets/slider.js b/src/widget/widgets/slider.js index ce76fc6..89f4da7 100644 --- a/src/widget/widgets/slider.js +++ b/src/widget/widgets/slider.js @@ -40,47 +40,38 @@ class WidgetSlider extends Component { } static getDerivedStateFromProps(props, state){ - let returnState = {}; - if(props.widget.customProperties.value !== ''){ - returnState["value"] = props.widget.customProperties.value; + let value = '' + let unit = '' + + if(props.widget.customProperties.hasOwnProperty('value') && props.widget.customProperties.value !== state.value){ + // set value to customProperties.value if this property exists and the value is different from current state + value = Number(props.widget.customProperties.value); + } else if (props.widget.customProperties.hasOwnProperty('default_value') && state.value === ''){ + // if customProperties.default_value exists and value has been assigned yet, set the value to the default_value + value = Number(props.widget.customProperties.default_value) } - if(props.widget.signalIDs.length === 0){ - - // set value to default - if (props.widget.customProperties.default_value && state.value === undefined && props.widget.customProperties.value === '') { - returnState["value"] = props.widget.customProperties.default_value; - } else { // if no default available - if (returnState !== {}){ - return returnState; - } - else{ - return null; - } - } - - } - - // Update value - if (props.widget.customProperties.default_value && state.value === undefined && props.widget.customProperties.value === '') { - returnState["value"] = props.widget.customProperties.default_value; - } - // Update unit (assuming there is exactly one signal for this widget) let signalID = props.widget.signalIDs[0]; let signal = props.signals.find(sig => sig.id === signalID); if(signal !== undefined){ - returnState["unit"] = signal.unit; + unit = signal.unit; } - if (returnState !== {}){ - return returnState; + if (unit !== '' && value !== ''){ + // unit and value have changed + return {unit: unit, value: value}; + } else if (unit !== ''){ + // only unit has changed + return {unit: unit} + } else if (value !== ''){ + // only value has changed + return {value: value} + } else { + // nothing has changed + return null } - else{ - return null; - } - } componentDidUpdate(prevProps: Readonly

, prevState: Readonly, snapshot: SS): void { @@ -114,7 +105,7 @@ class WidgetSlider extends Component { valueChanged(newValue) { if (this.props.onInputChanged) { - this.props.onInputChanged(newValue); + this.props.onInputChanged(newValue, 'value'); } } @@ -124,7 +115,7 @@ class WidgetSlider extends Component { let fields = { name: this.props.widget.name, control: this.valueIsChanging(v) } onAfterChange={ (v) => this.valueChanged(v) }/>, - value: { format('.3s')(Number.parseFloat(this.state.value)) }, + value: { format('.2f')(Number.parseFloat(this.state.value)) }, unit: { this.state.unit } } diff --git a/src/widget/widgets/table.js b/src/widget/widgets/table.js index 54383a8..089eec0 100644 --- a/src/widget/widgets/table.js +++ b/src/widget/widgets/table.js @@ -27,72 +27,55 @@ class WidgetTable extends Component { this.state = { rows: [], - sequence: null, - showUnit: false }; } - static getDerivedStateFromProps(props, state){ - if(props.widget.signalIDs.length === 0){ - return{ - rows: [], - sequence: null, - }; - } + let rows = []; + let signalID, sig; + for (signalID of props.widget.signalIDs) { + for (sig of props.signals) { + if (signalID === sig.id) { + // sig is a selected signal, get data + // determine ID of infrastructure component related to signal (via config) + let icID = props.icIDs[sig.id] + // distinguish between input and output signals + if (sig.direction === "out") { + if (props.data[icID] != null && props.data[icID].output != null && props.data[icID].output.values != null) { + if (props.data[icID].output.values[sig.index-1] !== undefined) { + let data = props.data[icID].output.values[sig.index-1]; + rows.push({ + name: sig.name, + unit: sig.unit, + value: data[data.length - 1].y + }); - const ICid = props.icIDs[0]; - let widgetSignals = props.signals.find(sig => { - for (let id of props.widget.signalIDs){ - if (id === sig.id){ - return true; - } - } - return false; - }); + } + } + } else if (sig.direction === "in") { + if (props.data[icID] != null && props.data[icID].input != null && props.data[icID].input.values != null) { + if (props.data[icID].input.values[sig.index-1] !== undefined) { + let data = props.data[icID].input.values[sig.index-1]; + rows.push({ + name: sig.name, + unit: sig.unit, + value: data[data.length - 1].y + }); + } + } + } + } // sig is selected signal + } // loop over props.signals + } // loop over selected signals - // check data - if (props.data == null - || props.data[ICid] == null - || props.data[ICid].output == null - || props.data[ICid].output.values.length === 0 - || props.data[ICid].output.values[0].length === 0) { + return {rows: rows} - // clear values - return{ - rows: [], - sequence: null, - showUnit: false, - }; - } - - // get rows - const rows = []; - - props.data[ICid].output.values.forEach((signal, index) => { - let s = widgetSignals.find( sig => sig.index === index); - // if the signal is used by the widget - if (s !== undefined) { - // push data of the signal - rows.push({ - name: s.name, - unit: s.unit, - value: signal[signal.length - 1].y - }); - } - }); - - return { - showUnit: props.showUnit, - rows: rows, - sequence: props.data[ICid].output.sequence - }; } render() { - + let rows = this.state.rows; if(rows.length === 0){ diff --git a/src/widget/widgets/value.js b/src/widget/widgets/value.js index f143cd0..0468b3e 100644 --- a/src/widget/widgets/value.js +++ b/src/widget/widgets/value.js @@ -33,31 +33,28 @@ class WidgetValue extends Component { return null; } - // TODO does the following line make sense? - const ICid = props.icIDs[0]; + // get the signal with the selected signal ID let signalID = props.widget.signalIDs[0]; - let signal = props.signals.find(sig => sig.id === signalID); + let signal = props.signals.filter(s => s.id === signalID) + // determine ID of infrastructure component related to signal[0] (there is only one signal for a value widget) + let icID = props.icIDs[signal[0].id]; - - // update value - let value = ''; - if (props.data == null - || props.data[ICid] == null - || props.data[ICid].output == null - || props.data[ICid].output.values == null) { + // check if data available + let value = '' + if (props.data == null || props.data[icID] == null || props.data[icID].output == null || props.data[icID].output.values == null) { value = ''; } else { // check if value has changed - const signalData = props.data[ICid].output.values[signal.index]; - if (signalData != null && state.value !== signalData[signalData.length - 1].y) { - value = signalData[signalData.length - 1].y + const data = props.data[icID].output.values[signal[0].index - 1]; + if (data != null && Number(state.value) !== data[data.length - 1].y) { + value = data[data.length - 1].y; } } // Update unit (assuming there is exactly one signal for this widget) let unit = ''; if(signal !== undefined){ - unit = signal.unit; + unit = signal[0].unit; } return { @@ -77,7 +74,7 @@ class WidgetValue extends Component { {Number.isNaN(value_to_render) ? NaN : format('.3s')(value_to_render)} {this.props.widget.customProperties.showUnit && [{this.state.unit}] - } + }

); }