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 branch 'develop' into signal-auto-config

This commit is contained in:
Sonja Happ 2020-07-06 10:06:01 +02:00
commit 693eaed8d6
25 changed files with 295 additions and 153 deletions

View file

@ -1,43 +1,39 @@
# <img src="doc/pictures/villas_web.png" width=40 /> VILLASweb
## Description
This is VILLASweb, the website displaying and processing simulation data in the web browser. The term __frontend__ refers to this project, the actual website.
The frontend connects to __two__ backends: _VILLASweb-backend-go_ and _VILLASnode_.
VILLASnode provides actual simulation data via websockets. VILLASweb-backend-go provides any other data like user accounts, infrastructure components and configurations, dashboards etc.
This is VILLASweb, the website to configure real-time co-simulations and display simulation real-time data in the web browser.
The term **frontend** refers to this project, the actual website.
The frontend connects to **two** backends: [VILLASweb-backend-go](https://git.rwth-aachen.de/acs/public/villas/web-backend-go) and [VILLASnode](https://git.rwth-aachen.de/acs/public/villas/node).
VILLASnode provides actual simulation data via websockets. VILLASweb-backend-go provides any other data such as user accounts, infrastructure components and configurations, dashboards etc.
For more information on the backends see their repositories.
## Frameworks
The frontend is build upon [ReactJS](https://facebook.github.io/react/) and [Flux](https://facebook.github.io/flux/).
React is responsible for rendering the UI and Flux for handling the data and communication with the backends. For more information also have a look at REACT.md
Additional libraries are used, for a complete list see package.json.
Additional libraries are used, for a complete list see the file `package.json`.
## Data model
![Datamodel](src/img/datamodel.png)
## Quick start
We recommend Docker to get started quickly:
```bash
$ git clone --recursive git@git.rwth-aachen.de:VILLASframework/VILLASweb.git
$ cd VILLASweb
$ git clone --recursive https://git.rwth-aachen.de/acs/public/villas/web.git
$ cd web
$ npm install
$ npm start
```
We recommend to start the VILLASweb-backend-go before the frontend.
If you want to use test data (including some test users), you can start the backend with the parameter `-mode=test`.
Please check the repository of the VILLASweb-backend-go to find information on the test user login names and passwords.
The testing mode is NOT intended for production deployments.
## Documentation
More details on the setup and usage of VILLASweb is available here:
- [Requirements](doc/Requirements.md)
- [Structure and datamodel](doc/Structure.md)
- [Development setup](doc/development.md)
- [Production setup](doc/Production.md)
## Copyright
2020, Institute for Automation of Complex Power Systems, EONERC

82
doc/Production.md Normal file
View file

@ -0,0 +1,82 @@
# Production Setup {#web-production}
## Setting up VILLASweb for production
For development setup instructions see @ref web-development.
The production setup is based on docker.
Clone the [frontend](https://git.rwth-aachen.de/acs/public/villas/web) and [backend](https://git.rwth-aachen.de/acs/public/villas/web-backend-go) repositories on your computer and build the Docker images for both:
### Frontend
- `cd VILLASweb`
- `docker build -t villasweb-frontend .`
### Backend
- `cd ..\VILLASweb-backend-go`
- `docker build -t villasweb-backend .`
### WIP Docker compose and/or Kubernetes
Run the production docker-compose file:
- `docker-compose -f docker-compose-production.yml up -d`
## Configure VILLASnode to get data into VILLASweb
### Install VILLASnode
See: @ref node-installation
### Create a VILLASnode demo data source
1. Create a new empty configuration file with the following contents and save it as `webdemo.conf`:
> WIP this example configuration requires revision!
```
nodes = {
sine = {
type = "signal"
signal = "mixed"
values = 5
rate = 25
frequency = 5
}
web = {
type = "websocket"
destinations = [
"TODO"
]
}
}
paths = (
{
in = "sine"
out = "web"
}
)
```
The node `sine` is a software signal generator for 5 signals.
The node `web` is the websocket interface to stream the data generated by the `sine` node to the browser.
> Note: If you do not want to use your local system as the destination for the websocket node,
>change the option `destinations` of the `web` node to the destination of your production environment, for example `https://my.production.environment/ws/webdemo`.
### Start the VILLASnode gateway
Run the following command on your system:
```bash
villas node webdemo.conf
```
> Note: Change the path to the configuration file accordingly. The `villas` command will only work if VILLASnode is installed on your system.
### Visualize real-time data in VILLASweb Dashboards
1. Use the VILLASweb frontend to create a new infrastructure component for the VILLASnode gateway from above (Admin user required).
2. Set the `host` parameter of the component to the target you used as the `web.destinations` parameter in the configuration from above.
3. Create a new scenario in VILLASweb and within that scenario create a new component configuration that uses the infrastructure component you created under 2.
4. WIP: Use the signal auto-configure function to retrieve the signal configuration of the VILLASnode automatically.
5. Create a new dashboard with widgets of your choice and link these widgets to the signals received from the infrastructure component.
6. Enjoy what you see.

View file

@ -1,16 +1,12 @@
# Requirements {#web-requirements}
## Services
- NodeJS: Runs VILLASweb frontend
- Go: Runs VILLASweb backend
- PostgreSQL database (min version 11): Backend database
## Services and tools required for development
- [NodeJS with npm](https://nodejs.org/en/): Runs VILLASweb frontend
- [Go](https://golang.org/): Runs VILLASweb backend
- [PostgreSQL database](https://www.postgresql.org/) (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
- [Docker](https://www.docker.com/): Container management system
## Installed on your local computer
- NodeJS with npm
- Go (at least version 1.11)
- [swag](https://github.com/swaggo/swag)
- Docker
## Additional requirements for productive use
- [NGinX](https://www.nginx.com/): Webserver and reverse proxy for backends

View file

@ -8,11 +8,11 @@ In order to get started with VILLASweb, you might also want to check our our [de
### Description
The website itself based on the React JavaScript framework.
The website itself based on the [React JavaScript framework](https://reactjs.org/) and the [Flux library](https://facebook.github.io/flux/).
### Required
- NodeJS with npm
- [NodeJS with npm](https://nodejs.org/en/)
### Setup
@ -25,18 +25,19 @@ The website itself based on the React JavaScript framework.
- `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.
The backend must be running to make the website work.
Type `http://localhost:3000/` in the address field of your browser to open the website.
## Backend
### Description
The backend of VILLASweb uses the programming language Go and a PostgreSQL data base.
The backend of VILLASweb uses the programming language Go and a PostgreSQL database.
### Required
- Go (min version 1.11)
- Running PostgreSQL data base (min version 11)
- [Go](https://golang.org/) (min version 1.11)
- [PostgreSQL database](https://www.postgresql.org/) (min version 11)
- [swag](https://github.com/swaggo/swag)
### Setup and Running

View file

@ -29,6 +29,7 @@ class TableColumn extends Component {
linkKey: '',
dataIndex: false,
inlineEditable: false,
inputType: 'text',
clickable: false,
labelKey: null,
checkbox: false,

View file

@ -205,7 +205,7 @@ class CustomTable extends Component {
return (<td key={cellIndex} tabIndex={tabIndex} onClick={ evtHdls.onCellClick } onFocus={ evtHdls.onCellFocus } onBlur={ evtHdls.onCellBlur }>
{(this.state.editCell[0] === cellIndex && this.state.editCell[1] === rowIndex ) ? (
<FormControl as='input' type="text" value={cell} onChange={(event) => children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} ref={ref => { this.activeInput = ref; }} />
<FormControl as='input' type={children[cellIndex].props.inputType} value={cell} onChange={(event) => children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} ref={ref => { this.activeInput = ref; }} />
) : (
<span>
{cell.map((element, elementIndex) => (

View file

@ -73,6 +73,10 @@ class Dashboard extends Component {
return thisWidgetHeight > maxHeightSoFar? thisWidgetHeight : maxHeightSoFar;
}, 0);
if(dashboard.height === 0 || maxHeight + 80 > dashboard.height)
{
dashboard.height = maxHeight + 80;
}
// filter component configurations to the ones that belong to this scenario
let configs = []
@ -108,7 +112,7 @@ class Dashboard extends Component {
return ICused;
});
}
return {
dashboard,
widgets,
@ -130,7 +134,6 @@ class Dashboard extends Component {
widgetOrigIDs: prevState.widgetOrigIDs || [],
maxWidgetHeight: maxHeight || null,
dropZoneHeight: maxHeight +80 || null,
};
}
@ -188,25 +191,6 @@ class Dashboard extends Component {
}
}
/*
* Adapt the area's height with the position of the new widget.
* Return true if the height increased, otherwise false.
*/
increaseHeightWithWidget(widget) {
let increased = false;
let thisWidgetHeight = widget.y + widget.height;
if (thisWidgetHeight > this.state.maxWidgetHeight) {
increased = true;
this.setState({
maxWidgetHeight: thisWidgetHeight,
dropZoneHeight: thisWidgetHeight + 40
});
}
return increased;
}
transformToWidgetsList(widgets) {
return Object.keys(widgets).map( (key) => widgets[key]);
@ -238,24 +222,6 @@ class Dashboard extends Component {
}
/*
* Set the initial height state based on the existing widgets
*/
computeHeightWithWidgets(widgets) {
// Compute max height from widgets
let maxHeight = Object.keys(widgets).reduce( (maxHeightSoFar, widgetKey) => {
let thisWidget = widgets[widgetKey];
let thisWidgetHeight = thisWidget.y + thisWidget.height;
return thisWidgetHeight > maxHeightSoFar? thisWidgetHeight : maxHeightSoFar;
}, 0);
this.setState({
maxWidgetHeight: maxHeight,
dropZoneHeight: maxHeight + 80
});
}
editWidget(widget, index){
this.setState({ editModal: true, modalData: widget, modalIndex: index });
@ -377,6 +343,12 @@ class Dashboard extends Component {
token: this.state.sessionToken,
param: '?dashboardID=' + this.state.dashboard.id
});
AppDispatcher.dispatch({
type: 'dashboards/start-load',
data: this.props.match.params.dashboard,
token: this.state.sessionToken
});
this.setState({ editing: false, widgetChangeData: []});
};
@ -389,6 +361,30 @@ class Dashboard extends Component {
this.forceUpdate();
};
setDashboardSize(value) {
const maxHeight = Object.values(this.state.widgets).reduce((currentHeight, widget) => {
const absolutHeight = widget.y + widget.height;
return absolutHeight > currentHeight ? absolutHeight : currentHeight;
}, 0);
let dashboard = this.state.dashboard;
if(value === -1){
let tempHeight = this.state.dashboard.height - 50;
if(tempHeight > (maxHeight + 80)){
dashboard.height = tempHeight;
this.setState({dashboard});
}
}
else{
dashboard.height = this.state.dashboard.height +50;
this.setState( {dashboard});
}
this.forceUpdate();
}
pauseData(){
this.setState({ paused: true });
};
@ -402,6 +398,7 @@ class Dashboard extends Component {
const grid = this.state.dashboard.grid;
const boxClasses = classNames('section', 'box', { 'fullscreen-padding': this.props.isFullscreen });
let draggable = this.state.editing;
let dropZoneHeight = this.state.dashboard.height;
return <div className={boxClasses} >
<div className='section-header box-header'>
<div className="section-title">
@ -424,10 +421,10 @@ class Dashboard extends Component {
<div className="box box-content" onContextMenu={ (e) => e.preventDefault() }>
{this.state.editing &&
<WidgetToolbox grid={grid} onGridChange={this.setGrid.bind(this)} widgets={this.state.widgets} />
<WidgetToolbox grid={grid} onGridChange={this.setGrid.bind(this)} onDashboardSizeChange={this.setDashboardSize.bind(this)} widgets={this.state.widgets} />
}
{!draggable?(
<WidgetArea widgets={this.state.widgets} editing={this.state.editing} grid={grid} onWidgetAdded={this.handleDrop.bind(this)}>
<WidgetArea widgets={this.state.widgets} dropZoneHeight = {dropZoneHeight} editing={this.state.editing} grid={grid} onWidgetAdded={this.handleDrop.bind(this)}>
{this.state.widgets != null && Object.keys(this.state.widgets).map(widgetKey => (
<WidgetContextMenu
key={widgetKey}
@ -448,7 +445,7 @@ class Dashboard extends Component {
))}
</WidgetArea>
) : (
<WidgetArea widgets={this.state.widgets} editing={this.state.editing} grid={grid} onWidgetAdded={this.handleDrop.bind(this)}>
<WidgetArea widgets={this.state.widgets} editing={this.state.editing} dropZoneHeight= {dropZoneHeight} grid={grid} onWidgetAdded={this.handleDrop.bind(this)}>
{this.state.widgets != null && Object.keys(this.state.widgets).map(widgetKey => (
<Widget
key={widgetKey}

View file

@ -111,8 +111,11 @@ class ICDataStore extends ReduceStore {
state[action.ic].input.sequence++;
state[action.ic].input.values[action.signal-1] = action.data;
ICDataDataManager.send(state[action.ic].input, action.ic);
// copy of state needed because changes are not yet propagated
let input = JSON.parse(JSON.stringify(state[action.ic].input));
ICDataDataManager.send(input, action.ic);
this.__emitChange();
return state;
default:

View file

@ -39,7 +39,8 @@ class EditSignalMapping extends React.Component {
this.state = {
dir,
signals: []
signals: [],
modifiedSignalIDs : []
};
}
@ -51,41 +52,60 @@ class EditSignalMapping extends React.Component {
});
return {
signals: signals
signals: signals,
};
}
onClose(canceled) {
for (let signalID of this.state.modifiedSignalIDs){
let sig = this.state.signals.find(s => s.id === signalID);
//dispatch changes to signal
AppDispatcher.dispatch({
type: 'signals/start-edit',
data: sig,
token: this.props.sessionToken,
});
}
this.props.onCloseEdit(this.state.dir)
}
handleMappingChange = (event, row, column) => {
let sig = {}
let signals = this.state.signals;
let modifiedSignals = this.state.modifiedSignalIDs;
if (column === 1) { // Name change
if (event.target.value !== '') {
sig = this.state.signals[row];
sig.name = event.target.value;
}
signals[row].name = event.target.value;
if (modifiedSignals.find(id => id === signals[row].id) === undefined){
modifiedSignals.push(signals[row].id);
}
} else if (column === 2) { // unit change
if (event.target.value !== '') {
sig = this.state.signals[row];
sig.unit = event.target.value;
}
signals[row].unit = event.target.value;
if (modifiedSignals.find(id => id === signals[row].id) === undefined){
modifiedSignals.push(signals[row].id);
}
} else if (column === 3) { // scaling factor change
signals[row].scalingFactor = parseFloat(event.target.value);
if (modifiedSignals.find(id => id === signals[row].id) === undefined){
modifiedSignals.push(signals[row].id);
}
} else if (column === 0) { //index change
sig = this.state.signals[row];
sig.index = parseInt(event.target.value, 10);
signals[row].index =parseInt(event.target.value, 10);
if (modifiedSignals.find(id => id === signals[row].id) === undefined){
modifiedSignals.push(signals[row].id);
}
}
if (sig !== {}){
//dispatch changes to signal
AppDispatcher.dispatch({
type: 'signals/start-edit',
data: sig,
token: this.props.sessionToken,
});
}
this.setState({
signals: signals,
modifiedSignalIDs: modifiedSignals
})
};
@ -108,7 +128,8 @@ class EditSignalMapping extends React.Component {
direction: this.state.dir,
name: "PlaceholderName",
unit: "PlaceholderUnit",
index: 999
index: 999,
scalingFactor: 1.0
};
AppDispatcher.dispatch({
@ -139,7 +160,7 @@ class EditSignalMapping extends React.Component {
<Dialog
show={this.props.show}
title="Edit Signal Mapping"
buttonTitle="Close"
buttonTitle="Save"
blendOutCancel = {true}
onClose={(c) => this.onClose(c)}
onReset={() => this.resetState()}
@ -149,9 +170,10 @@ class EditSignalMapping extends React.Component {
<FormLabel>{this.props.direction} Mapping</FormLabel>
<FormText>Click <i>Index</i>, <i>Name</i> or <i>Unit</i> cell to edit</FormText>
<Table data={this.state.signals}>
<TableColumn title='Index' dataKey='index' inlineEditable onInlineChange={(e, row, column) => this.handleMappingChange(e, row, column)} />
<TableColumn title='Name' dataKey='name' inlineEditable onInlineChange={(e, row, column) => this.handleMappingChange(e, row, column)} />
<TableColumn title='Unit' dataKey='unit' inlineEditable onInlineChange={(e, row, column) => this.handleMappingChange(e, row, column)} />
<TableColumn title='Index' dataKey='index' inlineEditable inputType='number' onInlineChange={(e, row, column) => this.handleMappingChange(e, row, column)} />
<TableColumn title='Name' dataKey='name' inlineEditable inputType='text' onInlineChange={(e, row, column) => this.handleMappingChange(e, row, column)} />
<TableColumn title='Unit' dataKey='unit' inlineEditable inputType='text' onInlineChange={(e, row, column) => this.handleMappingChange(e, row, column)} />
<TableColumn title='Scaling Factor' dataKey='scalingFactor' inlineEditable inputType='number' onInlineChange={(e, row, column) => this.handleMappingChange(e, row, column)} />
<TableColumn title='Remove' deleteButton onDelete={(index) => this.handleDelete(index)} />
</Table>

View file

@ -387,7 +387,7 @@ body {
.section-buttons-group-right {
height: auto !important;
padding: 5px;
float: right;
}

View file

@ -373,9 +373,9 @@ div[class*="-widget"] label {
/* End table widget*/
/* Begin box widget */
.box-widget .border {
.box-widget {
width: 100%;
height: 100%;
border: 2px solid;
border: 2px solid lightgray;
}
/* End box widget */

View file

@ -44,13 +44,8 @@ class WidgetArea extends React.Component {
}
render() {
const maxHeight = Object.values(this.props.widgets).reduce((currentHeight, widget) => {
const absolutHeight = widget.y + widget.height;
return absolutHeight > currentHeight ? absolutHeight : currentHeight;
}, 0);
return <Dropzone height={maxHeight + 80} onDrop={this.handleDrop} editing={this.props.editing} widgets={this.props.widgets}>
return <Dropzone height={this.props.dropZoneHeight} onDrop={this.handleDrop} editing={this.props.editing} widgets={this.props.widgets}>
{this.props.children}
<Grid size={this.props.grid} disabled={this.props.grid === 1 || this.props.editing !== true} />

View file

@ -168,6 +168,7 @@ class WidgetFactory {
widget.width = 100;
widget.height = 100;
widget.customProperties.border_color = 0;
widget.customProperties.background_color = 9;
widget.customProperties.background_color_opacity = 0.5;
widget.z = 0;
break;

View file

@ -27,7 +27,7 @@ class PlotLegend extends React.Component {
<ul>
{this.props.signals.map(signal =>
<li key={signal.id} className="signal-legend" style={{ color: colorScale(signal.id) }}>
<span className="signal-legend-name">{signal.name}</span>
<span className="signal-legend-name">{signal.name + "(x" + signal.scalingFactor + ")"}</span>
<span style={{ marginLeft: '0.3em' }} className="signal-unit">{signal.unit}</span>
</li>
)}

View file

@ -18,6 +18,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import Slider from 'rc-slider';
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap';
import Icon from "../common/icon";
import ToolboxItem from './toolbox-item';
@ -57,6 +59,21 @@ class WidgetToolbox extends React.Component {
<div>
<span>Grid: { this.props.grid > 1 ? this.props.grid : 'Disabled' }</span>
<Slider value={this.props.grid} style={{ width: '80px' }} step={5} onChange={this.onGridChange} />
</div>
</div>
<div className='section-buttons-group-right'>
<div>
<OverlayTrigger key={0} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"increase"}`}> Increase dashboard height </Tooltip>} >
<Button variant="dark" key={0} onClick={() => this.props.onDashboardSizeChange(1)} >
<Icon icon="plus" />
</Button>
</OverlayTrigger>
<OverlayTrigger key={1} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"decrease"}`}> Decrease dashboard height </Tooltip>} >
<Button variant="dark" key={1} onClick={() => this.props.onDashboardSizeChange(-1)} >
<Icon icon="minus" />
</Button>
</OverlayTrigger>
</div>
</div>
</div>;
@ -66,7 +83,8 @@ class WidgetToolbox extends React.Component {
WidgetToolbox.propTypes = {
widgets: PropTypes.array,
grid: PropTypes.number,
onGridChange: PropTypes.func
onGridChange: PropTypes.func,
onDashboardSizeChange: PropTypes.func
};
export default WidgetToolbox;

View file

@ -88,13 +88,13 @@ class Widget extends React.Component {
};
}
inputDataChanged(widget, data, controlID) {
inputDataChanged(widget, data, controlID, controlValue) {
// 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;
updatedWidget.customProperties[controlID] = controlValue;
AppDispatcher.dispatch({
type: 'widgets/start-edit',
token: this.state.sessionToken,
@ -118,7 +118,7 @@ class Widget extends React.Component {
type: 'icData/inputChanged',
ic: icID,
signal: signal[0].index,
data
data: signal[0].scalingFactor * data
});
}
@ -179,14 +179,14 @@ class Widget extends React.Component {
return <WidgetButton
widget={widget}
editing={this.props.editing}
onInputChanged={(value, controlID) => this.inputDataChanged(widget, value, controlID)}
onInputChanged={(value, controlID, controlValue) => this.inputDataChanged(widget, value, controlID, controlValue)}
signals={this.state.signals}
/>
} else if (widget.type === 'NumberInput') {
return <WidgetInput
widget={widget}
editing={this.props.editing}
onInputChanged={(value, controlID) => this.inputDataChanged(widget, value, controlID)}
onInputChanged={(value, controlID, controlValue) => this.inputDataChanged(widget, value, controlID, controlValue)}
signals={this.state.signals}
/>
} else if (widget.type === 'Slider') {
@ -194,7 +194,7 @@ class Widget extends React.Component {
widget={widget}
editing={this.props.editing}
onWidgetChange={(w) => this.props.onWidgetStatusChange(w, this.props.index) }
onInputChanged={(value, controlID) => this.inputDataChanged(widget, value, controlID)}
onInputChanged={(value, controlID, controlValue) => this.inputDataChanged(widget, value, controlID, controlValue)}
signals={this.state.signals}
/>
} else if (widget.type === 'Gauge') {

View file

@ -31,10 +31,8 @@ class WidgetBox extends Component {
}
return (
<div className="box-widget full">
<div className="border" style={colorStyle}>
<div className="box-widget full" style={colorStyle}>
{ }
</div>
</div>
);
}

View file

@ -28,11 +28,16 @@ class WidgetButton extends Component {
}
}
static getDerivedStateFromProps(props, state){
return {
pressed: props.widget.customProperties.pressed
}
}
onPress(e) {
if (e.button === 0 && !this.props.widget.customProperties.toggle) {
this.setState({ pressed: true });
this.valueChanged(this.props.widget.customProperties.on_value);
this.valueChanged(this.props.widget.customProperties.on_value, true);
}
}
@ -43,15 +48,14 @@ class WidgetButton extends Component {
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.valueChanged(nextState ? this.props.widget.customProperties.on_value : this.props.widget.customProperties.off_value, nextState);
}
}
valueChanged(newValue) {
if (this.props.onInputChanged)
this.props.onInputChanged(newValue, 'pressed');
valueChanged(newValue, pressed) {
if (this.props.onInputChanged) {
this.props.onInputChanged(newValue, 'pressed', pressed);
}
}
render() {

View file

@ -49,7 +49,7 @@ class WidgetGauge extends Component {
}
componentDidUpdate(prevProps: Readonly<P>, prevState: Readonly<S>, snapshot: SS): void {
// update gauge's value
if(prevState.value !== this.state.value){
this.gauge.set(this.state.value)
@ -116,7 +116,7 @@ class WidgetGauge extends Component {
// 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 (data != null) {
const value = Math.round(data[data.length - 1].y * 1e3) / 1e3;
const value = signal[0].scalingFactor * Math.round(data[data.length - 1].y * 1e3) / 1e3;
let minValue = null;
let maxValue = null;
@ -149,7 +149,7 @@ class WidgetGauge extends Component {
maxValue = props.widget.customProperties.valueMax;
updateMaxValue = true;
updateLabels = true;
}
if (updateLabels === false && state.gauge) {
@ -169,7 +169,7 @@ class WidgetGauge extends Component {
if(props.widget.customProperties.valueUseMinMax !== state.useMinMax){
returnState["useMinMax"] = props.widget.customProperties.valueUseMinMax;
}
// prepare returned state
if(updateValue === true){
returnState["value"] = value;
@ -201,7 +201,7 @@ 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) {

View file

@ -71,7 +71,7 @@ class WidgetInput extends Component {
valueChanged(newValue) {
if (this.props.onInputChanged) {
this.props.onInputChanged(Number(newValue), 'value');
this.props.onInputChanged(Number(newValue), 'value', Number(newValue));
}
}

View file

@ -49,8 +49,8 @@ class WidgetLamp extends Component {
// check if value has changed
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 };
if (data != null && Number(state.value) !== signal[0].scalingFactor * data[data.length - 1].y) {
return { value: signal[0].scalingFactor * data[data.length - 1].y };
}
return null;

View file

@ -49,13 +49,31 @@ class WidgetPlot extends React.Component {
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]);
let values = props.data[icID].output.values[sig.index-1];
if(sig.scalingFactor !== 1) {
let scaledValues = JSON.parse(JSON.stringify(values));
for (let i=0; i< scaledValues.length; i++){
scaledValues[i].y = scaledValues[i].y * sig.scalingFactor;
}
data.push(scaledValues);
} else {
data.push(values);
}
}
}
} 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]);
let values = props.data[icID].output.values[sig.index-1];
if(sig.scalingFactor !== 1) {
let scaledValues = JSON.parse(JSON.stringify(values));
for (let i=0; i< scaledValues.length; i++){
scaledValues[i].y = scaledValues[i].y * sig.scalingFactor;
}
data.push(scaledValues);
} else {
data.push(values);
}
}
}
}
@ -67,6 +85,11 @@ class WidgetPlot extends React.Component {
}
scaleData(data, scaleFactor){
// data is an array of value pairs x,y
}
render() {
return <div className="plot-widget" ref="wrapper">
<div className="widget-plot">

View file

@ -105,7 +105,7 @@ class WidgetSlider extends Component {
valueChanged(newValue) {
if (this.props.onInputChanged) {
this.props.onInputChanged(newValue, 'value');
this.props.onInputChanged(newValue, 'value', newValue);
}
}

View file

@ -41,15 +41,20 @@ class WidgetTable extends Component {
// determine ID of infrastructure component related to signal (via config)
let icID = props.icIDs[sig.id]
let signalName = sig.name;
if(sig.scalingFactor !== 1.0){
signalName = signalName + "(x" + String(sig.scalingFactor) + ")";
}
// 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,
name: signalName,
unit: sig.unit,
value: data[data.length - 1].y
value: data[data.length - 1].y * sig.scalingFactor
});
}
@ -59,9 +64,9 @@ class WidgetTable extends Component {
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,
name: signalName,
unit: sig.unit,
value: data[data.length - 1].y
value: data[data.length - 1].y * sig.scalingFactor
});
}
}
@ -86,7 +91,7 @@ class WidgetTable extends Component {
var columns = [
<TableColumn key={1} title="Signal" dataKey="name" width={120} />,
<TableColumn key={2} title="Value" dataKey="value" modifier={format('.4s')} />
<TableColumn key={2} title="Value" dataKey="value" modifier={format('.4f')} />
];
if (this.props.widget.customProperties.showUnit)

View file

@ -47,7 +47,7 @@ class WidgetValue extends Component {
// check if value has changed
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;
value = signal[0].scalingFactor * data[data.length - 1].y;
}
}
@ -66,12 +66,12 @@ class WidgetValue extends Component {
render() {
let value_to_render = Number(this.state.value);
let value_width = this.props.widget.customProperties.textSize*(value_to_render < 1000 ? (2):(3));
let value_width = this.props.widget.customProperties.textSize*(Math.abs(value_to_render) < 1000 ? (5):(8));
let unit_width = this.props.widget.customProperties.textSize*(this.state.unit.length + 0.7);
return (
<div className="single-value-widget">
<strong style={{ fontSize: this.props.widget.customProperties.textSize + 'px', flex: '1 1 auto'}}>{this.props.widget.name}</strong>
<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>
<span style={{ fontSize: this.props.widget.customProperties.textSize + 'px', flex: 'none', width: value_width}}>{Number.isNaN(value_to_render) ? NaN : format('.3f')(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>
}