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

continue merge

This commit is contained in:
Sonja Happ 2020-11-16 15:01:24 +01:00
parent 94d5669500
commit 86b1b2d5a5
16 changed files with 1395 additions and 178 deletions

View file

@ -538,7 +538,7 @@ Inside `index.html`, you can use it like this:
Only files inside the `public` folder will be accessible by `%PUBLIC_URL%` prefix. If you need to use a file from `src` or `node_modules`, youll have to copy it there to explicitly specify your intention to make this file a part of the build.
When you run `npm run build`, Create React App will substitute `%PUBLIC_URL%` with a correct absolute path so your project works even if you use client-side routing or host it at a non-root URL.
When you run `npm run build`, Create React App will substitute `%PUBLIC_URL%` with a correct absolute path so your project works even if you use client-side routing or websocketurl it at a non-root URL.
In JavaScript code, you can use `process.env.PUBLIC_URL` for similar purposes:
@ -789,7 +789,7 @@ You can find the companion GitHub repository [here](https://github.com/fullstack
>Note: this feature is available with `react-scripts@0.2.3` and higher.
People often serve the front-end React app from the same host and port as their backend implementation.<br>
People often serve the front-end React app from the same websocketurl and port as their backend implementation.<br>
For example, a production setup might look like this after the app is deployed:
```
@ -798,7 +798,7 @@ For example, a production setup might look like this after the app is deployed:
/api/todos - server handles any /api/* requests using the backend implementation
```
Such setup is **not** required. However, if you **do** have a setup like this, it is convenient to write requests like `fetch('/api/todos')` without worrying about redirecting them to another host or port during development.
Such setup is **not** required. However, if you **do** have a setup like this, it is convenient to write requests like `fetch('/api/todos')` without worrying about redirecting them to another websocketurl or port during development.
To tell the development server to proxy any unknown requests to your API server in development, add a `proxy` field to your `package.json`, for example:
@ -820,7 +820,7 @@ The `proxy` option supports HTTP, HTTPS and WebSocket connections.<br>
If the `proxy` option is **not** flexible enough for you, alternatively you can:
* Enable CORS on your server ([heres how to do it for Express](http://enable-cors.org/server_expressjs.html)).
* Use [environment variables](#adding-custom-environment-variables) to inject the right server host and port into your app.
* Use [environment variables](#adding-custom-environment-variables) to inject the right server websocketurl and port into your app.
## Using HTTPS in Development
@ -1512,10 +1512,10 @@ You can adjust various development and production settings by setting environmen
Variable | Development | Production | Usage
:--- | :---: | :---: | :---
BROWSER | :white_check_mark: | :x: | By default, Create React App will open the default system browser, favoring Chrome on macOS. Specify a [browser](https://github.com/sindresorhus/opn#app) to override this behavior, or set it to `none` to disable it completely.
HOST | :white_check_mark: | :x: | By default, the development web server binds to `localhost`. You may use this variable to specify a different host.
HOST | :white_check_mark: | :x: | By default, the development web server binds to `localhost`. You may use this variable to specify a different websocketurl.
PORT | :white_check_mark: | :x: | By default, the development web server will attempt to listen on port 3000 or prompt you to attempt the next available port. You may use this variable to specify a different port.
HTTPS | :white_check_mark: | :x: | When set to `true`, Create React App will run the development server in `https` mode.
PUBLIC_URL | :x: | :white_check_mark: | Create React App assumes your application is hosted at the serving web server's root or a subpath as specified in [`package.json` (`homepage`)](#building-for-relative-paths). Normally, Create React App ignores the hostname. You may use this variable to force assets to be referenced verbatim to the url you provide (hostname included). This may be particularly useful when using a CDN to host your application.
PUBLIC_URL | :x: | :white_check_mark: | Create React App assumes your application is hosted at the serving web server's root or a subpath as specified in [`package.json` (`homepage`)](#building-for-relative-paths). Normally, Create React App ignores the hostname. You may use this variable to force assets to be referenced verbatim to the url you provide (hostname included). This may be particularly useful when using a CDN to websocketurl your application.
CI | :large_orange_diamond: | :white_check_mark: | When set to `true`, Create React App treats warnings as failures in the build. It also makes the test runner non-watching. Most CIs set this flag by default.
## Troubleshooting

View file

@ -75,7 +75,7 @@ villas node webdemo.conf
### 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.
2. Set the `websocketurl` 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.

1005
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,9 +5,11 @@
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "^0.1.11",
"@fortawesome/react-fontawesome": "^0.1.12",
"babel-runtime": "^6.26.0",
"bootstrap": "^4.5.3",
"bufferutil": "^4.0.1",
"canvas": "^2.6.1",
"classnames": "^2.2.6",
"d3-array": "^2.8.0",
"d3-axis": "^2.0.0",
@ -17,6 +19,7 @@
"d3-shape": "^2.0.0",
"d3-time-format": "^3.0.0",
"es6-promise": "^4.2.8",
"fibers": "^5.0.0",
"file-saver": "^2.0.2",
"flux": "^3.1.3",
"gaugeJS": "^1.3.7",
@ -30,12 +33,12 @@
"node-sass": "^4.14.1",
"popper.js": "^1.16.1",
"prop-types": "^15.7.2",
"rc-slider": "^9.5.4",
"rc-slider": "^9.6.0",
"react": "^16.14.0",
"react-bootstrap": "^1.4.0",
"react-bootstrap-time-picker": "^2.0.1",
"react-collapse": "^5.0.1",
"react-color": "^2.18.1",
"react-color": "^2.19.3",
"react-contexify": "^4.1.1",
"react-d3": "^0.4.0",
"react-dnd": "^10.0.2",
@ -50,10 +53,15 @@
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.0",
"react-svg-pan-zoom": "^3.8.1",
"sass": "^1.27.0",
"sass": "^1.28.0",
"superagent": "^6.1.0",
"typescript": "^4.0.3",
"validator": "^13.1.17"
"ts-node": "^9.0.0",
"type-fest": "^0.13.1",
"typescript": "^4.0.5",
"utf-8-validate": "^5.0.2",
"validator": "^13.1.17",
"webpack-hot-middleware": "^2.25.0",
"webpack-plugin-serve": "^1.2.0"
},
"devDependencies": {
"chai": "^4.2.0"

View file

@ -16,19 +16,19 @@
******************************************************************************/
class WebsocketAPI {
constructor(host, callbacks) {
this.host = host;
constructor(websocketurl, callbacks) {
this.websocketurl = websocketurl;
this.callbacks = callbacks;
this.wasConnected = false;
this.isClosing = false;
this.connect(host, callbacks);
this.connect(websocketurl, callbacks);
}
connect(host, callbacks) {
connect(websocketurl, callbacks) {
// create web socket client
this.socket = new WebSocket(WebsocketAPI.getURL(host), 'live');
this.socket = new WebSocket(WebsocketAPI.getURL(websocketurl), 'live');
this.socket.binaryType = 'arraybuffer';
this.socket.onclose = this.onClose;
this.socket.onopen = this.onOpen;
@ -40,12 +40,12 @@ class WebsocketAPI {
}
reconnect() {
//console.log("Reconnecting: " + this.host);
this.connect(this.host, this.callbacks);
//console.log("Reconnecting: " + this.websocketurl);
this.connect(this.websocketurl, this.callbacks);
}
get url() {
return WebsocketAPI.getURL(this.host);
return WebsocketAPI.getURL(this.websocketurl);
}
send(data) {
@ -58,7 +58,7 @@ class WebsocketAPI {
}
onError = e => {
console.error('Error on WebSocket connection to: ' + this.host + ':', e);
console.error('Error on WebSocket connection to: ' + this.websocketurl + ':', e);
if ('onError' in this.callbacks)
this.callbacks.onError(e);
@ -78,16 +78,16 @@ class WebsocketAPI {
}
else {
if (this.wasConnected) {
console.log("Connection to " + this.host + " dropped. Attempt reconnect in 1 sec");
console.log("Connection to " + this.websocketurl + " dropped. Attempt reconnect in 1 sec");
window.setTimeout(() => { this.reconnect(); }, 1000);
}
}
}
static getURL(host) {
static getURL(websocketurl) {
// create an anchor element (note: no need to append this element to the document)
var link = document.createElement('a');
link.href = host;
link.href = websocketurl;
if (link.protocol === 'https:')
link.protocol = 'wss:';

View file

@ -98,6 +98,7 @@ class ArrayStore extends ReduceStore {
return state;
case this.type + '/added':
if(typeof action.data.managedexternally !== "undefined" && action.data.managedexternally === true ) return state;
return this.updateElements(state, [action.data]);
case this.type + '/add-error':

View file

@ -16,7 +16,8 @@
******************************************************************************/
import React from 'react';
import { Button, Modal } from 'react-bootstrap';
import { Button, Modal, FormLabel } from 'react-bootstrap';
import {Collapse} from 'react-collapse';
class DeleteDialog extends React.Component {
onModalKeyPress = (event) => {
@ -35,6 +36,9 @@ class DeleteDialog extends React.Component {
<Modal.Body>
Are you sure you want to delete the {this.props.title} <strong>'{this.props.name}'</strong>?
<Collapse isOpened={this.props.managedexternally} >
<FormLabel size="sm">The IC will be deleted if the respective VILLAScontroller sends "gone" state and no component config is using the IC anymore</FormLabel>
</Collapse>
</Modal.Body>
<Modal.Footer>

View file

@ -103,8 +103,9 @@ class CustomTable extends Component {
// add buttons
if (child.props.editButton) {
cell.push(<OverlayTrigger key={0} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"edit"}`}> Edit </Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onEdit(index)} disabled={child.props.onEdit == null}><Icon icon='edit' /></Button></OverlayTrigger>);
let disable = (typeof data.managedexternally !== "undefined" && data.managedexternally);
cell.push(<OverlayTrigger key={0} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"edit"}`}>{disable? "Externally managed ICs cannot be edited" : "edit"} </Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onEdit(index)} disabled={disable || child.props.onEdit == null}><Icon icon='edit' /></Button></OverlayTrigger>);
}
if (child.props.deleteButton) {

View file

@ -16,9 +16,9 @@
******************************************************************************/
import React from 'react';
import { FormGroup, FormControl, FormLabel } from 'react-bootstrap';
import { FormGroup, FormControl, FormLabel, FormCheck } from 'react-bootstrap';
import _ from 'lodash';
import {Collapse} from 'react-collapse';
import Dialog from '../common/dialogs/dialog';
import ParametersEditor from '../common/parameters-editor';
@ -30,10 +30,14 @@ class EditICDialog extends React.Component {
this.state = {
name: '',
host: '',
websocketurl: '',
apiurl: '',
location: '',
description: '',
type: '',
category: '',
properties: {},
managedexternally: false,
startParameterScheme: {},
};
}
@ -46,8 +50,20 @@ class EditICDialog extends React.Component {
data.name = this.state.name;
}
if (this.state.host != null && this.state.host !== "" && this.state.host !== "http://" && this.state.host !== this.props.ic.host) {
data.host = this.state.host;
if (this.state.websocketurl != null && this.state.websocketurl !== "" && this.state.websocketurl !== "http://" && this.state.websocketurl !== this.props.ic.websocketurl) {
data.websocketurl = this.state.websocketurl;
}
if (this.state.apiurl != null && this.state.apiurl !== "" && this.state.apiurl !== "http://" && this.state.apiurl !== this.props.ic.apiurl) {
data.apiurl = this.state.apiurl;
}
if (this.state.location != null && this.state.location !== this.props.ic.location) {
data.location = this.state.location;
}
if (this.state.description != null && this.state.description !== this.props.ic.description) {
data.description = this.state.description;
}
if (this.state.type != null && this.state.type !== "" && this.state.type !== this.props.ic.type) {
@ -57,37 +73,70 @@ class EditICDialog extends React.Component {
if (this.state.category != null && this.state.category !== "" && this.state.category !== this.props.ic.category) {
data.category = this.state.category;
}
if (this.state.properties !== {}) {
data.properties = this.state.properties
if (this.state.startParameterScheme !== {}) {
data.startParameterScheme = this.state.startParameterScheme
}
data.managedexternally = this.state.managedexternally;
this.props.onClose(data);
this.setState({managedexternally: false});
}
} else {
this.props.onClose();
this.setState({managedexternally: false});
}
}
handleChange(e) {
if(e.target.id === "managedexternally"){
this.setState({ managedexternally : !this.state.managedexternally});
}
else{
this.setState({ [e.target.id]: e.target.value });
}
}
handlePropertiesChange(data) {
this.setState({ properties: data });
handleStartParameterSchemeChange(data) {
this.setState({ startParameterScheme: data });
}
resetState() {
this.setState({
name: this.props.ic.name,
host: this.props.ic.host,
websocketurl: this.props.ic.websocketurl,
apiurl: this.props.ic.apiurl,
type: this.props.ic.type,
location: this.props.ic.location,
description: this.props.ic.description,
category: this.props.ic.category,
properties: _.merge({}, _.get(this.props.ic, 'rawProperties'), _.get(this.props.ic, 'properties'))
managedexternally: false,
startParameterScheme: this.props.ic.startParameterScheme,
});
}
render() {
let typeOptions = [];
switch(this.state.category){
case "simulator":
typeOptions = ["dummy","generic","dpsim","rtlab","rscad"];
break;
case "controller":
typeOptions = ["kubernetes","villas-controller"];
break;
case "gateway":
typeOptions = ["villas-node","villas-relay"];
break;
case "service":
typeOptions = ["ems","custom"];
break;
case "equipment":
typeOptions = ["chroma-emulator","chroma-loads","sma-sunnyboy","fleps","sonnenbatterie"];
break;
default:
typeOptions =[];
}
return (
<Dialog
show={this.props.show}
@ -105,27 +154,51 @@ class EditICDialog extends React.Component {
<FormControl type="text" placeholder={this.props.ic.name} value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="host">
<FormLabel column={false}>Host</FormLabel>
<FormControl type="text" placeholder={this.props.ic.host} value={this.state.host || 'http://' } onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="category">
<FormLabel column={false}>Category (e.g. Simulator, Gateway, ...)</FormLabel>
<FormControl type="text" placeholder={this.props.ic.category} value={this.state.category} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
<FormLabel column={false}>Category</FormLabel>
<FormControl as="select" value={this.state.category} onChange={(e) => this.handleChange(e)}>
<option>simulator</option>
<option>controller</option>
<option>service</option>
<option>gateway</option>
<option>equipment</option>
</FormControl>
</FormGroup>
<FormGroup controlId="type">
<FormLabel column={false}>Type (e.g. RTDS, VILLASnode, ...)</FormLabel>
<FormControl type="text" placeholder={this.props.ic.type} value={this.state.type} onChange={(e) => this.handleChange(e)} />
<FormLabel column={false}>Type</FormLabel>
<FormControl as="select" value={this.state.type} onChange={(e) => this.handleChange(e)}>
<option default>Select type</option>
{typeOptions.map((name,index) => (
<option key={index}>{name}</option>
))}
</FormControl>
</FormGroup>
<FormGroup controlId="websocketurl">
<FormLabel column={false}>Websocket URL</FormLabel>
<FormControl type="text" placeholder={this.props.ic.websocketurl} value={this.state.websocketurl || 'http://' } onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId='properties'>
<FormLabel column={false}>Properties</FormLabel>
<FormGroup controlId="apiurl">
<FormLabel column={false}>API URL</FormLabel>
<FormControl type="text" placeholder={this.props.ic.apiurl} value={this.state.apiurl || 'http://' } onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="location">
<FormLabel column={false}>Location</FormLabel>
<FormControl type="text" placeholder={this.props.ic.location} value={this.state.location || '' } onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="description">
<FormLabel column={false}>Description</FormLabel>
<FormControl type="text" placeholder={this.props.ic.description} value={this.state.description || '' } onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId='startParameterScheme'>
<FormLabel column={false}>Start parameter scheme of IC</FormLabel>
<ParametersEditor
content={this.state.properties}
content={this.state.startParameterScheme}
disabled={false}
onChange={(data) => this.handlePropertiesChange(data)}
onChange={(data) => this.handleStartParameterSchemeChange(data)}
/>
</FormGroup>
</form>

View file

@ -26,19 +26,19 @@ class IcDataDataManager {
this._sockets = {};
}
open(host, identifier) {
open(websocketurl, identifier) {
// pass signals to onOpen callback
if (this._sockets[identifier] != null)
return; // already open?
this._sockets[identifier] = new WebsocketAPI(host, { onOpen: (event) => this.onOpen(event, identifier, true), onClose: (event) => this.onClose(event, identifier), onMessage: (event) => this.onMessage(event, identifier) });
this._sockets[identifier] = new WebsocketAPI(websocketurl, { onOpen: (event) => this.onOpen(event, identifier, true), onClose: (event) => this.onClose(event, identifier), onMessage: (event) => this.onMessage(event, identifier) });
}
update(host, identifier) {
update(websocketurl, identifier) {
if (this._sockets[identifier] != null) {
if (this._sockets[identifier].host !== host) {
if (this._sockets[identifier].websocketurl !== websocketurl) {
this._sockets[identifier].close();
this._sockets[identifier] = new WebsocketAPI(host, { onOpen: (event) => this.onOpen(event, identifier, false), onClose: (event) => this.onClose(event, identifier), onMessage: (event) => this.onMessage(event, identifier), onError: (error) => this.onError(error, identifier) });
this._sockets[identifier] = new WebsocketAPI(websocketurl, { onOpen: (event) => this.onOpen(event, identifier, false), onClose: (event) => this.onClose(event, identifier), onMessage: (event) => this.onMessage(event, identifier), onError: (error) => this.onError(error, identifier) });
}
}
}

View file

@ -35,8 +35,8 @@ class InfrastructureComponentStore extends ArrayStore {
// connect to each infrastructure component
const ic = action.data;
if (ic.host != null && ic.host !== '') {
ICDataDataManager.update(ic.host, ic.id);
if (ic.websocketurl != null && ic.websocketurl !== '') {
ICDataDataManager.update(ic.websocketurl, ic.id);
}
return super.reduce(state, action);
@ -44,17 +44,17 @@ class InfrastructureComponentStore extends ArrayStore {
// open websocket for each IC contained in array action.data
// action.data contains only those IC used by the scenario
for (let ic of action.data) {
if (ic.host != null && ic.host !== '') {
ICDataDataManager.open(ic.host, ic.id);
if (ic.websocketurl != null && ic.websocketurl !== '') {
ICDataDataManager.open(ic.websocketurl, ic.id);
} else {
// TODO add to pool of notifications
const IC_WEBSOCKET_HOST_ERROR = {
const IC_WEBSOCKET_URL_ERROR = {
title: 'Websocket connection warning',
message: "Websocket host parameter not available for IC " + ic.name + "(" + ic.uuid + "), connection not possible",
message: "Websocket URL parameter not available for IC " + ic.name + "(" + ic.uuid + "), connection not possible",
level: 'warning'
};
NotificationsDataManager.addNotification(IC_WEBSOCKET_HOST_ERROR);
NotificationsDataManager.addNotification(IC_WEBSOCKET_URL_ERROR);
}
}
return super.reduce(state, action);

View file

@ -30,7 +30,7 @@ class ImportICDialog extends React.Component {
this.state = {
name: '',
host: '',
websocketurl: '',
uuid: ''
};
}
@ -45,8 +45,8 @@ class ImportICDialog extends React.Component {
uuid: this.state.uuid
};
if (this.state.host != null && this.state.host !== "" && this.state.host !== 'http://') {
data.host = this.state.host;
if (this.state.websocketurl != null && this.state.websocketurl !== "" && this.state.websocketurl !== 'http://') {
data.websocketurl = this.state.websocketurl;
}
this.props.onClose(data);
@ -61,7 +61,7 @@ class ImportICDialog extends React.Component {
}
resetState() {
this.setState({ name: '', host: 'http://', uuid: '' });
this.setState({ name: '', websocketurl: 'http://', uuid: '' });
}
loadFile(fileList) {
@ -81,7 +81,7 @@ class ImportICDialog extends React.Component {
self.imported = true;
self.setState({
name: _.get(ic, 'properties.name') || _.get(ic, 'rawProperties.name'),
host: _.get(ic, 'host'),
websocketurl: _.get(ic, 'websocketurl'),
uuid: ic.uuid
});
};
@ -123,9 +123,9 @@ class ImportICDialog extends React.Component {
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="host">
<FormLabel>Host</FormLabel>
<FormControl type="text" placeholder="Enter host" value={this.state.host} onChange={(e) => this.handleChange(e)} />
<FormGroup controlId="websocketurl">
<FormLabel>Websocket URL</FormLabel>
<FormControl type="text" placeholder="Enter websocketurl" value={this.state.websocketurl} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="uuid" validationState={this.validateForm('uuid')}>

View file

@ -16,8 +16,7 @@
******************************************************************************/
import React from 'react';
import { FormGroup, FormControl, FormLabel } from 'react-bootstrap';
import { FormGroup, FormControl, FormLabel, FormCheck, OverlayTrigger, Tooltip} from 'react-bootstrap';
import Dialog from '../common/dialogs/dialog';
class NewICDialog extends React.Component {
@ -28,10 +27,11 @@ class NewICDialog extends React.Component {
this.state = {
name: '',
host: '',
websocketurl: '',
uuid: '',
type: '',
category: '',
managedexternally: false,
};
}
@ -42,33 +42,41 @@ class NewICDialog extends React.Component {
name: this.state.name,
type: this.state.type,
category: this.state.category,
uuid: this.state.uuid
uuid: this.state.uuid,
managedexternally: this.state.managedexternally,
};
if (this.state.host != null && this.state.host !== "" && this.state.host !== 'http://') {
data.host = this.state.host;
if (this.state.websocketurl != null && this.state.websocketurl !== "" && this.state.websocketurl !== 'http://') {
data.websocketurl = this.state.websocketurl;
}
this.props.onClose(data);
this.setState({managedexternally: false});
}
} else {
this.props.onClose();
this.setState({managedexternally: false});
}
}
handleChange(e) {
if(e.target.id === "managedexternally"){
this.setState({ managedexternally : !this.state.managedexternally});
}
else{
this.setState({ [e.target.id]: e.target.value });
}
}
resetState() {
this.setState({ name: '', host: 'http://', uuid: this.uuidv4(), type: '', category: ''});
this.setState({ name: '', websocketurl: 'http://', uuid: this.uuidv4(), type: '', category: '', managedexternally: false});
}
validateForm(target) {
// check all controls
let name = true;
let uuid = true;
let host = true;
let websocketurl = true;
let type = true;
let category = true;
@ -76,7 +84,7 @@ class NewICDialog extends React.Component {
name = false;
}
if (this.state.uuid === '') {
if (!this.state.managedexternally && this.state.uuid === '') {
uuid = false;
}
@ -88,14 +96,16 @@ class NewICDialog extends React.Component {
category = false;
}
this.valid = name && uuid && host && type && category;
this.valid = name && uuid && websocketurl && type && category;
// return state to control
if (target === 'name') return name ? "success" : "error";
if (target === 'uuid') return uuid ? "success" : "error";
if (target === 'host') return host ? "success" : "error";
if (target === 'websocketurl') return websocketurl ? "success" : "error";
if (target === 'type') return type ? "success" : "error";
if (target === 'category') return category ? "success" : "error";
return this.valid;
}
uuidv4() {
@ -107,27 +117,84 @@ class NewICDialog extends React.Component {
}
render() {
let typeOptions = [];
switch(this.state.category){
case "simulator":
typeOptions = ["dummy","generic","dpsim","rtlab","rscad"];
break;
case "controller":
typeOptions = ["kubernetes","villas-controller"];
break;
case "gateway":
typeOptions = ["villas-node","villas-relay"];
break;
case "service":
typeOptions = ["ems","custom"];
break;
case "equipment":
typeOptions = ["chroma-emulator","chroma-loads","sma-sunnyboy","fleps","sonnenbatterie"];
break;
default:
typeOptions =[];
}
return (
<Dialog show={this.props.show} title="New Infrastructure Component" buttonTitle="Add" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<Dialog show={this.props.show} title="New Infrastructure Component" buttonTitle="Add" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.validateForm()}>
<form>
<FormGroup controlId="managedexternally">
<OverlayTrigger key="3" placement={'left'} overlay={<Tooltip id={`tooltip-${"me"}`}>An externally managed component will show up in the list only after a VILLAScontroller for the component type has created the component and cannot be edited by users</Tooltip>} >
<FormCheck type={"checkbox"} label={"Managed externally"} defaultChecked={this.state.managedexternally} onChange={e => this.handleChange(e)}>
</FormCheck>
</OverlayTrigger>
</FormGroup>
<FormGroup controlId="name" valid={this.validateForm('name')}>
<FormLabel>Name</FormLabel>
<OverlayTrigger key="0" placement={'right'} overlay={<Tooltip id={`tooltip-${"required"}`}> Required field </Tooltip>} >
<FormLabel>Name *</FormLabel>
</OverlayTrigger>
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="host">
<FormLabel>Host</FormLabel>
<FormControl type="text" placeholder="Enter host" value={this.state.host} onChange={(e) => this.handleChange(e)} />
<FormGroup controlId="category" valid={this.validateForm('category')}>
<OverlayTrigger key="1" placement={'right'} overlay={<Tooltip id={`tooltip-${"required"}`}> Required field </Tooltip>} >
<FormLabel>Category of component *</FormLabel>
</OverlayTrigger>
<FormControl as="select" value={this.state.category} onChange={(e) => this.handleChange(e)}>
<option default>Select category</option>
<option>simulator</option>
<option>controller</option>
<option>service</option>
<option>gateway</option>
<option>equipment</option>
</FormControl>
</FormGroup>
<FormGroup controlId="type" valid={this.validateForm('type')}>
<OverlayTrigger key="3" placement={'right'} overlay={<Tooltip id={`tooltip-${"required"}`}> Required field </Tooltip>} >
<FormLabel>Type of component *</FormLabel>
</OverlayTrigger>
<FormControl as="select" value={this.state.type} onChange={(e) => this.handleChange(e)}>
<option default>Select type</option>
{typeOptions.map((name,index) => (
<option key={index}>{name}</option>
))}
</FormControl>
</FormGroup>
<FormGroup controlId="websocketurl">
<FormLabel>Websocket URL</FormLabel>
<FormControl type="text" placeholder="Enter Websocket URL" value={this.state.websocketurl} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="category">
<FormLabel>Category of component (e.g. Simulator, Gateway, ...)</FormLabel>
<FormControl type="text" placeholder="Enter category" value={this.state.category} onChange={(e) => this.handleChange(e)} />
<FormGroup controlId="apiurl">
<FormLabel>API URL</FormLabel>
<FormControl type="text" placeholder="Enter API URL" value={this.state.apiurl} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="type">
<FormLabel>Type of component (e.g. RTDS, VILLASnode, ...)</FormLabel>
<FormControl type="text" placeholder="Enter type" value={this.state.type} onChange={(e) => this.handleChange(e)} />
<FormGroup controlId="location">
<FormLabel>Location</FormLabel>
<FormControl type="text" placeholder="Enter Location" value={this.state.location} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="description">
<FormLabel>Description</FormLabel>
<FormControl type="text" placeholder="Enter Description" value={this.state.description} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="uuid" valid={this.validateForm('uuid')}>

View file

@ -44,6 +44,7 @@ import EditSignalMapping from "../signal/edit-signal-mapping";
import FileStore from "../file/file-store"
import WidgetStore from "../widget/widget-store";
import { Redirect } from 'react-router-dom';
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
class Scenario extends React.Component {
@ -494,6 +495,43 @@ class Scenario extends React.Component {
// TODO do we need this if the dispatches happen in the dialog?
}
signalsAutoConf(index){
let componentConfig = this.state.configs[index];
// determine apiurl of infrastructure component
let ic = this.state.ics.find(ic => ic.id === componentConfig.icID)
if(!ic.type.includes("villas-node")){
let message = "Cannot autoconfigure signals for IC type " + ic.type + " of category " + ic.category + ". This is only possible for gateway ICs of type 'VILLASnode'."
console.warn(message);
const SIGNAL_AUTOCONF_WARN_NOTIFICATION = {
title: 'Failed to load signal config for IC ' + ic.name,
message: message,
level: 'warning'
};
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_WARN_NOTIFICATION);
return;
}
let splitWebsocketURL = ic.websocketurl.split("/")
AppDispatcher.dispatch({
type: 'signals/start-autoconfig',
url: ic.apiurl+"/nodes",
socketname: splitWebsocketURL[splitWebsocketURL.length -1],
token: this.state.sessionToken,
configID: componentConfig.id
});
}
uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
// eslint-disable-next-line
var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/* ##############################################
* File modification methods
############################################## */
@ -534,12 +572,12 @@ class Scenario extends React.Component {
console.warn("There is more than one CIM file selected in this component configuration. I will open them all in a separate tab.")
}
let base_host = 'aaa.bbb.ccc.ddd/api/v2/files/'
let baseURL = 'aaa.bbb.ccc.ddd/api/v2/files/'
for (let file of files) {
// endpoint param serves for download and upload of CIM file, token is required for authentication
let params = {
token: this.state.sessionToken,
endpoint: base_host + String(file.id),
endpoint: baseURL + String(file.id),
}
// TODO start Pintura for editing CIM/ XML file from here
@ -621,6 +659,11 @@ class Scenario extends React.Component {
editButton
onEdit={index => this.setState({ editInputSignalsModal: true, modalConfigData: this.state.configs[index], modalConfigIndex: index })}
/>
<TableColumn
title='Signal AutoConf'
exportButton
onExport={(index) => this.signalsAutoConf(index)}
/>
<TableColumn title='Infrastructure Component' dataKey='icID' modifier={(icID) => this.getICName(icID)} />
<TableColumn
title=''

View file

@ -17,6 +17,7 @@
import ArrayStore from '../common/array-store';
import SignalsDataManager from './signals-data-manager'
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
class SignalStore extends ArrayStore{
constructor() {
@ -26,10 +27,33 @@ class SignalStore extends ArrayStore{
reduce(state, action) {
switch (action.type) {
case 'signals/added':
SignalsDataManager.reloadConfig(action.token, action.data);
this.dataManager.reloadConfig(action.token, action.data);
return super.reduce(state, action);
case 'signals/removed':
SignalsDataManager.reloadConfig(action.token, action.data);
this.dataManager.reloadConfig(action.token, action.data);
return super.reduce(state, action);
case 'signals/start-autoconfig':
this.dataManager.startAutoConfig(action.url, action.socketname, action.token, action.configID)
return super.reduce(state, action);
case 'signals/autoconfig-loaded':
console.log("AutoConfig Loaded: ", action.data)
this.dataManager.saveSignals(action.data, action.token, action.configID, action.socketname);
return super.reduce(state, action);
case 'signals/autoconfig-error':
if (action.error && !action.error.handled && action.error.response) {
const SIGNAL_AUTOCONF_ERROR_NOTIFICATION = {
title: 'Failed to load signal config ',
message: action.error.response.body.message,
level: 'error'
};
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_ERROR_NOTIFICATION);
}
return super.reduce(state, action);
default:

View file

@ -18,6 +18,7 @@
import RestDataManager from '../common/data-managers/rest-data-manager';
import RestAPI from "../common/api/rest-api";
import AppDispatcher from "../common/app-dispatcher";
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
class SignalsDataManager extends RestDataManager{
@ -36,6 +37,152 @@ class SignalsDataManager extends RestDataManager{
}
startAutoConfig(url, socketname, token, configID){
// This function queries the VILLASnode API to obtain the configuration of the VILLASnode located at url
// Endpoint: http[s]://server:port/api/v1 (to be generated based on IC API URL, port 4000)
// data contains the request data: { action, id, (request)}
// See documentation of VILLASnode API: https://villas.fein-aachen.org/doc/node-dev-api-node.html
RestAPI.get(url, null).then(response => {
AppDispatcher.dispatch({
type: 'signals/autoconfig-loaded',
data: response,
token: token,
socketname: socketname,
configID: configID
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'signals/autoconfig-error',
error: error
})
})
}
saveSignals(nodes, token, configID, socketname){
if(nodes.length === 0){
const SIGNAL_AUTOCONF_ERROR_NOTIFICATION = {
title: 'Failed to load nodes ',
message: 'VILLASnode returned empty response',
level: 'error'
};
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_ERROR_NOTIFICATION);
return;
}
let configured = false;
let error = false;
for(let nodeConfig of nodes){
console.log("parsing node config: ", nodeConfig)
if(!nodeConfig.hasOwnProperty("name")){
console.warn("Could not parse the following node config because it lacks a name parameter:", nodeConfig);
} else if(nodeConfig.name === socketname){
if(configured){
const SIGNAL_AUTOCONF_WARNING_NOTIFICATION = {
title: 'There might be a problem with the signal auto-config',
message: 'VILLASnode returned multiple node configurations for the websocket ' + socketname + '. This is a problem of the VILLASnode.',
level: 'warning'
};
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_WARNING_NOTIFICATION);
continue;
}
// signals are not yet configured:
let index_in = 1
let index_out = 1
if(!nodeConfig.in.hasOwnProperty("signals")){
const SIGNAL_AUTOCONF_ERROR_NOTIFICATION = {
title: 'Failed to load in signal config ',
message: 'No field for in signals contained in response.',
level: 'error'
};
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_ERROR_NOTIFICATION);
error = true;
} else{
// add all in signals
for(let inSig of nodeConfig.in.signals) {
if (inSig.enabled) {
console.log("adding input signal:", inSig);
let newSignal = {
configID: configID,
direction: 'in',
name: inSig.hasOwnProperty("name") ? inSig.name : "in_" + String(index_in),
unit: inSig.hasOwnProperty("unit") ? inSig.unit : '-',
index: index_in,
scalingFactor: 1.0
};
AppDispatcher.dispatch({
type: 'signals/start-add',
data: newSignal,
token: token
});
index_in++;
}
}
}
if(!nodeConfig.out.hasOwnProperty("signals")){
const SIGNAL_AUTOCONF_ERROR_NOTIFICATION = {
title: 'Failed to load out signal config ',
message: 'No field for out signals contained in response.',
level: 'error'
};
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_ERROR_NOTIFICATION);
error=true;
}else {
// add all out signals
for (let outSig of nodeConfig.out.signals) {
if (outSig.enabled) {
console.log("adding output signal:", outSig);
let newSignal = {
configID: configID,
direction: 'out',
name: outSig.hasOwnProperty("name") ? outSig.name : "out_" + String(index_out),
unit: outSig.hasOwnProperty("unit") ? outSig.unit : '-',
index: index_out,
scalingFactor: 1.0
};
AppDispatcher.dispatch({
type: 'signals/start-add',
data: newSignal,
token: token
});
index_out++;
}
}
}
console.log("Configured", index_in-1, "input signals and", index_out-1, "output signals");
configured=true;
} else {
console.log("ignoring node with name ",nodeConfig.name, " expecting ", socketname )
}
}
if(!error) {
const SIGNAL_AUTOCONF_INFO_NOTIFICATION = {
title: 'Signal configuration loaded successfully.',
message: '',
level: 'info'
};
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_INFO_NOTIFICATION);
}
}
}
export default new SignalsDataManager()