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:
commit
693eaed8d6
25 changed files with 295 additions and 153 deletions
34
README.md
34
README.md
|
@ -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
|
||||
|
||||

|
||||
|
||||
## 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
82
doc/Production.md
Normal 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.
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -29,6 +29,7 @@ class TableColumn extends Component {
|
|||
linkKey: '',
|
||||
dataIndex: false,
|
||||
inlineEditable: false,
|
||||
inputType: 'text',
|
||||
clickable: false,
|
||||
labelKey: null,
|
||||
checkbox: false,
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -387,7 +387,7 @@ body {
|
|||
|
||||
.section-buttons-group-right {
|
||||
height: auto !important;
|
||||
|
||||
padding: 5px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue