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

merge develop into feature_addUser (conflicts in scenario.js)

This commit is contained in:
Sonja Happ 2020-06-23 15:45:48 +02:00
commit 3bd2a9eaa3
39 changed files with 906 additions and 689 deletions

16
doc/Requirements.md Normal file
View file

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

79
doc/Structure.md Normal file
View file

@ -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.

69
doc/development.md Normal file
View file

@ -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.

View file

@ -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] } },

View file

@ -56,7 +56,7 @@ class Dialog extends React.Component {
</Modal.Body>
<Modal.Footer>
<Button onClick={this.cancelModal}>Cancel</Button>
{this.props.blendOutCancel? <div></div>: <Button onClick={this.cancelModal}>Cancel</Button>}
<Button onClick={this.closeModal} disabled={!this.props.valid}>{this.props.buttonTitle}</Button>
</Modal.Footer>
</Modal>

View file

@ -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(<span>
&nbsp;
<FormLabel column={false} classes={child.props.labelStyle(data[labelKey], data)}>
{labelContent.toString()}
<FormLabel column={false} className={labelStyle}>
{labelContent}
</FormLabel>
</span>
);
}
if (child.props.dataIndex) {
cell.push(index);
}

View file

@ -65,11 +65,18 @@ class DashboardButtonGroup extends React.Component {
);
}
buttons.push(
<Button key={key++} onClick={this.props.onEditFiles} style={buttonStyle}>
<Icon icon="file" /> Edit Files
</Button>
);
buttons.push(
<Button key={key++} onClick={this.props.onEdit} style={buttonStyle}>
<Icon icon="pen" /> Edit Layout
</Button>
);
}
return <div className='section-buttons-group-right'>

View file

@ -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)}
/>
</div>
@ -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}
/>
<EditFiles
sessionToken={this.state.sessionToken}
show={this.state.filesEditModal}
onClose={this.closeEditFiles.bind(this)}
signals={this.state.signals}
files={this.state.files}
scenarioID={this.state.dashboard.scenarioID}
/>
</div>
</div>;

166
src/file/edit-files.js Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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(
<option key = {0} default>Select image file</option>
)
fileOptions.push(this.props.files.map((file, index) => (
<option key={index+1} value={file.id}>{file.name}</option>
)))
} else {
fileOptions = <option disabled value style={{ display: 'none' }}>No files found, please upload one first.</option>
}
const progressBarStyle = {
marginLeft: '100px',
marginTop: '-40px'
};
return (
<Dialog show={this.props.show} title="Edit Files of scenario" buttonTitle="Close" onClose={(c) => this.onClose(c)} blendOutCancel = {true} valid={true}>
<div>
<div className="edit-table">
<Table data={this.props.files} width = {467}>
<TableColumn title='ID' dataKey='id' width={42} />
<TableColumn title='Name' dataKey='name' width={107}/>
<TableColumn title='Size (bytes)' dataKey='size' width={83.3}/>
<TableColumn title='Type' dataKey='type' width={159.7}/>
<TableColumn
title='Delete'
width='75'
deleteButton
onDelete={(index) => this.deleteFile(index)}
/>
</Table>
</div>
<FormGroup as={Col} >
<FormControl
disabled={this.props.disabled}
type='file'
onChange={(event) => this.selectUploadFile(event)} />
</FormGroup>
<FormGroup as={Col} >
<Button
disabled={this.state.uploadFile === null}
onClick={() => this.startFileUpload()}>
Upload
</Button>
</FormGroup>
<FormGroup as={Col} >
<ProgressBar
striped={true}
animated={true}
now={this.state.uploadProgress}
label={this.state.uploadProgress + '%'}
style={progressBarStyle}
/>
</FormGroup>
</div>
</Dialog>
);
}
}
export default EditFilesDialog;

View file

@ -55,11 +55,12 @@ class ICAction extends React.Component {
));
return <div>
Send command to infrastructure component
<ButtonToolbar>
<DropdownButton title={this.state.selectedAction != null ? this.state.selectedAction.title : ''} id="action-dropdown" onSelect={this.setAction}>
{actionList}
</DropdownButton>
<Button style={{ marginLeft: '5px' }} disabled={this.props.runDisabled} onClick={() => this.props.runAction(this.state.selectedAction)}>Run</Button>
<Button style={{ marginLeft: '5px' }} disabled={this.props.runDisabled} onClick={() => this.props.runAction(this.state.selectedAction)}>Send command</Button>
</ButtonToolbar>
</div>;

View file

@ -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);

View file

@ -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);

View file

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

View file

@ -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 {
<Table data={this.state.ics}>
<TableColumn checkbox onChecked={(index, event) => this.onICChecked(index, event)} width='30' />
<TableColumn title='Name' dataKeys={['name', 'rawProperties.name']} />
<TableColumn title='State' labelKey='state' tooltipKey='error' labelModifier={InfrastructureComponents.stateLabelModifier} labelStyle={InfrastructureComponents.stateLabelStyle} />
<TableColumn title='State' labelKey='state' tooltipKey='error' labelStyle={(state, component) => this.stateLabelStyle(state, component)} />
<TableColumn title='Category' dataKeys={['category', 'rawProperties.category']} />
<TableColumn title='Type' dataKeys={['type', 'rawProperties.type']} />
<TableColumn title='Location' dataKeys={['properties.location', 'rawProperties.location']} />

View file

@ -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 <div className='section'>
<h1>{this.state.scenario.name}</h1>
{/*Scenario Users table*/}
<h2>Users</h2>
<h2 style={tableHeadingStyle}>Users</h2>
<div>
<Table data={this.state.scenario.users}>
<TableColumn title='Name' dataKey='username' link='/users/' linkKey='id' />
@ -464,7 +437,7 @@ class Scenario extends React.Component {
{/*Component Configurations table*/}
<h2>Component Configurations</h2>
<h2 style={tableHeadingStyle}>Component Configurations</h2>
<Table data={this.state.configs}>
<TableColumn checkbox onChecked={(index, event) => this.onConfigChecked(index, event)} width='30' />
<TableColumn title='Name' dataKey='name' />
@ -497,8 +470,9 @@ class Scenario extends React.Component {
<div style={{ float: 'left' }}>
<ICAction
runDisabled={this.state.selectedConfigs.length === 0}
runAction={this.runAction}
runAction={(action) => 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 {
<EditSignalMapping
show={this.state.editOutputSignalsModal}
onCloseEdit={(data, direction) => 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}
/>
<EditSignalMapping
show={this.state.editInputSignalsModal}
onCloseEdit={(data, direction) => 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*/}
<h2>Dashboards</h2>
<h2 style={tableHeadingStyle}>Dashboards</h2>
<Table data={this.state.dashboards}>
<TableColumn title='Name' dataKey='name' link='/dashboards/' linkKey='id' />
<TableColumn title='Grid' dataKey='grid' />

View file

@ -214,6 +214,15 @@ class Scenarios extends Component {
FileSaver.saveAs(blob, 'scenario - ' + scenario.name + '.json');
}
modifyRunningColumn(running){
if(running){
return <Icon icon='check' />
} else {
return <Icon icon='times' />
}
}
render() {
const buttonStyle = {
@ -227,7 +236,7 @@ class Scenarios extends Component {
<Table data={this.state.scenarios}>
<TableColumn title='Name' dataKey='name' link='/scenarios/' linkKey='id' />
<TableColumn title='ID' dataKey='id' />
<TableColumn title='Running' dataKey='running' />
<TableColumn title='Running' dataKey='running' modifier={(running) => this.modifyRunningColumn(running)}/>
<TableColumn
width='200'
editButton

View file

@ -18,35 +18,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button, FormGroup, FormLabel, FormText} from 'react-bootstrap';
import Table from '../common/table';
import TableColumn from '../common/table-column';
import Dialog from "../common/dialogs/dialog";
import SignalStore from "./signal-store"
import Icon from "../common/icon";
import AppDispatcher from "../common/app-dispatcher";
class EditSignalMapping extends React.Component {
valid = false;
static getStores() {
return [ SignalStore];
}
constructor(props) {
super(props);
super(props);
let dir = "";
if ( this.props.direction === "Output"){
dir = "out";
} else if ( this.props.direction === "Input" ){
dir = "in";
}
let dir = "";
if ( this.props.direction === "Output"){
dir = "out";
} else if ( this.props.direction === "Input" ){
dir = "in";
}
this.state = {
dir,
signals: [],
};
this.state = {
dir,
signals: []
};
}
static getDerivedStateFromProps(props, state){
@ -63,55 +57,51 @@ class EditSignalMapping extends React.Component {
onClose(canceled) {
if (canceled === false) {
if (this.valid) {
let data = this.state.signals;
//forward modified signals back to callback function
this.props.onCloseEdit(data, this.state.dir)
}
} else {
this.props.onCloseEdit(null, this.state.dir);
}
}
onDelete(e){
console.log("On signal delete");
this.props.onCloseEdit(this.state.dir)
}
handleMappingChange = (event, row, column) => {
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(
<Dialog show={this.props.show} title="Edit Signal Mapping" buttonTitle="Save Edits" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<Dialog
show={this.props.show}
title="Edit Signal Mapping"
buttonTitle="Close"
blendOutCancel = {true}
onClose={(c) => this.onClose(c)}
onReset={() => this.resetState()}
valid={true}>
<FormGroup>
<FormLabel>{this.props.direction} Mapping</FormLabel>

View file

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

View file

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

View file

@ -122,6 +122,16 @@ class Users extends Component {
}
};
modifyActiveColumn(active){
if(active){
return <Icon icon='check' />
} else {
return <Icon icon='times' />
}
}
render() {
return (
@ -132,8 +142,8 @@ class Users extends Component {
<TableColumn title='Username' width='150' dataKey='username' />
<TableColumn title='ID' width='150' dataKey='id' />
<TableColumn title='E-mail' dataKey='mail' />
<TableColumn title='Role' dataKey='role' modifier={(role) => this.getHumanRoleName(role)} />
<TableColumn title='Active' dataKey='active' />
<TableColumn title='Role' dataKey='role' modifier={(role) => this.getHumanRoleName(role)} />
<TableColumn title='Active' dataKey='active' modifier={(active) => this.modifyActiveColumn(active)} />
<TableColumn width='200' editButton deleteButton onEdit={index => this.setState({ editModal: true, modalData: this.state.users[index] })} onDelete={index => this.setState({ deleteModal: true, modalData: this.state.users[index] })} />
</Table>

View file

@ -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 (<FormCheck type='radio' key={idx} name={this.props.label} style={colorStyle} className={checkedClass} value={idx} inline onChange={(e) => this.props.handleChange({target: { id: this.props.controlId, value: idx}})} />)
return (<FormCheck
type='radio'
key={idx}
name={this.props.label}
style={colorStyle}
className={checkedClass}
value={idx}
inline
defaultChecked={isCustomProperty ? this.state.widget[parts[0]][parts[1]] ===idx: this.state.widget[this.props.controlId] === idx}
onChange={(e) => this.props.handleChange({target: { id: this.props.controlId, value: idx}})} />)
}
)
}

View file

@ -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(
<EditWidgetSignalControl key={0} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalControl key={0} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} direction={'in'} />,
);
break;
case 'Value':
DialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Signal name'} placeholder={'Enter text'} handleChange={e => handleChange(e)} />,
<EditWidgetSignalControl key={1} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalControl key={1} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} direction={'out'}/>,
<EditWidgetTextSizeControl key={2} widget={widget} handleChange={e => handleChange(e)} />,
<EditWidgetCheckboxControl key={3} widget={widget} controlId={'customProperties.showUnit'} input text="Show unit" handleChange={e => handleChange(e)} />
);
break;
case 'Lamp':
DialogControls.push(
<EditWidgetSignalControl key={0} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalControl key={0} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} direction={'out'}/>,
<EditWidgetTextControl key={1} widget={widget} controlId={'customProperties.threshold'} label={'Threshold'} placeholder={'0.5'} handleChange={e => handleChange(e)} />,
<EditWidgetColorControl key={2} widget={widget} controlId={'customProperties.on_color'} label={'Color On'} handleChange={(e) => handleChange(e)} />,
<EditWidgetColorControl key={3} widget={widget} controlId={'customProperties.off_color'} label={'Color Off'} handleChange={(e) => handleChange(e)} />,
@ -69,14 +69,14 @@ export default function CreateControls(widgetType = null, widget = null, session
case 'Plot':
DialogControls.push(
<EditWidgetTimeControl key={0} widget={widget} controlId={'customProperties.time'} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalsControl key={1} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalsControl key={1} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} direction={'out'}/>,
<EditWidgetTextControl key={2} widget={widget} controlId={'customProperties.ylabel'} label={'Y-Axis name'} placeholder={'Enter a name for the y-axis'} handleChange={(e) => handleChange(e)} />,
<EditWidgetMinMaxControl key={3} widget={widget} controlId="customProperties.y" handleChange={e => handleChange(e)} />
);
break;
case 'Table':
DialogControls.push(
<EditWidgetSignalsControl key={0} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalsControl key={0} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} direction={'out'}/>,
<EditWidgetCheckboxControl key={1} widget={widget} controlId={'customProperties.showUnit'} input text="Show unit" handleChange={e => 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(
<EditFileWidgetControl key={0} widget={widget} controlId={"customProperties.file"} files={files} type={'image'} handleChange={(e) => handleChange(e)} onUpload={(f,i) => onUpload(f,i)} />,
<EditFileWidgetControl key={0} widget={widget} controlId={"customProperties.file"} files={files} type={'image'} handleChange={(e) => handleChange(e)} />,
<EditWidgetAspectControl key={1} widget={widget} controlId={"customProperties.lockAspect"} handleChange={e => handleChange(e)} />
);
break;
case 'Gauge':
DialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} handleChange={e => handleChange(e)} />,
<EditWidgetSignalControl key={1} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalControl key={1} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} direction={'out'}/>,
<EditWidgetCheckboxControl key={2} widget={widget} controlId="customProperties.colorZones" input text="Show color zones" handleChange={e => handleChange(e)} />,
<EditWidgetColorZonesControl key={3} widget={widget} handleChange={e => handleChange(e)} />,
<EditWidgetMinMaxControl key={4} widget={widget} controlId="customProperties.value" handleChange={e => handleChange(e)} />
);
break;
case 'PlotTable':
DialogControls.push(
<EditWidgetSignalsControl key={0} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} />,
<EditWidgetTextControl key={1} widget={widget} controlId={'customProperties.ylabel'} label={'Y-Axis'} placeholder={'Enter a name for the Y-axis'} handleChange={(e) => handleChange(e)} />,
<EditWidgetTimeControl key={2} widget={widget} controlId={'customProperties.time'} handleChange={(e) => handleChange(e)} />,
<EditWidgetMinMaxControl key={3} widget={widget} controlId="customProperties.y" handleChange={e => handleChange(e)} />
);
break;
case 'Slider':
DialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} handleChange={e => handleChange(e)} />,
<EditWidgetOrientation key={1} widget={widget} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalControl key={2} widget={widget} controlId={'signalIDs'} input signals={signals} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalControl key={2} widget={widget} controlId={'signalIDs'} input signals={signals} handleChange={(e) => handleChange(e)} direction={'in'}/>,
<EditWidgetCheckboxControl key={3} widget={widget} controlId={'customProperties.continous_update'} input text={'Continous Update'} handleChange={(e) => handleChange(e)} />,
<EditWidgetCheckboxControl key={4} widget={widget} controlId={'customProperties.showUnit'} input text="Show unit" handleChange={e => handleChange(e)} />,
<EditWidgetMinMaxControl key={5} widget={widget} controlId={'customProperties.range'} handleChange={e => handleChange(e)} />,
@ -120,7 +112,7 @@ export default function CreateControls(widgetType = null, widget = null, session
case 'Button':
DialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} handleChange={e => handleChange(e)} />,
<EditWidgetSignalControl key={1} widget={widget} controlId={'signalIDs'} input signals={signals} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalControl key={1} widget={widget} controlId={'signalIDs'} input signals={signals} handleChange={(e) => handleChange(e)} direction={'in'}/>,
<EditWidgetCheckboxControl key={2} widget={widget} controlId={'customProperties.toggle'} input text="Toggle" handleChange={e => handleChange(e)} />,
<EditWidgetNumberControl key={3} widget={widget} controlId={'customProperties.on_value'} label={'On Value'} defaultValue={1} handleChange={(e) => handleChange(e)} />,
<EditWidgetNumberControl key={4} widget={widget} controlId={'customProperties.off_value'} label={'Off Value'} defaultValue={0} handleChange={(e) => handleChange(e)} />
@ -130,7 +122,7 @@ export default function CreateControls(widgetType = null, widget = null, session
DialogControls.push(
<EditWidgetColorControl key={0} widget={widget} controlId={'customProperties.border_color'} label={'Border color'} handleChange={(e) => handleChange(e)} />,
<EditWidgetColorControl key={1} widget={widget} controlId={'customProperties.background_color'} label={'Background color'} handleChange={e => handleChange(e)} />,
<EditWidgetNumberControl key={2} widget={widget} controlId={'customProperties.background_color_opacity'} label={'Background opacity'} defaultValue={0.5} handleChange={(e) => handleChange(e)} />
<EditWidgetNumberControl key={2} widget={widget} controlId={'customProperties.background_color_opacity'} label={'Background opacity (0.0 - 1.0)'} defaultValue={0.5} 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(
<EditFileWidgetControl key={0} widget={widget} controlId={"customProperties.file"} files={files} type={'xml'} handleChange={(e) => handleChange(e) } onUpload={(f,i) => onUpload(f,i)} />
<EditFileWidgetControl key={0} widget={widget} controlId={"customProperties.file"} files={files} type={'xml'} handleChange={(e) => handleChange(e) } />
);
break;
case 'NumberInput':
DialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} handleChange={e => handleChange(e)} />,
<EditWidgetSignalControl key={2} widget={widget} controlId={'signalIDs'} input signals={signals} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalControl key={2} widget={widget} controlId={'signalIDs'} input signals={signals} handleChange={(e) => handleChange(e)} direction={'in'}/>,
<EditWidgetCheckboxControl key={1} widget={widget} controlId={'customProperties.showUnit'} input text="Show unit" handleChange={e => handleChange(e)} />
);
break;

View file

@ -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 {
<FormLabel>Image</FormLabel>
<FormControl
as="select"
value={isCustomProperty ? this.state.widget[parts[0]][parts[1]] : this.state.widget[this.props.controlId]}
value={isCustomProperty ? this.props.widget[parts[0]][parts[1]] : this.props.widget[this.props.controlId]}
onChange={(e) => this.handleFileChange(e)}>{fileOptions} </FormControl>
</FormGroup>
<FormGroup controlId="upload">
<FormLabel>Upload</FormLabel>
<FormControl type="file" onChange={(e) => this.setState({ fileList: e.target.files }) } />
</FormGroup>
<ProgressBar striped active={'true'} now={this.state.progress} label={`${this.state.progress}%`} />
<Button size='sm' onClick={this.startFileUpload}>Upload</Button>
</div>;
}
}

View file

@ -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 (
<FormGroup controlId={this.props.controlId}>
<FormLabel>{this.props.label}</FormLabel>
<FormControl type="number" step={step} value={this.state.widget[this.props.controlId]} onChange={e => this.props.handleChange(e)} />
<FormControl
type="number"
step={step}
value={isCustomProperty ? this.state.widget[parts[0]][parts[1]] : this.state.widget[this.props.controlId]}
onChange={e => this.props.handleChange(e)} />
</FormGroup>
);
}

View file

@ -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 {
<FormControl as="select" value={this.props.widget.signalIDs[0] || ""} onChange={(e) => this.handleSignalChange(e)}>
<option default>Select signal</option>
{
this.props.signals.length === 0 ? (
this.state.signals.length === 0 ? (
<option disabled value style={{ display: 'none' }}>No signals available.</option>
) : (
this.props.signals.map((signal, index) => (
this.state.signals.map((signal, index) => (
<option key={index} value={signal.id}>{signal.name}</option>
))
)

View file

@ -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 {
<FormGroup>
<FormLabel>Signals</FormLabel>
{
this.props.signals === 0 || !this.state.widget.hasOwnProperty(this.props.controlId)? (
this.state.signals === 0 || !this.state.widget.hasOwnProperty(this.props.controlId)? (
<FormLabel>No signals available</FormLabel>
) : (
this.props.signals.map((signal, index) => (
this.state.signals.map((signal, index) => (
<FormCheck
type={'checkbox'}
label={signal.name}
@ -61,7 +71,7 @@ class EditWidgetSignalsControl extends Component {
onChange={(e) => this.handleSignalChange(e.target.checked, signal.id)}>
</FormCheck>
))
)
)
}
</FormGroup>
);

View file

@ -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 (

View file

@ -51,7 +51,7 @@ class ToolboxItem extends React.Component {
if (this.props.disabled === false) {
return this.props.connectDragSource(
<div className={itemClass}>
<span className="btn btn-default">
<span className="btn btn-info ">
{this.props.icon && <Icon style={{marginRight: '5px'}} icon={this.props.icon} /> }
{this.props.name}
</span>
@ -61,7 +61,7 @@ class ToolboxItem extends React.Component {
else {
return (
<div className={itemClass}>
<span className="btn btn-default">
<span className="btn btn-info">
{this.props.icon && <Icon style={{marginRight: '5px'}} icon={this.props.icon} /> }
{this.props.name}
</span>

View file

@ -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;

View file

@ -39,20 +39,19 @@ class WidgetToolbox extends React.Component {
const topologyItemMsg = thereIsTopologyWidget? 'Currently only one is supported' : '';
return <div className='toolbox box-header'>
<ToolboxItem name='Lamp' type='widget' />
<ToolboxItem name='Value' type='widget' />
<ToolboxItem name='Plot' type='widget' />
<ToolboxItem name='Table' type='widget' />
<ToolboxItem name='Label' type='widget' />
<ToolboxItem name='Image' type='widget' />
<ToolboxItem name='PlotTable' type='widget' />
<ToolboxItem name='Button' type='widget' />
<ToolboxItem name='NumberInput' type='widget' />
<ToolboxItem name='Slider' type='widget' />
<ToolboxItem name='Gauge' type='widget' />
<ToolboxItem name='Box' type='widget' />
<ToolboxItem name='HTML' type='html' />
<ToolboxItem name='Topology' type='widget' disabled={thereIsTopologyWidget} title={topologyItemMsg}/>
<ToolboxItem name='Lamp' type='widget' icon = 'plus' />
<ToolboxItem name='Value' type='widget' icon = 'plus' />
<ToolboxItem name='Plot' type='widget' icon = 'plus'/>
<ToolboxItem name='Table' type='widget' icon = 'plus'/>
<ToolboxItem name='Label' type='widget' icon = 'plus'/>
<ToolboxItem name='Image' type='widget' icon = 'plus'/>
<ToolboxItem name='Button' type='widget' icon = 'plus'/>
<ToolboxItem name='NumberInput' type='widget' icon = 'plus'/>
<ToolboxItem name='Slider' type='widget' icon = 'plus'/>
<ToolboxItem name='Gauge' type='widget' icon = 'plus'/>
<ToolboxItem name='Box' type='widget' icon = 'plus'/>
<ToolboxItem name='HTML' type='html' icon = 'plus'/>
<ToolboxItem name='Topology' type='widget' disabled={thereIsTopologyWidget} title={topologyItemMsg} icon = 'plus'/>
<div className='section-buttons-group-right'>
<div>

View file

@ -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 <WidgetCustomAction widget={widget} data={this.state.icData} dummy={this.state.sequence} signals={this.state.signals} icIDs={this.state.icIDs} />
return <WidgetCustomAction
widget={widget}
data={this.state.icData}
signals={this.state.signals}
icIDs={this.state.icIDs}
/>
} else if (widget.type === 'Action') {
return <WidgetAction widget={widget} data={this.state.icData} dummy={this.state.sequence} />
return <WidgetAction
widget={widget}
data={this.state.icData}
/>
} else if (widget.type === 'Lamp') {
return <WidgetLamp widget={widget} data={this.state.icData} dummy={this.state.sequence} signals={this.state.signals} icIDs={this.state.icIDs} />
return <WidgetLamp
widget={widget}
data={this.state.icData}
signals={this.state.signals}
icIDs={this.state.icIDs}
/>
} else if (widget.type === 'Value') {
return <WidgetValue widget={widget} data={this.state.icData} dummy={this.state.sequence} signals={this.state.signals} icIDs={this.state.icIDs} />
return <WidgetValue
widget={widget}
data={this.state.icData}
signals={this.state.signals}
icIDs={this.state.icIDs}
/>
} else if (widget.type === 'Plot') {
return <WidgetPlot widget={widget} data={this.state.icData} dummy={this.state.sequence} signals={this.state.signals} icIDs={this.state.icIDs} paused={this.props.paused} />
return <WidgetPlot
widget={widget}
data={this.state.icData}
signals={this.state.signals}
icIDs={this.state.icIDs}
paused={this.props.paused}
/>
} else if (widget.type === 'Table') {
return <WidgetTable widget={widget} data={this.state.icData} dummy={this.state.sequence} signals={this.state.signals} icIDs={this.state.icIDs} />
return <WidgetTable
widget={widget}
data={this.state.icData}
signals={this.state.signals}
icIDs={this.state.icIDs}
/>
} else if (widget.type === 'Label') {
return <WidgetLabel widget={widget} />
} else if (widget.type === 'PlotTable') {
return <WidgetPlotTable widget={widget} data={this.state.icData} dummy={this.state.sequence} signals={this.state.signals} icIDs={this.state.icIDs} editing={this.props.editing} onWidgetChange={(w) => this.props.onWidgetStatusChange(w, this.props.index)} paused={this.props.paused} />
return <WidgetLabel
widget={widget}
/>
} else if (widget.type === 'Image') {
return <WidgetImage widget={widget} files={this.state.files} token={this.state.sessionToken} />
return <WidgetImage
widget={widget}
files={this.state.files}
token={this.state.sessionToken}
/>
} else if (widget.type === 'Button') {
return <WidgetButton widget={widget} editing={this.props.editing} onInputChanged={(value) => this.inputDataChanged(widget, value)} signals={this.state.signals} />
return <WidgetButton
widget={widget}
editing={this.props.editing}
onInputChanged={(value, controlID) => this.inputDataChanged(widget, value, controlID)}
signals={this.state.signals}
/>
} else if (widget.type === 'NumberInput') {
return <WidgetInput widget={widget} editing={this.props.editing} onInputChanged={(value) => this.inputDataChanged(widget, value)} signals={this.state.signals} />
return <WidgetInput
widget={widget}
editing={this.props.editing}
onInputChanged={(value, controlID) => this.inputDataChanged(widget, value, controlID)}
signals={this.state.signals}
/>
} else if (widget.type === 'Slider') {
return <WidgetSlider widget={widget} editing={this.props.editing} onWidgetChange={(w) => this.props.onWidgetStatusChange(w, this.props.index) } onInputChanged={value => this.inputDataChanged(widget, value)} signals={this.state.signals}/>
return <WidgetSlider
widget={widget}
editing={this.props.editing}
onWidgetChange={(w) => 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 <WidgetGauge widget={widget} data={this.state.icData} editing={this.props.editing} signals={this.state.signals} icIDs={this.state.icIDs} />
return <WidgetGauge
widget={widget}
data={this.state.icData}
editing={this.props.editing}
signals={this.state.signals}
icIDs={this.state.icIDs}
/>
} else if (widget.type === 'Box') {
return <WidgetBox widget={widget} editing={this.props.editing} />
return <WidgetBox
widget={widget}
editing={this.props.editing}
/>
} else if (widget.type === 'HTML') {
return <WidgetHTML widget={widget} editing={this.props.editing} />
return <WidgetHTML
widget={widget}
editing={this.props.editing}
/>
} else if (widget.type === 'Topology') {
return <WidgetTopology widget={widget} files={this.state.files} token={this.state.sessionToken} />
return <WidgetTopology
widget={widget}
files={this.state.files}
token={this.state.sessionToken}
/>
}
return null;

View file

@ -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 (
<div className="button-widget full">
<Button className="full" active={ this.state.pressed } disabled={ this.props.editing } onMouseDown={ (e) => this.onPress(e) } onMouseUp={ (e) => this.onRelease(e) }>{this.props.widget.name}</Button>
<Button
className="full"
active={ this.state.pressed }
disabled={ this.props.editing }
onMouseDown={ (e) => this.onPress(e) }
onMouseUp={ (e) => this.onRelease(e) }>
{this.props.widget.name}
</Button>
</div>
);
}

View file

@ -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<P>, prevState: Readonly<S>, 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
};
}

View file

@ -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 {
</Col>
<Col>
<InputGroup>
<FormControl type="number" step="any" disabled={ this.props.editing } onKeyPress={ (e) => this.handleKeyPress(e) } onBlur={ (e) => this.valueChanged(this.state.value) } onChange={ (e) => this.valueIsChanging(e.target.value) } placeholder="Enter value" value={ this.state.value } />
<FormControl
type="number"
step="any"
disabled={ this.props.editing }
onKeyPress={ (e) => 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? (
<InputGroup.Append>
<InputGroup.Text>{this.state.unit}</InputGroup.Text>

View file

@ -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;

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 <FormCheck key={signal.index} className={chkBxClasses} checked={checked} disabled={this.props.editing}
// onChange={(e) => this.updateSignalSelection(signal, e.target.checked)}> {signal.name} </FormCheck>
// });
}
return (
<div className="plot-table-widget" ref="wrapper">
<div className="content">
<div className="table-plot-row">
<div className="widget-table">
{checkBoxes.length > 0 ? (
<FormGroup className="btn-group-vertical">
{checkBoxes}
</FormGroup>
) : (<small>Use edit menu to change selected signals.</small>)
}
</div>
<div className="widget-plot">
<Plot
data={this.state.data}
time={this.props.widget.customProperties.time}
width={this.props.widget.width - 100}
height={this.props.widget.height - 55}
yMin={this.props.widget.customProperties.yMin}
yMax={this.props.widget.customProperties.yMax}
yUseMinMax={this.props.widget.customProperties.yUseMinMax}
paused={this.props.paused}
yLabel={this.props.widget.customProperties.ylabel}
/>
</div>
</div>
{showLegend ? (
<PlotLegend signals={this.state.signals}/>) : (<div></div>)
}
</div>
</div>
);
}
}
export default WidgetPlotTable;

View file

@ -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<P>, prevState: Readonly<S>, 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: <Slider min={ this.props.widget.customProperties.rangeMin } max={ this.props.widget.customProperties.rangeMax } step={ this.props.widget.customProperties.step } value={ this.state.value } disabled={ this.props.editing } vertical={ isVertical } onChange={ (v) => this.valueIsChanging(v) } onAfterChange={ (v) => this.valueChanged(v) }/>,
value: <span>{ format('.3s')(Number.parseFloat(this.state.value)) }</span>,
value: <span>{ format('.2f')(Number.parseFloat(this.state.value)) }</span>,
unit: <span className="signal-unit">{ this.state.unit }</span>
}

View file

@ -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){

View file

@ -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 {
<span style={{ fontSize: this.props.widget.customProperties.textSize + 'px', flex: 'none', width: value_width }}>{Number.isNaN(value_to_render) ? NaN : format('.3s')(value_to_render)}</span>
{this.props.widget.customProperties.showUnit &&
<span style={{ fontSize: this.props.widget.customProperties.textSize + 'px', flex: 'none', width: unit_width}}>[{this.state.unit}]</span>
}
}
</div>
);
}