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 'master' into react-update and more package updates (WIP)

# Conflicts:
#	package-lock.json
#	package.json
#	src/dashboard/dashboard-button-group.js
#	src/dashboard/dashboard.js
This commit is contained in:
Sonja Happ 2021-03-04 15:22:17 +01:00
commit fc0c535d43
72 changed files with 5152 additions and 2516 deletions

1
.gitignore vendored
View file

@ -17,3 +17,4 @@ yarn-debug.log*
yarn-error.log*
.vscode/
*.code-workspace
package-lock.json

View file

@ -1,7 +1,8 @@
variables:
GIT_SUBMODULE_STRATEGY: normal
DOCKER_TAG: ${CI_COMMIT_SHORT_SHA}
DOCKER_TAG: ${CI_COMMIT_BRANCH}
DOCKER_IMAGE: ${CI_REGISTRY_IMAGE}
FF_GITLAB_REGISTRY_HELPER_IMAGE: 1
cache:
untracked: true
@ -42,8 +43,5 @@ deploy:
--dockerfile ${CI_PROJECT_DIR}/Dockerfile
--destination ${DOCKER_IMAGE}:${DOCKER_TAG}
--snapshotMode=redo
--cache=true
--cache-ttl=12h
only:
refs:
- master
dependencies:
- build

View file

@ -2,39 +2,15 @@
[![pipeline status](https://git.rwth-aachen.de/acs/public/villas/web/badges/master/pipeline.svg)](https://git.rwth-aachen.de/acs/public/villas/web/-/commits/master)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
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.
VILLASweb is a tool to configure real-time co-simulations and display simulation real-time data in the web browser.
## 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 the file `package.json`.
## Data model
![Datamodel](src/img/datamodel.png)
## Quick start
```bash
$ 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)
More details on the setup and usage of VILLASweb is available [in the VILLAS documentation](https://villas.fein-aachen.org/doc/web.html).
## Copyright
@ -70,7 +46,7 @@ For other licensing options please consult [Prof. Antonello Monti](mailto:amonti
[![EONERC ACS Logo](doc/pictures/eonerc_logo.png)](http://www.acs.eonerc.rwth-aachen.de)
- Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
- Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
- Sonja Happ <sonja.happ@eonerc.rwth-aachen.de>
[Institute for Automation of Complex Power Systems (ACS)](http://www.acs.eonerc.rwth-aachen.de)

View file

@ -1,82 +0,0 @@
# 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 `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.
6. Enjoy what you see.

View file

@ -1,12 +0,0 @@
# Requirements {#web-requirements}
## 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
- [Docker](https://www.docker.com/): Container management system
## Additional requirements for productive use
- [NGinX](https://www.nginx.com/): Webserver and reverse proxy for backends

View file

@ -1,79 +0,0 @@
# VILLASweb data structure {#web-datastructure}
This document describes how data (scenarios, infrastructure components, users etc., not only live data) is structured in VILLASweb.
## Data model
![Datamodel](../src/img/datamodel.png)
VILLASweb features the following data classes:
- Users
- Infrastructure Components
- Scenarios
* Component Configurations and Signals
* Dashboards and Widgets
* Files
### Users
- You need a username and a password to authenticate in VILLASweb
- There exist three categories of users: Guest, User, and Admin
- Guests have only read access and cannot modify anything
- Users are normal users, they have access to their scenarios, can see available infrastructure components, and modify their accounts (except for their role)
- Admin users have full access to everything, they are the only users that can create new users or change the role of existing users. Only admin users can add or modify infrastructure components.
### Infrastructure Components
- Components of research infrastructure
- Category: for example simulator, gateway, amplifier, database, etc.
- Type: for example RTDS, OpalRT, VILLASnode, Cassandra
- Can only be added/ modified by admin users
### Scenarios
- A collection of component configurations, dashboards, and files for a specific experiment
- Users can have access to multiple scenarios
- Users can be added to and removed from scenarios
### Component Configurations and Signals
- Configure an infrastructure component for the use in a specific scenario
- Input signals: Signals that can be modified in VILLASweb
- Output signals: Signals that can be visualized on dashboards of VILLASweb
- Parameters: Additional configuration parameters of the infrastructure component
- Signals are the actual live data that is displayed or modified through VILLASweb dashboards
### Dashboards and Widgets
- Visualize ongoing experiments in real-time
- Interact with ongoing experiments in real-time
- Use widgets to design the dashboard according to the needs
### Files
- Files can be added to scenarios optionally
- Can be images, model files, CIM xml files
- Can be used in widgets or component configurations
## Setup strategy
The easiest way to start from scratch is the following (assuming the infrastructure components are already configured by an admin user, see below):
1. Create a new scenario.
2. Create and configure a new component configuration and link it with an available infrastructure component.
3. Configure the input and output signals of the component configuration according to the signals provided by the selected infrastructure component. The number of signals and their order (index starting at 1) must match.
4. Create a new dashboard and add widgets as desired. Configure the widgets by right-clicking to open the edit menu
5. If needed, files can be added to the scenario and used by component configurations or widgets (models, images, CIM-files, etc.)
6. For collaboration with other users, users can be added to a scenario
### Setup of infrastructure components
In the "Infrastructure Components" menu point admin users can create and edit components to be used in experiments. Normal uses can view the available components, but not edit them.
The components are global at any time and are shared among all users of VILLASweb.
To create a new infrastructure component, you need to provide:
- Name
- Category (see above for examples)
- Type (see above for examples)
- Location
- Host (network address of the component)
At the moment, you need to know the input and output signals of the infrastructure component a priori to be able to create compatible component configurations by hand.
An auto-detection mechanism for signals is planned for future releases.
> Hint: At least one infrastructure component is required to receive data in VILLASweb.

View file

@ -1,70 +0,0 @@
# Development {#web-development}
- @subpage web-datastructure
In order to get started with VILLASweb, you might also want to check our our [demo project](https://git.rwth-aachen.de/acs/public/villas/Demo) which is simple to setup using Docker Compose.
## Frontend
### Description
The website itself based on the [React JavaScript framework](https://reactjs.org/) and the [Flux library](https://facebook.github.io/flux/).
### Required
- [NodeJS with npm](https://nodejs.org/en/)
### Setup
- `git clone git@git.rwth-aachen.de/acs/public/villas/web.git` to copy the project on your computer
- `cd VILLASweb`
- `npm install`
### Running
- `npm start`
This runs the development server for the website on your local computer at port 3000.
The backend must be running to make the website work.
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 database.
### Required
- [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
- `git clone git@git.rwth-aachen.de/acs/public/villas/web-backend-go.git` to copy the project on your computer
- `cd VILLASweb-backend-go`
- `go mod tidy`
- `go run start.go [params]`
To obtain a list of available parameters use `go run start.go --help`.
To run the tests use `go test $(go list ./... ) -p 1` in the top-level folder of the repo.
Running the backend will only work if the PostgreSQL database is setup properly. Otherwise, you will get error messages.
### Auto-generate the API documentation
The documentation of the VILLASweb API in the OpenAPI format can be auto-generated from the source code documentation using the tool swag.
To do this run the following in the top-level folder of the repo:
- `go mod tidy`
- `go install github.com/swaggo/swag/cmd/swag`
- `swag init -p pascalcase -g "start.go" -o "./doc/api/"`
The `.yaml` and `.json` files in OpenAPI swagger format are created in the output folder `doc/api`.
### PostgreSQL database setup
Please check the [Readme file in the backend repository](https://git.rwth-aachen.de/acs/public/villas/web-backend-go) for some useful hints on the local setup of the PostreSQL database.

3940
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,15 +3,16 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@createnl/grouped-checkboxes": "^1.1.2",
"@fortawesome/fontawesome-svg-core": "^1.2.34",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/react-fontawesome": "^0.1.14",
"babel-runtime": "^6.26.0",
"bootstrap": "^4.5.3",
"bootstrap": "^4.6.0",
"bufferutil": "^4.0.3",
"canvas": "^2.6.1",
"canvas": "^2.7.0",
"classnames": "^2.2.6",
"d3-array": "^2.9.1",
"d3-array": "^2.11.0",
"d3-axis": "^2.0.0",
"d3-scale": "^3.2.3",
"d3-scale-chromatic": "^2.0.0",
@ -21,52 +22,54 @@
"es6-promise": "^4.2.8",
"fibers": "^5.0.0",
"file-saver": "^2.0.5",
"flux": "^4.0.0",
"flux": "^4.0.1",
"gaugeJS": "^1.3.7",
"handlebars": "^4.7.6",
"jquery": "^3.5.1",
"jszip": "^3.5.0",
"handlebars": "^4.7.7",
"jquery": "^3.6.0",
"jszip": "^3.6.0",
"libcimsvg": "git+https://git.rwth-aachen.de/acs/public/cim/pintura-npm-package.git",
"lodash": "^4.17.20",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"multiselect-react-dropdown": "^1.6.3",
"moment-duration-format": "^2.3.2",
"multiselect-react-dropdown": "^1.6.11",
"node-sass": "^5.0.0",
"popper.js": "^1.16.1",
"prop-types": "^15.7.2",
"rc-slider": "^9.7.1",
"react": "^17.0.1",
"react-bootstrap": "^1.4.3",
"react-bootstrap": "^1.5.1",
"react-bootstrap-time-picker": "^2.0.1",
"react-collapse": "^5.1.0",
"react-color": "^2.19.3",
"react-contexify": "^5.0.0",
"react-d3": "^0.4.0",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dnd": "^13.1.1",
"react-dnd-html5-backend": "^12.1.1",
"react-dom": "^17.0.1",
"react-full-screen": "^0.3.2-0",
"react-full-screen": "^1.0.1",
"react-fullscreenable": "^2.5.1-0",
"react-grid-system": "^7.1.1",
"react-json-view": "^1.19.1",
"react-json-view": "^1.21.1",
"react-notification-system": "^0.4.0",
"react-rnd": "^10.2.4",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.1",
"react-scripts": "^4.0.3",
"react-svg-pan-zoom": "^3.10.0",
"react-trafficlight": "^5.2.1",
"sass": "^1.32.2",
"sass": "^1.32.8",
"superagent": "^6.1.0",
"swagger-ui-react": "^3.44.0",
"ts-node": "^9.1.1",
"type-fest": "^0.20.2",
"typescript": "^4.1.3",
"type-fest": "^0.21.2",
"typescript": "^4.2.2",
"utf-8-validate": "^5.0.4",
"validator": "^13.5.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-plugin-serve": "^1.2.1"
"webpack-plugin-serve": "^1.3.0"
},
"devDependencies": {
"chai": "^4.2.0"
"chai": "^4.3.3"
},
"scripts": {
"start": "react-scripts start",

View file

@ -38,6 +38,7 @@ import Scenarios from './scenario/scenarios';
import Scenario from './scenario/scenario';
import Users from './user/users';
import User from './user/user';
import APIBrowser from './common/api-browser';
import './styles/app.css';
@ -45,6 +46,11 @@ class App extends React.Component {
constructor(props) {
super(props);
AppDispatcher.dispatch({
type: 'config/load',
});
this.state = {
showSidebarMenu: false,
}
@ -117,6 +123,7 @@ class App extends React.Component {
<Route path="/infrastructure" component={InfrastructureComponents} />
<Route path="/account" component={User} />
<Route path="/users" component={Users} />
<Route path="/api" component={APIBrowser} />
</div>
</div>

82
src/common/api-browser.js Normal file
View file

@ -0,0 +1,82 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import SwaggerUI from 'swagger-ui-react'
import 'swagger-ui-react/swagger-ui.css'
import '../styles/swagger-ui.css';
import RestAPI from './api/rest-api';
import RestDataManager from './data-managers/rest-data-manager';
class APIBrowser extends React.Component {
constructor(props) {
super(props);
this.state = {
'spec': null
};
}
mangleSpec(spec) {
spec.host = window.location.host;
return spec;
}
componentWillMount() {
this._asyncRequest = RestAPI.get('/api/v2/openapi')
.then((spec) => {
this._asyncRequest = null;
this.setState({
'spec': this.mangleSpec(spec)
});
});
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
requestInterceptor(req) {
var token = localStorage.getItem('token');
if (token)
req.headers.Authorization = 'Bearer ' + token;
return req
}
render() {
return (
<div>
{ this.state.spec &&
<SwaggerUI
spec={this.state.spec}
tryItOutEnabled={true}
requestInterceptor={this.requestInterceptor}
/> }
</div>
);
}
}
export default APIBrowser;

View file

@ -26,10 +26,13 @@ import NotificationsFactory from "../data-managers/notifications-factory";
function isNetworkError(err, url) {
let result = false;
// If not status nor response fields, it is a network error. TODO: Handle timeouts
if (err.status == null || err.status === 500 || err.response == null) {
result = true;
if (err.status === 500 && err.response != null){
let notification = NotificationsFactory.INTERNAL_SERVER_ERROR(err.response)
NotificationsDataManager.addNotification(notification);
} else if (err.status == null || err.status === 500 || err.response == null) {
// If not status nor response fields, it is a network error. TODO: Handle timeouts
result = true;
let notification = err.timeout? NotificationsFactory.REQUEST_TIMEOUT : NotificationsFactory.SERVER_NOT_REACHABLE(url);
NotificationsDataManager.addNotification(notification);
}
@ -41,7 +44,8 @@ let prevURL = null;
class RestAPI {
get(url, token) {
return new Promise(function (resolve, reject) {
var req = request.get(url);
let req = request.get(url);
if (token != null) {
req.set('Authorization', "Bearer " + token);
@ -61,7 +65,7 @@ class RestAPI {
post(url, body, token) {
return new Promise(function (resolve, reject) {
var req = request.post(url).send(body).timeout({ response: 5000 }); // Simple response start timeout (3s)
let req = request.post(url).send(body).timeout({ response: 5000 }); // Simple response start timeout (3s)
if (token != null) {
req.set('Authorization', "Bearer " + token);
@ -82,7 +86,7 @@ class RestAPI {
delete(url, token) {
return new Promise(function (resolve, reject) {
var req = request.delete(url);
let req = request.delete(url);
if (token != null) {
req.set('Authorization', "Bearer " + token);
@ -101,7 +105,7 @@ class RestAPI {
put(url, body, token) {
return new Promise(function (resolve, reject) {
var req = request.put(url).send(body);
let req = request.put(url).send(body);
if (token != null) {
req.set('Authorization', "Bearer " + token);
@ -140,11 +144,14 @@ class RestAPI {
download(url, token, fileID) {
return new Promise(function (resolve, reject) {
let req = request.get(url + "/" + fileID).buffer(true).responseType("blob")
// use blob response type and buffer
if (token != null) {
req.set('Authorization', "Bearer " + token);
let completeURL = url + "/" + fileID;
if (token != null){
completeURL = completeURL + "?token=" + token
}
let req = request.get(completeURL).buffer(true).responseType("blob")
// use blob response type and buffer
// Do not use auth header for file download
req.end(function (error, res) {
if (error !== null || res.status !== 200) {
@ -159,6 +166,28 @@ class RestAPI {
});
}
apiDownload(url, token) {
return new Promise(function (resolve, reject) {
let req = request.get(url).buffer(true).responseType("blob");
if (token != null) {
req.set('Authorization', "Bearer " + token);
}
req.end(function (error, res) {
if (res == null || res.status !== 200) {
if (req.url !== prevURL) error.handled = isNetworkError(error);
prevURL = req.url;
reject(error);
} else {
let parts = url.split("/");
resolve({data: res.body, type: res.type, id: parts[parts.length-1]})
}
});
});
}
}
export default new RestAPI();

View file

@ -42,6 +42,14 @@ class NotificationsFactory {
};
}
static INTERNAL_SERVER_ERROR(response) {
return {
title: 'Internal server error',
message: response.message,
level: 'error'
};
}
static ADD_ERROR(message) {
return {
title: "Add Error",
@ -130,6 +138,14 @@ class NotificationsFactory {
};
}
static ACTION_INFO() {
return {
title: 'Action successfully requested',
level: 'info'
};
}
}
export default NotificationsFactory;

View file

@ -37,12 +37,14 @@ 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>
<FormLabel size="sm">The IC will be deleted if the respective manager sends "gone" state and no component config is using the IC anymore</FormLabel>
</Collapse>
</Modal.Body>
<Modal.Footer>
<Button onClick={() => this.props.onClose(false)}>Cancel</Button>
<span className='solid-button'>
<Button variant='secondary' onClick={() => this.props.onClose(false)}>Cancel</Button>
</span>
<Button variant="danger" onClick={() => this.props.onClose(true)}>Delete</Button>
</Modal.Footer>
</Modal>;

View file

@ -45,6 +45,11 @@ class Dialog extends React.Component {
}
render() {
const buttonStyle = {
marginLeft: '10px'
};
return (
<Modal size={this.props.size || 'sm'} keyboard show={this.props.show} onEnter={this.props.onReset} onHide={this.cancelModal} onKeyPress={this.onKeyPress}>
<Modal.Header>
@ -56,8 +61,10 @@ class Dialog extends React.Component {
</Modal.Body>
<Modal.Footer>
{this.props.blendOutCancel? <div></div>: <Button onClick={this.cancelModal}>Cancel</Button>}
<Button onClick={this.closeModal} disabled={!this.props.valid}>{this.props.buttonTitle}</Button>
<span className='solid-button'>
{this.props.blendOutCancel? <div/>: <Button variant='secondary' onClick={this.cancelModal} style={buttonStyle}>Cancel</Button>}
<Button variant='secondary' onClick={this.closeModal} disabled={!this.props.valid} style={buttonStyle}>{this.props.buttonTitle}</Button>
</span>
</Modal.Footer>
</Modal>
);

View file

@ -18,28 +18,10 @@
import React from 'react';
import config from '../config';
import {Redirect} from "react-router-dom";
import { Redirect } from "react-router-dom";
import { NavLink } from 'react-router-dom';
class Home extends React.Component {
constructor(props) {
super(props);
// create url for API documentation, distinguish between localhost and production deployment
let docs_url = "";
let docs_location = "/swagger/index.html";
let base_url = window.location.origin;
if (base_url.search("localhost") === -1){
docs_url = base_url + docs_location;
} else {
// useful for local testing, replace port 3000 with port 4000 (port of backend)
docs_url = base_url.replace("3000", "4000") + docs_location;
}
this.state = {
docs_url: docs_url
};
}
getCounts(type) {
if (this.state.hasOwnProperty('counts'))
@ -61,18 +43,15 @@ class Home extends React.Component {
<img style={{height: 120, float: 'right'}} src={require('../img/villas_web.svg').default} alt="Logo VILLASweb" />
<h1>Home</h1>
<p>
Welcome to <b>{config.instance}</b> hosted by <a href={"mailto:" + config.admin.mail}>{config.admin.name}</a>!<br />
{/*Welcome to <b>{config.instance}</b> hosted by <a href={"mailto:" + config.admin.mail}>{config.admin.name}</a>!<br />*/}
Welcome to <b>{config.instance}</b>!
</p>
<p>
You are logged in as user <b>{currentUser.username}</b> with <b>ID {currentUser.id}</b> and role <b>{currentUser.role}</b>.
</p>
<p>
An interactive documentation of the VILLASweb API is available <a href={this.state.docs_url} target="_blank" rel="noopener noreferrer">here</a>.
</p>
<h3>Data Model</h3>
{/*<h3>Data Model</h3>
<img height={400} src={require('../img/datamodel.png').default} alt="Datamodel VILLASweb" />
<h3>Terminology </h3>
@ -101,34 +80,47 @@ class Home extends React.Component {
<ul>
<li>A collection of component configurations and dashboards for a specific experiment</li>
<li>Users can have access to multiple scenarios</li>
</ul>
</ul>*/}
<h3>Credits</h3>
<p>VILLASweb is developed by the <a href="http://acs.eonerc.rwth-aachen.de">Institute for Automation of Complex Power Systems</a> at the <a href="https;//www.rwth-aachen.de">RWTH Aachen University</a>.</p>
<p>VILLASweb is an open source project developed by the <a href="http://acs.eonerc.rwth-aachen.de">Institute for Automation of Complex Power Systems</a> at <a href="https;//www.rwth-aachen.de">RWTH Aachen University</a>.</p>
<img height={60} src={require('../img/eonerc_rwth.svg').default} alt="Logo ACS" />
<ul>
<li><a href="mailto:stvogel@eonerc.rwth-aachen.de">Steffen Vogel</a></li>
<li><a href="mailto:sonja.happ@eonerc.rwth-aachen.de">Sonja Happ</a></li>
</ul>
<h3>Links</h3>
<ul>
<li><a href="http://fein-aachen.org/projects/villas-framework/">Project Page</a></li>
<li><a href="https://villas.fein-aachen.org/doc/web.html">Documentation</a></li>
<li><a href="https://git.rwth-aachen.de/acs/public/villas/web">Source Code</a></li>
<li><NavLink to="/api">VILLASweb API browser</NavLink></li>
<li><a href="http://fein-aachen.org/projects/villas-framework/">FEIN Aachen e.V. project page of VILLASframework</a></li>
<li><a href="https://villas.fein-aachen.org/doc/web.html">Documentation of VILLASweb</a></li>
<li><a href="https://git.rwth-aachen.de/acs/public/villas/web">Source Code of VILLASweb frontend</a></li>
<li><a href="https://git.rwth-aachen.de/acs/public/villas/web-backend-go">Source Code of VILLASweb backend</a></li>
</ul>
<h3>Funding</h3>
<p>The development of <a href="http://fein-aachen.org/projects/villas-framework/">VILLASframework</a> projects have received funding from</p>
<p>The development of <a href="http://fein-aachen.org/projects/villas-framework/">VILLASframework</a> projects has received funding from</p>
<ul>
<li><a href="http://www.acs.eonerc.rwth-aachen.de/cms/E-ON-ERC-ACS/Forschung/Forschungsprojekte/Gruppe-Real-Time-Simulation-and-Hardware/~qxvw/Urban-Energy-Lab-4/">Urban Energy Lab 4.0</a> a project funded by OP EFRE NRW (European Regional Development Fund) for the setup of a novel energy research infrastructure.</li>
<li><a href="http://www.re-serve.eu">RESERVE</a> a European Unions Horizon 2020 research and innovation programme under grant agreement No 727481</li>
<li><a href="http://www.jara.org/en/research/energy">JARA-ENERGY</a>. Jülich-Aachen Research Alliance (JARA) is an initiative of RWTH Aachen University and Forschungszentrum Jülich.</li>
<p>SLEW: Second Life for Energiewende, an Exploratory Teaching Space project funded by RWTH Aachen University</p>
<p><a href="https://erigrid2.eu/"> ERIgrid 2.0: </a> An EU Horizon 2020 research and innovation action project for connecting European Smart Grid Infrastructures (grant agreement No 870620)</p>
<p>
<img height={100} src={require('../img/european_commission.svg').default} alt="Logo EU" />
<img height={70} src={require('../img/erigrid2.png').default} alt="Logo ERIgrid 2.0" />
</p>
<p><a href="http://www.uel4-0.de/">Urban Energy Lab 4.0:</a> A project funded by EFRE.NRW (European Regional Development Fund) for the setup of a novel energy research infrastructure.</p>
<p>
<img height={70} src={require('../img/uel_efre.jpeg').default} alt="Logo UEL OP EFRE NRW" />
<img height={70} src={require('../img/uel.png').default} alt="Logo UEL" />
</p>
<p><a href="http://www.re-serve.eu">RESERVE:</a> An EU Horizon 2020 research and innovation project (grant agreement No 727481)</p>
<p>
<img height={100} src={require('../img/european_commission.svg').default} alt="Logo EU" />
<img height={70} src={require('../img/reserve.svg').default} alt="Logo RESERVE" />
</p>
<p><a href="http://www.jara.org/en/research/energy">JARA-ENERGY:</a> Jülich-Aachen Research Alliance (JARA) is an initiative of RWTH Aachen University and Forschungszentrum Jülich.</p>
</ul>
<img height={100} src={require('../img/european_commission.svg').default} alt="Logo EU" />
<img height={70} src={require('../img/reserve.svg').default} alt="Logo EU" />
<img height={70} src={require('../img/uel_efre.jpeg').default} alt="Logo UEL OP EFRE NRW" />
<img height={70} src={require('../img/uel.png').default} alt="Logo UEL" />
<img height={60} src={require('../img/eonerc_rwth.svg').default} alt="Logo ACS" />
{
//<img height={70} src={require('../img/jara.svg').default} alt="Logo JARA" />
}

View file

@ -28,7 +28,7 @@ library.add(fas);
class Icon extends React.Component {
render() {
return <FontAwesomeIcon size={this.props.size} style={this.props.style} icon={this.props.icon} />
return <FontAwesomeIcon className={this.props.classname} size={this.props.size} style={this.props.style} icon={this.props.icon} />
}
}

View file

@ -17,9 +17,73 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { Container } from 'flux/utils';
import LoginStore from '../user/login-store';
import AppDispatcher from './app-dispatcher';
class SidebarMenu extends React.Component {
constructor(props) {
super(props);
this.state = {
externalAuth: false,
logoutLink: "",
}
}
static getStores() {
return [LoginStore]
}
static calculateState(prevState, props) {
let config = LoginStore.getState().config;
let logout_url = _.get(config, ['authentication', 'logout_url']);
if (logout_url) {
return {
externalAuth: true,
logoutLink: logout_url,
}
}
return {
externalAuth: false,
logoutLink: "/logout",
}
}
logout() {
AppDispatcher.dispatch({
type: 'users/logout'
});
// The Login Store is deleted automatically
// discard login token and current User
localStorage.setItem('token', '');
localStorage.setItem('currentUser', '');
}
render() {
if (this.state.externalAuth) {
return (
<div className="menu-sidebar">
<h2>Menu</h2>
<ul>
<li><NavLink to="/home" activeClassName="active" title="Home">Home</NavLink></li>
<li><NavLink to="/scenarios" activeClassName="active" title="Scenarios">Scenarios</NavLink></li>
<li><NavLink to="/infrastructure" activeClassName="active" title="Infrastructure Components">Infrastructure Components</NavLink></li>
{ this.props.currentRole === 'Admin' ?
<li><NavLink to="/users" activeClassName="active" title="User Management">User Management</NavLink></li> : ''
}
<li><NavLink to="/account" title="Account">Account</NavLink></li>
<a onClick={this.logout.bind(this)} href={this.state.logoutLink}>Logout</a>
<li><NavLink to="/api" title="API Browser">API Browser</NavLink></li>
</ul>
</div>
);
}
return (
<div className="menu-sidebar">
<h2>Menu</h2>
@ -32,11 +96,13 @@ class SidebarMenu extends React.Component {
<li><NavLink to="/users" activeClassName="active" title="User Management">User Management</NavLink></li> : ''
}
<li><NavLink to="/account" title="Account">Account</NavLink></li>
<li><NavLink to="/logout" title="Logout">Logout</NavLink></li>
<li><NavLink to={this.state.logoutLink} title="Logout">Logout</NavLink></li>
<li><NavLink to="/api" title="API Browser">API Browser</NavLink></li>
</ul>
</div>
);
}
}
export default SidebarMenu;
let fluxContainerConverter = require('../common/FluxContainerConverter');
export default Container.create(fluxContainerConverter.convert(SidebarMenu));

View file

@ -23,7 +23,9 @@ class TableColumn extends Component {
modifier: null,
width: null,
editButton: false,
showEditButton: null,
deleteButton: false,
showDeleteButton: null,
exportButton: false,
duplicateButton: false,
link: '/',
@ -35,6 +37,7 @@ class TableColumn extends Component {
labelKey: null,
checkbox: false,
checkboxKey: '',
checkboxDisabled: null,
labelStyle: null,
labelModifier: null

View file

@ -29,16 +29,17 @@ class CustomTable extends Component {
this.state = {
rows: CustomTable.getRows(props),
editCell: [ -1, -1 ]
editCell: [-1, -1]
};
}
static defaultProps = {
width: null
width: null,
checked: true
};
onClick(event, row, column) {
this.setState({ editCell: [ column, row ]}); // x, y
this.setState({ editCell: [column, row] }); // x, y
}
static addCell(data, index, child) {
@ -52,25 +53,46 @@ class CustomTable extends Component {
break;
}
}
} else if ('data' in child.props && 'dataKey' in child.props) {
content = new Map();
let keys = _.get(data, child.props.dataKey);
let filteredData = child.props.data.filter(data => keys.includes(data.id))
filteredData.forEach(file => {
content.set(_.get(file, 'id'), _.get(file, 'name'));
})
} else if ('dataKey' in child.props) {
content = _.get(data, child.props.dataKey);
}
const modifier = child.props.modifier;
if (modifier && content != null) {
content = modifier(content);
content = modifier(content, data);
}
let cell = [];
if (content != null) {
//content = content.toString();
// check if cell should be a link
const linkKey = child.props.linkKey;
if (linkKey && data[linkKey] != null) {
cell.push(<Link to={child.props.link + data[linkKey]}>{content}</Link>);
} else if (child.props.clickable) {
cell.push(<Button variant="link" onClick={() => child.props.onClick(index)}>{content}</Button>);
} else if (linkKey === 'filebuttons') {
content.forEach((contentvalue, contentkey) => {
cell.push(
<OverlayTrigger
key={contentkey}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"export"}`}>Download {contentvalue}</Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onDownload(contentkey)}
disabled={child.props.onDownload == null}>
{contentkey + ' ' }
<Icon icon='file-download' />
</Button>
</OverlayTrigger>);
});
} else {
cell.push(content);
}
@ -90,8 +112,8 @@ class CustomTable extends Component {
cell.push(<span>
&nbsp;
<FormLabel column={false} className={labelStyle}>
{labelContent}
</FormLabel>
{labelContent}
</FormLabel>
</span>
);
}
@ -102,37 +124,133 @@ class CustomTable extends Component {
}
// add buttons
if (child.props.editButton) {
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>);
let showEditButton = true
if (child.props.showEditButton !== null)
{
showEditButton = child.props.showEditButton(index)
}
if(showEditButton){
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>);
}
}
if (child.props.deleteButton) {
cell.push(<OverlayTrigger key={1} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"delete"}`}> Delete </Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onDelete(index)} disabled={child.props.onDelete == null}><Icon icon='trash' /></Button></OverlayTrigger>);
}
if (child.props.checkbox) {
const checkboxKey = child.props.checkboxKey;
cell.push(<FormCheck className="table-control-checkbox" inline checked={checkboxKey ? data[checkboxKey] : null} onChange={e => child.props.onChecked(index, e)} />);
let isDisabled = false;
if (child.props.checkboxDisabled != null){
isDisabled = child.props.checkboxDisabled(index)
}
cell.push(
<FormCheck
className="table-control-checkbox"
inline
disabled = {isDisabled}
checked={checkboxKey ? data[checkboxKey] : null}
onChange={e => child.props.onChecked(data, e)}
/>);
}
if (child.props.exportButton) {
cell.push(<OverlayTrigger key={2} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"export"}`}> Export </Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onExport(index)} disabled={child.props.onExport == null}><Icon icon='download' /></Button></OverlayTrigger>);
cell.push(
<OverlayTrigger
key={1}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"export"}`}> Export </Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onExport(index)}
disabled={child.props.onExport == null}>
<Icon icon='download' />
</Button>
</OverlayTrigger>);
}
if (child.props.duplicateButton) {
cell.push(<OverlayTrigger key={3} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"duplicate"}`}> Duplicate </Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onDuplicate(index)} disabled={child.props.onDuplicate == null}><Icon icon='copy' /></Button></OverlayTrigger>);
cell.push(
<OverlayTrigger
key={2}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"duplicate"}`}> Duplicate </Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onDuplicate(index)}
disabled={child.props.onDuplicate == null}>
<Icon icon='copy' />
</Button>
</OverlayTrigger>);
}
if (child.props.addRemoveFilesButton) {
cell.push(
<OverlayTrigger
key={3}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"export"}`}>Add/remove File(s)</Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onAddRemove(index)}
disabled={child.props.onAddRemove == null}>
<Icon icon='file' />
</Button>
</OverlayTrigger>);
}
if (child.props.downloadAllButton) {
cell.push(
<OverlayTrigger
key={4}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"export"}`}>Download All Files</Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onDownloadAll(index)}
disabled={child.props.onDownloadAll == null}>
<Icon icon='file-download' />
</Button>
</OverlayTrigger>);
}
let showDeleteButton = true;
if (child.props.showDeleteButton !== null){
showDeleteButton = child.props.showDeleteButton(index)
}
if (showDeleteButton){
if (child.props.deleteButton) {
cell.push(
<OverlayTrigger
key={5}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"delete"}`}> Delete </Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onDelete(index)}
disabled={child.props.onDelete == null}>
<Icon icon='trash' />
</Button>
</OverlayTrigger>);
}
}
return cell;
} // addCell
static getDerivedStateFromProps(props, state){
static getDerivedStateFromProps(props, state) {
const rows = CustomTable.getRows(props);
return { rows };
@ -147,12 +265,12 @@ class CustomTable extends Component {
onCellFocus(index) {
// When a cell focus is detected, update the current state in order to uncover the input element
this.setState({ editCell: [ index.cell, index.row ]});
this.setState({ editCell: [index.cell, index.row] });
}
cellLostFocus() {
// Reset cell selection state
this.setState({ editCell: [ -1, -1 ] });
this.setState({ editCell: [-1, -1] });
}
static getRows(props) {
@ -164,7 +282,7 @@ class CustomTable extends Component {
// check if multiple columns
if (Array.isArray(props.children) === false) {
// table only has a single column
return [ CustomTable.addCell(data, index, props.children) ];
return [CustomTable.addCell(data, index, props.children)];
}
const row = [];
@ -181,7 +299,7 @@ class CustomTable extends Component {
// get children
let children = this.props.children;
if (Array.isArray(this.props.children) === false) {
children = [ children ];
children = [children];
}
return (
@ -200,28 +318,38 @@ class CustomTable extends Component {
let isCellInlineEditable = children[cellIndex].props.inlineEditable === true;
let tabIndex = isCellInlineEditable? 0 : -1;
let tabIndex = isCellInlineEditable ? 0 : -1;
let evtHdls = isCellInlineEditable ? {
onCellClick: (event) => this.onClick(event, rowIndex, cellIndex),
onCellFocus: () => this.onCellFocus({cell: cellIndex, row: rowIndex}),
onCellFocus: () => this.onCellFocus({ cell: cellIndex, row: rowIndex }),
onCellBlur: () => this.cellLostFocus()
} : {
onCellClick: () => {},
onCellFocus: () => {},
onCellBlur: () => {}
};
onCellClick: () => { },
onCellFocus: () => { },
onCellBlur: () => { }
};
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={children[cellIndex].props.inputType} value={cell} onChange={(event) => children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} ref={ref => { this.activeInput = ref; }} />
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={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) => (
<span key={elementIndex}>{element}</span>
))}
</span>
)}
<span>
{cell.map((element, elementIndex) => (
<span key={elementIndex}>{element}</span>
))}
</span>
)}
</td>)
})
}

43
src/config-reader.js Normal file
View file

@ -0,0 +1,43 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import RestDataManager from './common/data-managers/rest-data-manager';
import RestAPI from './common/api/rest-api';
import AppDispatcher from './common/app-dispatcher';
class ConfigReader extends RestDataManager {
constructor() {
super('config', '/config');
}
loadConfig() {
RestAPI.get(this.makeURL('/config'), null).then(response => {
AppDispatcher.dispatch({
type: 'config/loaded',
data: response,
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'config/load-error',
error: error,
});
});
}
};
export default new ConfigReader();

View file

@ -22,7 +22,11 @@ const config = {
admin: {
name: 'Institute for Automation of Complex Power Systems (ACS), RWTH Aachen University, Germany',
mail: 'stvogel@eonerc.rwth-aachen.de'
}
},
externalAuth: true,
loginURL: '/oauth2/start?rd=/login/complete',
provider: 'Jupyter',
disableVillasLogin: false,
};
export default config

View file

@ -25,11 +25,10 @@ class DashboardButtonGroup extends React.Component {
const buttonStyle = {
marginLeft: '12px',
height: '44px',
width : '35px'
width : '35px',
};
const iconStyle = {
color: '#007bff',
height: '25px',
width : '25px'
}
@ -37,20 +36,20 @@ class DashboardButtonGroup extends React.Component {
const buttons = [];
let key = 0;
if (this.props.fullscreen) {
/*if (this.props.fullscreen) {
return null;
}
}*/
if (this.props.editing) {
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"save"}`}> Save changes </Tooltip>} >
<Button variant= 'light' size="lg" key={key} onClick={this.props.onSave} style={buttonStyle}>
<Icon icon="save" style={iconStyle} />
<Icon icon="save" classname='icon-color' style={iconStyle} />
</Button>
</OverlayTrigger>,
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"cancel"}`}> Discard changes </Tooltip>} >
<Button key={key} variant= 'light' size="lg" onClick={this.props.onCancel} style={buttonStyle}>
<Icon icon="times" style={iconStyle}/>
<Icon icon="times" classname='icon-color' style={iconStyle}/>
</Button>
</OverlayTrigger>
);
@ -59,17 +58,25 @@ class DashboardButtonGroup extends React.Component {
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"expand"}`}> Change to fullscreen view </Tooltip>} >
<Button key={key} variant= 'light' size="lg" onClick={this.props.onFullscreen} style={buttonStyle}>
<Icon icon="expand" style={iconStyle}/>
<Icon icon="expand" classname='icon-color' style={iconStyle}/>
</Button>
</OverlayTrigger>
);
} else {
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"compress"}`}> Back to normal view </Tooltip>} >
<Button key={key} variant= 'light' size="lg" onClick={this.props.onFullscreen} style={buttonStyle}>
<Icon icon="compress" classname='icon-color' style={iconStyle}/>
</Button>
</OverlayTrigger>
);
}
if (this.props.paused) {
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"play"}`}> Continue simulation </Tooltip>} >
<Button key={key} variant= 'light' size="lg" onClick={this.props.onUnpause} style={buttonStyle}>
<Icon icon="play" style={iconStyle}/>
<Icon icon="play" classname='icon-color' style={iconStyle}/>
</Button>
</OverlayTrigger>
);
@ -77,44 +84,48 @@ class DashboardButtonGroup extends React.Component {
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"pause"}`}> Pause simulation </Tooltip>} >
<Button key={key} variant= 'light' size="lg" onClick={this.props.onPause} style={buttonStyle}>
<Icon icon="pause" style={iconStyle}/>
<Icon icon="pause" classname='icon-color' style={iconStyle}/>
</Button>
</OverlayTrigger>
);
}
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"file"}`}> Add, edit or delete files of scenario </Tooltip>} >
<Button key={key} variant= 'light' size="lg" onClick={this.props.onEditFiles} style={buttonStyle}>
<Icon icon="file" style={iconStyle}/>
</Button>
</OverlayTrigger>
);
if (this.props.fullscreen !== true) {
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"file"}`}> Add, edit or delete files of scenario </Tooltip>}>
<Button key={key} variant='light' size="lg" onClick={this.props.onEditFiles} style={buttonStyle}>
<Icon icon="file" classname='icon-color' style={iconStyle}/>
</Button>
</OverlayTrigger>
);
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"file"}`}> Add, edit or delete input signals </Tooltip>} >
<Button key={key} variant= 'light' size="lg" onClick={this.props.onEditInputSignals} style={buttonStyle}>
<Icon icon="sign-in-alt" style={iconStyle}/>
</Button>
</OverlayTrigger>
);
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"file"}`}> Add, edit or delete input signals </Tooltip>}>
<Button key={key} variant='light' size="lg" onClick={this.props.onEditInputSignals} style={buttonStyle}>
<Icon icon="sign-in-alt" classname='icon-color' style={iconStyle}/>
</Button>
</OverlayTrigger>
);
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"file"}`}> Add, edit or delete output signals </Tooltip>} >
<Button key={key} variant= 'light' size="lg" onClick={this.props.onEditOutputSignals} style={buttonStyle}>
<Icon icon="sign-out-alt" style={iconStyle}/>
</Button>
</OverlayTrigger>
);
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"layout"}`}> Add widgets and edit layout </Tooltip>} >
<Button key={key} variant= 'light' size="lg" onClick={this.props.onEdit} style={buttonStyle}>
<Icon icon="pen" style={iconStyle} />
</Button>
</OverlayTrigger>
);
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"file"}`}> Add, edit or delete output signals </Tooltip>}>
<Button key={key} variant='light' size="lg" onClick={this.props.onEditOutputSignals} style={buttonStyle}>
<Icon icon="sign-out-alt" classname='icon-color' style={iconStyle}/>
</Button>
</OverlayTrigger>
);
buttons.push(
<OverlayTrigger key={key++} placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"layout"}`}> Add widgets and edit layout </Tooltip>}>
<Button key={key} variant='light' size="lg" onClick={this.props.onEdit} style={buttonStyle}>
<Icon icon="pen" classname='icon-color' style={iconStyle}/>
</Button>
</OverlayTrigger>
);
}
}
return <div className='section-buttons-group-right'>

View file

@ -475,14 +475,11 @@ class Dashboard extends Component {
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">
<span>{this.state.dashboard.name}</span>
</div>
return (<div className={boxClasses} >
<div className='section-header box-header'>
<div className="section-title">
<h2>{this.state.dashboard.name}</h2>
</div>
<DashboardButtonGroup
editing={this.state.editing}

View file

@ -73,7 +73,7 @@ class EditDashboardDialog extends React.Component {
return (
<Dialog show={this.props.show} title="Edit Dashboard" buttonTitle="Save" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<form>
<FormGroup controlId="name" validationstate={this.validateForm('name')}>
<FormGroup controlId="name" valid={this.validateForm('name')}>
<FormLabel>Name</FormLabel>
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />

View file

@ -69,7 +69,7 @@ class NewDashboardDialog extends React.Component {
return (
<Dialog show={this.props.show} title="New Dashboard" buttonTitle="Add" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<form>
<FormGroup controlId="name" validationstate={this.validateForm('name')}>
<FormGroup controlId="name" valid={this.validateForm('name')}>
<FormLabel>Name</FormLabel>
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />

View file

@ -40,7 +40,7 @@ class EditFilesDialog extends React.Component {
}
onClose() {
this.props.onClose();
}
@ -66,7 +66,12 @@ class EditFilesDialog extends React.Component {
};
updateUploadProgress = (event) => {
this.setState({ uploadProgress: parseInt(event.percent.toFixed(), 10) });
if (event.hasOwnProperty("percent")){
this.setState({ uploadProgress: parseInt(event.percent.toFixed(), 10) });
} else {
this.setState({ uploadProgress: 0 });
}
};
clearProgress = (newFileID) => {
@ -83,7 +88,7 @@ class EditFilesDialog extends React.Component {
};
closeEditModal(){
this.setState({editModal: false});
}
@ -144,11 +149,14 @@ class EditFilesDialog extends React.Component {
</FormGroup>
<FormGroup as={Col} >
<span className='solid-button'>
<Button
variant='secondary'
disabled={this.state.uploadFile === null}
onClick={() => this.startFileUpload()}>
Upload
</Button>
</span>
</FormGroup>
<FormGroup as={Col} >
@ -167,7 +175,7 @@ class EditFilesDialog extends React.Component {
</div>
</Dialog>
);
}
}

48
src/ic/confirm-command.js Normal file
View file

@ -0,0 +1,48 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { Button, Modal} from 'react-bootstrap';
class ConfirmCommand extends React.Component {
onModalKeyPress = (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.props.onClose(false);
}
}
render() {
return <Modal keyboard show={this.props.show} onHide={() => this.props.onClose(false)} onKeyPress={this.onModalKeyPress}>
<Modal.Header>
<Modal.Title>Confirm {this.props.command}</Modal.Title>
</Modal.Header>
<Modal.Body>
Are you sure you want to {this.props.command} <strong>'{this.props.name}'</strong>?
</Modal.Body>
<Modal.Footer>
<Button onClick={() => this.props.onClose(true)}>Cancel</Button>
<Button onClick={() => this.props.onClose(false)}>Confirm</Button>
</Modal.Footer>
</Modal>;
}
}
export default ConfirmCommand;

View file

@ -50,13 +50,13 @@ class EditICDialog extends React.Component {
data.name = this.state.name;
}
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;
}
data.websocketurl = this.state.websocketurl;
data.apiurl = this.state.apiurl;
if (this.state.location != null && this.state.location !== this.props.ic.location) {
data.location = this.state.location;
@ -122,8 +122,8 @@ class EditICDialog extends React.Component {
case "simulator":
typeOptions = ["dummy","generic","dpsim","rtlab","rscad", "opalrt"];
break;
case "controller":
typeOptions = ["kubernetes","villas-controller"];
case "manager":
typeOptions = ["villas-node","villas-relay","generic"];
break;
case "gateway":
typeOptions = ["villas-node","villas-relay"];
@ -158,10 +158,10 @@ class EditICDialog extends React.Component {
<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>
<option>manager</option>
</FormControl>
</FormGroup>
<FormGroup controlId="type">
@ -175,12 +175,12 @@ class EditICDialog extends React.Component {
</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 type="text" placeholder={this.props.ic.websocketurl} value={this.state.websocketurl} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<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 type="text" placeholder={this.props.ic.apiurl} value={this.state.apiurl} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="location">

View file

@ -16,75 +16,280 @@
******************************************************************************/
import React from 'react';
import { Button, ButtonToolbar, DropdownButton, Dropdown } from 'react-bootstrap';
import TimePicker from 'react-bootstrap-time-picker'
import { Button, DropdownButton, Dropdown, InputGroup, FormControl } from 'react-bootstrap';
import AppDispatcher from "../common/app-dispatcher";
import NotificationsFactory from "../common/data-managers/notifications-factory";
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
class ICAction extends React.Component {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.state = {
selectedAction: null,
selectedDelay: 0
};
let t = new Date()
Number.prototype.pad = function(size) {
var s = String(this);
while (s.length < (size || 2)) {s = "0" + s;}
return s;
}
static getDerivedStateFromProps(props, state){
if (state.selectedAction == null) {
if (props.actions != null && props.actions.length > 0) {
return{
selectedAction: props.actions[0]
};
}
}
return null
}
let time = new Date();
time.setMinutes(5 * Math.round(time.getMinutes() / 5 + 1))
setAction = id => {
// search action
for (let action of this.props.actions) {
if (action.id === id) {
this.setState({ selectedAction: action });
}
}
this.state = {
selectedAction: null,
time: time
};
}
setDelayForAction = time => {
// time in int format: (hours * 3600 + minutes * 60 + seconds)
this.setState({selectedDelay: time})
static getDerivedStateFromProps(props, state) {
if (state.selectedAction == null) {
if (props.actions != null && props.actions.length > 0) {
return {
selectedAction: props.actions[0]
};
}
}
render() {
return null
}
let sendCommandDisabled = this.props.runDisabled || this.state.selectedAction == null || this.state.selectedAction.id === "-1"
runAction(action, when) {
const actionList = this.props.actions.map(action => (
<Dropdown.Item key={action.id} eventKey={action.id} active={this.state.selectedAction === action.id}>
{action.title}
</Dropdown.Item>
));
return <div>
{"Select delay for command execution (Format hh:mm, max 1h):"}
<TimePicker
format={24}
initialValue={this.state.selectedDelay}
value={this.state.selectedDelay}
start={"00:00"}
end={"01:00"}
step={1}
onChange={this.setDelayForAction}
/>
<ButtonToolbar>
<DropdownButton title={this.state.selectedAction != null ? this.state.selectedAction.title : ''} id="action-dropdown" onSelect={this.setAction}>
{actionList}
</DropdownButton>
<Button style={{ marginLeft: '5px' }} disabled={sendCommandDisabled} onClick={() => this.props.runAction(this.state.selectedAction, this.state.selectedDelay)}>Send command</Button>
</ButtonToolbar>
</div>;
if (action.data.action === 'none') {
console.warn("No command selected. Nothing was sent.");
return;
}
if (!this.props.hasConfigs){
let newAction = {};
newAction["action"] = action.data.action
newAction["when"] = when
for (let index of this.props.selectedICs) {
let ic = this.props.ics[index];
let icID = ic.id;
/* VILLAScontroller protocol
see: https://villas.fein-aachen.org/doc/controller-protocol.html
RESET SHUTDOWN
{
"action": "reset/shutdown/stop/pause/resume"
"when": "1234567"
}
DELETE
{
"action": "delete"
"parameters":{
"uuid": "uuid-of-the-manager-for-this-IC"
}
"when": "1234567"
}
CREATE is not possible within ICAction (see add IC)
*/
if (newAction.action === "delete"){
// prepare parameters for delete incl. correct IC id
newAction["parameters"] = {};
newAction.parameters["uuid"] = ic.uuid;
// get the ID of the manager IC
let managerIC = null;
for (let i of this.props.ics){
if (i.uuid === ic.manager){
managerIC = i;
}
}
if (managerIC == null){
NotificationsDataManager.addNotification(NotificationsFactory.DELETE_ERROR("Could not find manager IC with UUID " + ic.manager));
continue;
}
icID = managerIC.id; // send delete action to manager of IC
}
AppDispatcher.dispatch({
type: 'ics/start-action',
icid: icID,
action: newAction,
result: null,
token: this.props.token
});
} // end for loop over selected ICs
} else {
/*VILLAScontoller protocol
see: https://villas.fein-aachen.org/doc/controller-protocol.html
*
* STOP PAUSE RESUME
{
"action": "reset/shutdown/stop/pause/resume"
"when": "1234567"
}
*
* START
{
"action": "start"
"when": 1234567
"parameters": {
Start parameters for this IC as configured in the component config
}
"model": {
"type": "url"
"url": "https://villas.k8s.eonerc.rwth-aachen.de/api/v2/files/{fileID}" where fileID is the model file configured in the component config
"token": "asessiontoken"
}
"results":{
"type": "url"
"url" : "https://villas.k8s.eonerc.rwth-aachen.de/api/v2/results/{resultID}/file" where resultID is the ID of the result created for this run
"token": "asessiontoken"
}
}
*
*
* */
let newActions = [];
for (let config of this.props.selectedConfigs) {
let newAction = {}
newAction["action"] = action.data.action
newAction["when"] = when
// get IC for component config
let ic = null;
for (let component of this.props.ics) {
if (component.id === config.icID) {
ic = component;
}
}
if (ic == null) {
continue;
}
// the following is not required by the protocol; it is an internal help
newAction["icid"] = ic.id
if (newAction.action === 'start') {
newAction["parameters"] = config.startParameters;
if (config.fileIDs.length > 0){
newAction["model"] = {}
newAction.model["type"] = "url"
newAction.model["token"] = this.props.token
let fileURLs = []
for (let fileID of config.fileIDs){
fileURLs.push("/files/" + fileID.toString())
}
newAction.model["url"] = fileURLs
}
newAction["results"] = {}
newAction.results["type"] = "url"
newAction.results["token"] = this.props.token
newAction.results["url"] = "/results/RESULTID/file" // RESULTID serves as placeholder and is replaced later
}
// add the new action
newActions.push(newAction);
} // end for loop over selected configs
let newResult = {}
newResult["result"] = {}
if (action.data.action === 'start') {
let configSnapshots = [];
// create config snapshots in case action is start
for (let config of this.props.selectedConfigs) {
let index = this.props.configs.indexOf(config)
configSnapshots.push(this.props.snapshotConfig(index));
}
// create new result for new run
newResult.result["description"] = "Start at " + when;
newResult.result["scenarioID"] = this.props.selectedConfigs[0].scenarioID
newResult.result["configSnapshots"] = configSnapshots
}
console.log("Dispatching actions for configs", newActions, newResult)
AppDispatcher.dispatch({
type: 'ics/start-action',
action: newActions,
result: newResult,
token: this.props.token
});
}
}
setAction = id => {
// search action
for (let action of this.props.actions) {
if (action.id === id) {
this.setState({ selectedAction: action });
}
}
};
setTimeForAction = (time) => {
this.setState({ time: new Date(time) })
}
render() {
let sendCommandDisabled = false;
if (!this.props.hasConfigs && this.props.selectedICs.length === 0 || this.state.selectedAction == null || this.state.selectedAction.id === "-1"){
sendCommandDisabled = true;
}
if (this.props.hasConfigs && this.props.selectedConfigs.length === 0|| this.state.selectedAction == null || this.state.selectedAction.id === "-1"){
sendCommandDisabled = true;
}
let time = this.state.time.getFullYear().pad(4) + '-' +
this.state.time.getMonth().pad(2) + '-' +
this.state.time.getDay().pad(2) + 'T' +
this.state.time.getHours().pad(2) + ':' +
this.state.time.getMinutes().pad(2);
const actionList = this.props.actions.map(action => (
<Dropdown.Item key={action.id} eventKey={action.id} active={this.state.selectedAction === action.id}>
{action.title}
</Dropdown.Item>
));
return <div className='solid-button'>
<InputGroup>
<InputGroup.Prepend>
<DropdownButton
variant="secondary"
title={this.state.selectedAction != null ? this.state.selectedAction.title : ''}
id="action-dropdown"
onSelect={this.setAction}>
{actionList}
</DropdownButton>
<FormControl
type="datetime-local"
variant="outline-secondary"
value={time}
onChange={this.setTimeForAction} />
</InputGroup.Prepend>
<Button
variant="secondary"
disabled={sendCommandDisabled}
onClick={() => this.runAction(this.state.selectedAction, this.state.time)}>Run</Button>
</InputGroup>
<small className="text-muted">Select time for synced command execution</small>
</div>;
}
}
export default ICAction;

View file

@ -17,6 +17,7 @@
import WebsocketAPI from '../common/api/websocket-api';
import AppDispatcher from '../common/app-dispatcher';
import RestAPI from "../common/api/rest-api";
const OFFSET_TYPE = 2;
const OFFSET_VERSION = 4;

View file

@ -1,6 +1,10 @@
import React from 'react';
import {FormLabel} from 'react-bootstrap';
import {Button, Row, Col} from 'react-bootstrap';
import Dialog from '../common/dialogs/dialog';
import Icon from "../common/icon";
import ConfirmCommand from './confirm-command';
import ReactJson from 'react-json-view';
import FileSaver from 'file-saver';
class ICDialog extends React.Component {
@ -10,7 +14,8 @@ class ICDialog extends React.Component {
super(props);
this.state = {
ic: props.ic
confirmCommand: false,
command: '',
};
}
@ -19,25 +24,115 @@ class ICDialog extends React.Component {
}
handleChange(e) {
}
showFurtherInfo(key){
if(typeof this.state[key] === 'undefined') this.setState({[key]: false});
this.setState({[key]: !this.state[key]});
}
closeConfirmModal(canceled){
if(!canceled){
this.props.sendControlCommand(this.state.command,this.props.ic);
}
this.setState({confirmCommand: false, command: ''});
}
async downloadGraph(url) {
let blob = await fetch(url).then(r => r.blob())
FileSaver.saveAs(blob, this.props.ic.name + ".svg");
}
render() {
let graphURL = ""
if (this.props.ic.apiurl !== ""){
graphURL = this.props.ic.apiurl + "/graph.svg"
}
return (
<Dialog
show={this.props.show}
title="Infos and Controls"
buttonTitle="Save"
title={this.props.ic.name + " ( " + this.props.ic.uuid + " )"}
buttonTitle="Close"
onClose={(c) => this.onClose(c)}
valid={true}
size='lg'
size='xl'
blendOutCancel={true}
>
<form>
<FormLabel>Infos and Controls</FormLabel>
<Row>
<Col>
<h5>State: {this.props.ic.state}</h5>
<h5>Category: {this.props.ic.category}</h5>
<h5>Type: {this.props.ic.type}</h5>
<h5>Uptime: {this.props.ic.uptime}</h5>
<h5>Location: {this.props.ic.location}</h5>
<h5>Description: {this.props.ic.description}</h5>
<h5>Websocket URL: {this.props.ic.websocketurl}</h5>
<h5>API URL: {this.props.ic.apiurl}</h5>
<h5>Start parameter scheme:</h5>
<ReactJson
src={this.props.ic.startParameterScheme}
name={false}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard={false}
collapsed={0}
/>
</Col>
<Col>
<h5>Raw Status:</h5>
<ReactJson
src={this.props.ic.statusupdateraw}
name={false}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard={false}
collapsed={1}
/>
{this.props.ic.type === "villas-node" || this.props.ic.type === "villas-relay" ? (
<>
<div className='section-buttons-group-right'>
<Button style={{margin: '5px'}} size='sm' onClick={() => this.downloadGraph(graphURL)}><Icon
icon="download"/></Button>
</div>
<h5>Graph:</h5>
<div>
<img alt={"Graph image download failed and/or incorrect image API URL"} src={graphURL}/>
</div>
{this.props.userRole === "Admin" ? (
<div>
<h5>Controls:</h5>
<div className='solid-button'>
<Button variant='secondary' style={{margin: '5px'}} size='lg'
onClick={() => this.setState({confirmCommand: true, command: 'restart'})}>Restart</Button>
<Button variant='secondary' style={{margin: '5px'}} size='lg' onClick={() => this.setState({
confirmCommand: true,
command: 'shutdown'
})}>Shutdown</Button>
</div>
</div>)
: (<div/>)}
<ConfirmCommand show={this.state.confirmCommand} command={this.state.command} name={this.props.ic.name}
onClose={c => this.closeConfirmModal(c)}/>
</>) : (<div/>)}
</Col>
</Row>
</form>
</Dialog>
);
}
}

View file

@ -20,6 +20,7 @@ import ICsDataManager from './ics-data-manager';
import ICDataDataManager from './ic-data-data-manager';
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
import NotificationsFactory from "../common/data-managers/notifications-factory";
import AppDispatcher from '../common/app-dispatcher';
class InfrastructureComponentStore extends ArrayStore {
constructor() {
@ -65,16 +66,120 @@ class InfrastructureComponentStore extends ArrayStore {
return state;
case 'ics/start-action':
if (!Array.isArray(action.data))
action.data = [ action.data ]
if (!Array.isArray(action.action))
action.action = [ action.action ]
ICsDataManager.doActions(action.ic, action.data, action.token);
ICsDataManager.doActions(action.icid, action.action, action.token, action.result);
return state;
case 'ics/action-started':
NotificationsDataManager.addNotification(NotificationsFactory.ACTION_INFO());
return state;
case 'ics/action-error':
console.log(action.error);
return state;
case 'ics/action-result-added':
for (let a of action.actions){
if (a.results !== undefined && a.results != null){
// adapt URL for newly created result ID
a.results.url = a.results.url.replace("RESULTID", action.data.result.id);
a.results.url = ICsDataManager.makeURL(a.results.url);
a.results.url = window.location.host + a.results.url;
}
if (a.model !== undefined && a.model != null && JSON.stringify(a.model) !== JSON.stringify({})) {
// adapt URL(s) for model file
let modelURLs = []
for (let url of a.model.url){
let modifiedURL = ICsDataManager.makeURL(url);
modifiedURL = window.location.host + modifiedURL;
modelURLs.push(modifiedURL)
}
a.model.url = modelURLs
}
ICsDataManager.doActions(a.icid, [a], action.token)
}
return state;
case 'ics/action-result-add-error':
console.log(action.error);
return state
case 'ics/get-status':
ICsDataManager.getStatus(action.url, action.token, action.ic);
return super.reduce(state, action);
case 'ics/status-received':
let tempIC = action.ic;
if(!tempIC.managedexternally){
tempIC.state = action.data.state;
tempIC.uptime = action.data.time_now - action.data.time_started;
if (tempIC.statusupdateraw === null || tempIC.statusupdateraw === undefined){
tempIC.statusupdateraw = {};
tempIC.statusupdateraw["status"] = action.data;
} else {
tempIC.statusupdateraw["status"] = action.data;
}
AppDispatcher.dispatch({
type: 'ics/start-edit',
data: tempIC,
token: action.token,
});
}
return super.reduce(state, action);
case 'ics/status-error':
console.log("status error:", action.error);
return super.reduce(state, action);
case 'ics/nodestats-received':
let tempIC2 = action.ic;
if(!tempIC2.managedexternally){
if (tempIC2.statusupdateraw === null || tempIC2.statusupdateraw === undefined){
tempIC2.statusupdateraw = {};
tempIC2.statusupdateraw["statistics"] = action.data;
} else {
tempIC2.statusupdateraw["statistics"] = action.data;
}
AppDispatcher.dispatch({
type: 'ics/start-edit',
data: tempIC2,
token: action.token,
});
}
return super.reduce(state, action);
case 'ics/nodestats-error':
console.log("nodestats error:", action.error);
return super.reduce(state, action);
case 'ics/restart':
ICsDataManager.restart(action.url, action.token);
return super.reduce(state, action);
case 'ics/restart-successful':
console.log("restart response:", action.data);
return super.reduce(state, action);
case 'ics/restart-error':
console.log("restart error:", action.error);
return super.reduce(state, action);
case 'ics/shutdown':
ICsDataManager.shutdown(action.url, action.token);
return super.reduce(state, action);
case 'ics/shutdown-successful':
console.log("shutdown response:", action.data);
return super.reduce(state, action);
case 'ics/shutdown-error':
console.log("shutdown error:", action.error);
return super.reduce(state, action);
default:
return super.reduce(state, action);
}

View file

@ -20,23 +20,151 @@ import RestAPI from '../common/api/rest-api';
import AppDispatcher from '../common/app-dispatcher';
class IcsDataManager extends RestDataManager {
constructor() {
super('ic', '/ic');
}
constructor() {
super('ic', '/ic');
}
doActions(ic, action, token = null) {
RestAPI.post(this.makeURL(this.url + '/' + ic.id + '/action'), action, token).then(response => {
AppDispatcher.dispatch({
doActions(icid, actions, token = null, result=null) {
if (icid !== undefined && icid != null && JSON.stringify(icid) !== JSON.stringify({})) {
for (let action of actions) {
if (action.when) {
// Send timestamp as Unix Timestamp
action.when = Math.round(action.when.getTime() / 1000);
}
}
// sending action to a specific IC via IC list
RestAPI.post(this.makeURL(this.url + '/' + icid + '/action'), actions, token).then(response => {
AppDispatcher.dispatch({
type: 'ics/action-started',
data: response
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'ics/action-error',
error
});
});
} else {
// sending the same action to multiple ICs via scenario controls
// distinguish between "start" action and any other
if (actions[0].action !== "start"){
for (let a of actions){
// sending action to a specific IC via IC list
if (a.when) {
// Send timestamp as Unix Timestamp
a.when = Math.round(a.when.getTime() / 1000);
}
RestAPI.post(this.makeURL(this.url + '/' + a.icid + '/action'), [a], token).then(response => {
AppDispatcher.dispatch({
type: 'ics/action-started',
data: response
});
}).catch(error => {
AppDispatcher.dispatch({
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'ics/action-error',
error
});
});
});
}
}
} else{
// for start actions procedure is different
// first a result needs to be created, then the start actions can be sent
RestAPI.post(this.makeURL( '/results'), result, token).then(response => {
AppDispatcher.dispatch({
type: 'ics/action-result-added',
data: response,
actions: actions,
token: token,
});
AppDispatcher.dispatch({
type: "results/added",
data: response.result,
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'ics/action-result-add-error',
error
});
});
}
}
}
getStatus(url,token,ic){
RestAPI.get(url + "/status", null).then(response => {
AppDispatcher.dispatch({
type: 'ics/status-received',
data: response,
token: token,
ic: ic
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'ics/status-error',
error: error
})
})
// get name of websocket
/*let ws_api = ic.websocketurl.split("/")
let ws_name = ws_api[ws_api.length-1] // websocket name is the last element in the websocket url
RestAPI.get(url + "/node/" + ws_name + "/stats", null).then(response => {
AppDispatcher.dispatch({
type: 'ics/nodestats-received',
data: response,
token: token,
ic: ic
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'ics/nodestats-error',
error: error
})
})*/
}
restart(url,token){
RestAPI.post(url, null).then(response => {
AppDispatcher.dispatch({
type: 'ics/restart-successful',
data: response,
token: token,
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'ics/restart-error',
error: error
})
})
}
shutdown(url,token){
RestAPI.post(url, null).then(response => {
AppDispatcher.dispatch({
type: 'ics/shutdown-successful',
data: response,
token: token,
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'ics/shutdown-error',
error: error
})
})
}
}
export default new IcsDataManager();

View file

@ -17,11 +17,12 @@
import React, { Component } from 'react';
import { Container } from 'flux/utils';
import { Button, Badge } from 'react-bootstrap';
import {Button, Badge, Tooltip, OverlayTrigger} from 'react-bootstrap';
import FileSaver from 'file-saver';
import _ from 'lodash';
import moment from 'moment'
import AppDispatcher from '../common/app-dispatcher';
import InfrastructureComponentStore from './ic-store';
@ -35,10 +36,12 @@ import ICDialog from './ic-dialog';
import ICAction from './ic-action';
import DeleteDialog from '../common/dialogs/delete-dialog';
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
import NotificationsFactory from "../common/data-managers/notifications-factory";
class InfrastructureComponents extends Component {
static getStores() {
return [ InfrastructureComponentStore ];
return [ InfrastructureComponentStore];
}
static statePrio(state) {
@ -74,9 +77,27 @@ class InfrastructureComponents extends Component {
}
});
// collect number of external ICs
let externalICs = ics.filter(ic => ic.managedexternally === true)
let numberOfExternalICs = externalICs.length;
// collect all IC categories
let managers = ics.filter(ic => ic.category === "manager")
let gateways = ics.filter(ic => ic.category === "gateway")
let simulators = ics.filter(ic => ic.category === "simulator")
let services = ics.filter(ic => ic.category === "service")
let equipment = ics.filter(ic => ic.category === "equipment")
return {
sessionToken: localStorage.getItem("token"),
ics: ics,
managers: managers,
gateways: gateways,
simulators: simulators,
services: services,
equipment: equipment,
numberOfExternalICs,
modalIC: {},
deleteModal: false,
icModal: false,
@ -91,7 +112,7 @@ class InfrastructureComponents extends Component {
token: this.state.sessionToken,
});
// Start timer for periodic refresh
// Start timer for periodic refresh
this.timer = window.setInterval(() => this.refresh(), 10000);
}
@ -109,19 +130,56 @@ class InfrastructureComponents extends Component {
type: 'ics/start-load',
token: this.state.sessionToken,
});
// get status of VILLASnode and VILLASrelay ICs
this.state.ics.forEach(ic => {
if ((ic.type === "villas-node" || ic.type === "villas-relay")
&& ic.apiurl !== '' && ic.apiurl !== undefined && ic.apiurl !== null && !ic.managedexternally) {
AppDispatcher.dispatch({
type: 'ics/get-status',
url: ic.apiurl,
token: this.state.sessionToken,
ic: ic
});
}
})
}
}
closeNewModal(data) {
this.setState({ newModal : false });
if (data) {
AppDispatcher.dispatch({
type: 'ics/start-add',
data,
token: this.state.sessionToken,
});
if (!data.managedexternally) {
AppDispatcher.dispatch({
type: 'ics/start-add',
data,
token: this.state.sessionToken,
});
} else {
// externally managed IC: dispatch create action to selected manager
let newAction = {};
newAction["action"] = "create";
newAction["parameters"] = data;
newAction["when"] = new Date()
// find the manager IC
let managerIC = this.state.ics.find(ic => ic.uuid === data.manager)
if (managerIC === null || managerIC === undefined){
NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Could not find manager IC with UUID " + data.manager));
return;
}
AppDispatcher.dispatch({
type: 'ics/start-action',
icid: managerIC.id,
action: newAction,
result: null,
token: this.state.sessionToken
});
}
}
}
@ -181,7 +239,9 @@ class InfrastructureComponents extends Component {
}
}
onICChecked(index, event) {
onICChecked(ic, event) {
let index = this.state.ics.indexOf(ic);
const selectedICs = Object.assign([], this.state.selectedICs);
for (let key in selectedICs) {
if (selectedICs[key] === index) {
@ -206,28 +266,18 @@ class InfrastructureComponents extends Component {
this.setState({ selectedICs: selectedICs });
}
runAction(action) {
for (let index of this.state.selectedICs) {
AppDispatcher.dispatch({
type: 'ics/start-action',
ic: this.state.ics[index],
data: action.data,
token: this.state.sessionToken,
});
}
}
static isICOutdated(component) {
if (!component.stateUpdatedAt)
if (!component.stateUpdateAt)
return true;
const fiveMinutes = 5 * 60 * 1000;
return Date.now() - new Date(component.stateUpdatedAt) > fiveMinutes;
return Date.now() - new Date(component.stateUpdateAt) > fiveMinutes;
}
stateLabelStyle(state, component){
var style = [ 'badge' ];
if (InfrastructureComponents.isICOutdated(component) && state !== 'shutdown') {
@ -272,7 +322,6 @@ class InfrastructureComponents extends Component {
default:
style.push('badge-default');
/* Possible states of ICs
* 'error': ['resetting', 'error'],
'idle': ['resetting', 'error', 'idle', 'starting', 'shuttingdown'],
@ -291,114 +340,209 @@ class InfrastructureComponents extends Component {
return style.join(' ')
}
stateUpdateModifier(updatedAt) {
let dateFormat = 'DD MMM YYYY HH:mm:ss';
stateUpdateModifier(updatedAt, component) {
let dateFormat = 'ddd, DD MMM YYYY HH:mm:ss ZZ';
let dateTime = moment(updatedAt, dateFormat);
return dateTime.fromNow()
}
modifyManagedExternallyColumn(managedExternally){
modifyManagedExternallyColumn(managedExternally, component){
if(managedExternally){
return <Icon icon='check' />
} else {
return <Icon icon='times' />
return ""
}
}
modifyUptimeColumn(uptime){
modifyUptimeColumn(uptime, component){
if(uptime >= 0){
return <span>{uptime + "s"}</span>
let momentDurationFormatSetup = require("moment-duration-format");
momentDurationFormatSetup(moment)
let timeString = moment.duration(uptime, "seconds").humanize();
return <span>{timeString}</span>
}
else{
return <Badge variant="secondary">Unknown</Badge>
}
}
modifyNameColumn(name){
let ic = this.state.ics.find(ic => ic.name === name);
modifyNameColumn(name, component){
let index = this.state.ics.indexOf(component);
return <Button variant="link" onClick={() => this.openICStatus(component)}>{name}</Button>
}
openICStatus(ic){
let index = this.state.ics.indexOf(ic);
if(ic.type === "villas-node" || ic.type === "villas-relay"){
return <Button variant="link" onClick={() => this.setState({ icModal: true, modalIC: ic, modalIndex: index })}>{name}</Button> }
else{
return <span>{name}</span>
this.setState({ icModal: true, modalIC: ic, modalIndex: index })
}
sendControlCommand(command,ic){
if(command === "restart"){
AppDispatcher.dispatch({
type: 'ics/restart',
url: ic.apiurl + "/restart",
token: this.state.sessionToken,
});
}else if(command === "shutdown"){
AppDispatcher.dispatch({
type: 'ics/shutdown',
url: ic.apiurl + "/shutdown",
token: this.state.sessionToken,
});
}
}
isLocalIC(index, ics){
let ic = ics[index]
return !ic.managedexternally
}
getICCategoryTable(ics, editable, title){
if (ics && ics.length > 0) {
return (<div>
<h2>{title}</h2>
<Table data={ics}>
<TableColumn
checkbox
checkboxDisabled={(index) => this.isLocalIC(index, ics) === true}
onChecked={(ic, event) => this.onICChecked(ic, event)}
width='30'
/>
<TableColumn
title='Name'
dataKeys={['name']}
modifier={(name, component) => this.modifyNameColumn(name, component)}
/>
<TableColumn
title='State'
labelKey='state'
tooltipKey='error'
labelStyle={(state, component) => this.stateLabelStyle(state, component)}
/>
<TableColumn
title='Type'
dataKeys={['type']}
/>
<TableColumn
title='Uptime'
dataKey='uptime'
modifier={(uptime, component) => this.modifyUptimeColumn(uptime, component)}
/>
<TableColumn
title='Last Update'
dataKey='stateUpdateAt'
modifier={(stateUpdateAt, component) => this.stateUpdateModifier(stateUpdateAt, component)}
/>
{this.state.currentUser.role === "Admin" ?
<TableColumn
width='150'
editButton
showEditButton ={(index) => this.isLocalIC(index, ics)}
exportButton
deleteButton
showDeleteButton = {(index) => this.isLocalIC(index, ics)}
onEdit={index => this.setState({editModal: true, modalIC: ics[index], modalIndex: index})}
onExport={index => this.exportIC(index)}
onDelete={index => this.setState({deleteModal: true, modalIC: ics[index], modalIndex: index})}
/>
:
<TableColumn
width='50'
exportButton
onExport={index => this.exportIC(index)}
/>
}
</Table>
</div>);
} else {
return <div/>
}
}
render() {
const buttonStyle = {
marginLeft: '10px'
};
const iconStyle = {
height: '30px',
width: '30px'
}
let managerTable = this.getICCategoryTable(this.state.managers, false, "IC Managers")
let simulatorTable = this.getICCategoryTable(this.state.simulators, true, "Simulators")
let gatewayTable = this.getICCategoryTable(this.state.gateways, true, "Gateways")
let serviceTable = this.getICCategoryTable(this.state.services, true, "Services")
let equipmentTable = this.getICCategoryTable(this.state.equipment, true, "Equipment")
return (
<div className='section'>
<h1>Infrastructure Components</h1>
<Table data={this.state.ics}>
<TableColumn checkbox onChecked={(index, event) => this.onICChecked(index, event)} width='30' />
<TableColumn title='Name' dataKeys={['name', 'rawProperties.name']} modifier={(name) => this.modifyNameColumn(name)}/>
<TableColumn title='State' labelKey='state' tooltipKey='error' labelStyle={(state, component) => this.stateLabelStyle(state, component)} />
<TableColumn title='Category' dataKeys={['category', 'rawProperties.category']} />
<TableColumn title='Type' dataKeys={['type', 'rawProperties.type']} />
<TableColumn title='Managed externally' dataKey='managedexternally' modifier={(managedexternally) => this.modifyManagedExternallyColumn(managedexternally)} width='105' />
<TableColumn title='Uptime' dataKey='uptime' modifier={(uptime) => this.modifyUptimeColumn(uptime)}/>
<TableColumn title='Location' dataKey='location' />
{/* <TableColumn title='Realm' dataKeys={['properties.realm', 'rawProperties.realm']} /> */}
<TableColumn title='WebSocket URL' dataKey='websocketurl' />
<TableColumn title='API URL' dataKey='apiurl' />
<TableColumn title='Last Update' dataKey='stateUpdateAt' modifier={(stateUpdateAt) => this.stateUpdateModifier(stateUpdateAt)} />
<h1>Infrastructure Components
{this.state.currentUser.role === "Admin" ?
<TableColumn
width='200'
editButton
exportButton
deleteButton
onEdit={index => this.setState({ editModal: true, modalIC: this.state.ics[index], modalIndex: index })}
onExport={index => this.exportIC(index)}
onDelete={index => this.setState({ deleteModal: true, modalIC: this.state.ics[index], modalIndex: index })}
/>
:
<TableColumn
width='100'
exportButton
onExport={index => this.exportIC(index)}
/>
(<span className='icon-button'>
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add Infrastructure Component </Tooltip>} >
<Button variant='light' onClick={() => this.setState({newModal: true})} style={buttonStyle}><Icon icon="plus" classname='icon-color' style={iconStyle}
/></Button>
</OverlayTrigger>
<OverlayTrigger
key={2}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"import"}`}> Import Infrastructure Component </Tooltip>} >
<Button variant='light' onClick={() => this.setState({importModal: true})} style={buttonStyle}><Icon icon="upload" classname='icon-color' style={iconStyle}
/></Button>
</OverlayTrigger>
</span>)
:
(<span> </span>)
}
</Table>
{this.state.currentUser.role === "Admin" ?
<div style={{ float: 'left' }}>
</h1>
{managerTable}
{simulatorTable}
{gatewayTable}
{serviceTable}
{equipmentTable}
{this.state.currentUser.role === "Admin" && this.state.numberOfExternalICs > 0 ?
<div style={{float: 'left'}}>
<ICAction
runDisabled={this.state.selectedICs.length === 0}
runAction={action => this.runAction(action)}
hasConfigs = {false}
ics={this.state.ics}
selectedICs={this.state.selectedICs}
token={this.state.sessionToken}
actions={[
{ id: '-1', title: 'Select command', data: { action: 'none' } },
{ id: '0', title: 'Reset', data: { action: 'reset' } },
{ id: '1', title: 'Shutdown', data: { action: 'shutdown' } },
]}
{id: '-1', title: 'Action', data: {action: 'none'}},
{id: '0', title: 'Reset', data: {action: 'reset'}},
{id: '1', title: 'Shutdown', data: {action: 'shutdown'}},
{id: '2', title: 'Delete', data: {action: 'delete'}}
]}
/>
</div>
:
<div> </div>
}
{this.state.currentUser.role === "Admin" ?
<div style={{ float: 'right' }}>
<Button onClick={() => this.setState({ newModal: true })} style={buttonStyle}><Icon icon="plus" /> Infrastructure Component</Button>
<Button onClick={() => this.setState({ importModal: true })} style={buttonStyle}><Icon icon="upload" /> Import</Button>
</div>
:
<div> </div>
<div/>
}
<div style={{ clear: 'both' }} />
<NewICDialog show={this.state.newModal} onClose={data => this.closeNewModal(data)} />
<NewICDialog show={this.state.newModal} onClose={data => this.closeNewModal(data)} managers={this.state.managers} />
<EditICDialog show={this.state.editModal} onClose={data => this.closeEditModal(data)} ic={this.state.modalIC} />
<ImportICDialog show={this.state.importModal} onClose={data => this.closeImportModal(data)} />
<ICDialog show={this.state.icModal} onClose={data => this.closeICModal(data)} ic={this.state.modalIC} token={this.state.sessionToken} />
<DeleteDialog title="infrastructure-component" name={this.state.modalIC.name || 'Unknown'} show={this.state.deleteModal} onClose={(e) => this.closeDeleteModal(e)} />
<ICDialog
show={this.state.icModal}
onClose={data => this.closeICModal(data)}
ic={this.state.modalIC}
token={this.state.sessionToken}
userRole={this.state.currentUser.role}
sendControlCommand={(command, ic) => this.sendControlCommand(command, ic)}/>
</div>
);
}

View file

@ -118,7 +118,7 @@ class ImportICDialog extends React.Component {
<FormControl type="file" onChange={(e) => this.loadFile(e.target.files)} />
</FormGroup>
<FormGroup controlId="name" validationState={this.validateForm('name')}>
<FormGroup controlId="name" valid={this.validateForm('name')}>
<FormLabel>Name</FormLabel>
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
@ -128,7 +128,7 @@ class ImportICDialog extends React.Component {
<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')}>
<FormGroup controlId="uuid" valid={this.validateForm('uuid')}>
<FormLabel>UUID</FormLabel>
<FormControl type="text" placeholder="Enter uuid" value={this.state.uuid} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />

View file

@ -34,7 +34,8 @@ class NewICDialog extends React.Component {
category: '',
managedexternally: false,
description: '',
location: ''
location: '',
manager: ''
};
}
@ -48,7 +49,8 @@ class NewICDialog extends React.Component {
uuid: this.state.uuid,
managedexternally: this.state.managedexternally,
location: this.state.location,
description: this.state.description
description: this.state.description,
manager: this.state.manager
};
if (this.state.websocketurl != null && this.state.websocketurl !== "" && this.state.websocketurl !== 'http://') {
@ -88,6 +90,7 @@ class NewICDialog extends React.Component {
let websocketurl = true;
let type = true;
let category = true;
let manager = true;
if (this.state.name === '') {
name = false;
@ -97,6 +100,10 @@ class NewICDialog extends React.Component {
uuid = false;
}
if(this.state.managedexternally && manager === ''){
manager = false;
}
if (this.state.type === '') {
type = false;
}
@ -105,7 +112,7 @@ class NewICDialog extends React.Component {
category = false;
}
this.valid = name && uuid && websocketurl && type && category;
this.valid = name && uuid && websocketurl && type && category && manager;
// return state to control
if (target === 'name') return name ? "success" : "error";
@ -113,6 +120,7 @@ class NewICDialog extends React.Component {
if (target === 'websocketurl') return websocketurl ? "success" : "error";
if (target === 'type') return type ? "success" : "error";
if (target === 'category') return category ? "success" : "error";
if (target === 'manager') return manager ? "success" : "error";
return this.valid;
}
@ -131,8 +139,8 @@ class NewICDialog extends React.Component {
case "simulator":
typeOptions = ["dummy","generic","dpsim","rtlab","rscad","opalrt"];
break;
case "controller":
typeOptions = ["kubernetes","villas-controller"];
case "manager":
typeOptions = ["villas-node","villas-relay","generic"];
break;
case "gateway":
typeOptions = ["villas-node","villas-relay"];
@ -146,33 +154,60 @@ class NewICDialog extends React.Component {
default:
typeOptions =[];
}
let managerOptions = [];
managerOptions.push(<option default>Select manager</option>);
for (let m of this.props.managers) {
managerOptions.push (
<option key={m.id} value={m.uuid}>{m.name}</option>
);
}
return (
<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>
{this.props.managers.length > 0 ?
<>
<FormGroup controlId="managedexternally">
<OverlayTrigger key="-1" placement={'left'} overlay={<Tooltip id={`tooltip-${"me"}`}>An externally managed component is created and managed by an IC manager via AMQP</Tooltip>} >
<FormCheck type={"checkbox"} label={"Managed externally"} defaultChecked={this.state.managedexternally} onChange={e => this.handleChange(e)}>
</FormCheck>
</OverlayTrigger>
</FormGroup>
{this.state.managedexternally === true ?
<FormGroup controlId="manager" valid={this.validateForm('manager')}>
<OverlayTrigger key="0" placement={'right'} overlay={<Tooltip id={`tooltip-${"required"}`}> Required field </Tooltip>} >
<FormLabel>Manager to create new IC *</FormLabel>
</OverlayTrigger>
<FormControl as="select" value={this.state.manager} onChange={(e) => this.handleChange(e)}>
{managerOptions}
</FormControl>
</FormGroup>
: <div/>
}
</>
: <div/>
}
<FormGroup controlId="name" valid={this.validateForm('name')}>
<OverlayTrigger key="0" placement={'right'} overlay={<Tooltip id={`tooltip-${"required"}`}> Required field </Tooltip>} >
<OverlayTrigger key="1" 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="category" valid={this.validateForm('category')}>
<OverlayTrigger key="1" placement={'right'} overlay={<Tooltip id={`tooltip-${"required"}`}> Required field </Tooltip>} >
<OverlayTrigger key="2" 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>
<option>manager</option>
</FormControl>
</FormGroup>
<FormGroup controlId="type" valid={this.validateForm('type')}>
@ -206,11 +241,15 @@ class NewICDialog extends React.Component {
<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')}>
<FormLabel>UUID</FormLabel>
<FormControl type="text" placeholder="Enter uuid" value={this.state.uuid} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
{this.state.managedexternally === false ?
<FormGroup controlId="uuid" valid={this.validateForm('uuid')}>
<FormLabel>UUID</FormLabel>
<FormControl type="text" placeholder="Enter uuid" value={this.state.uuid}
onChange={(e) => this.handleChange(e)}/>
<FormControl.Feedback/>
</FormGroup>
: <div/>
}
</form>
</Dialog>
);

BIN
src/img/dog-waiting-bw.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
src/img/erigrid2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

208
src/result/edit-result.js Normal file
View file

@ -0,0 +1,208 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { FormGroup, FormControl, FormLabel, Col, Row, Button, ProgressBar } from 'react-bootstrap';
import AppDispatcher from "../common/app-dispatcher";
import FileStore from "../file/file-store"
import Table from "../common/table";
import TableColumn from "../common/table-column";
import Dialog from '../common/dialogs/dialog';
class EditResultDialog extends React.Component {
valid = true;
constructor(props) {
super(props);
this.state = {
id: 0,
description: '',
uploadFile: null,
uploadProgress: 0,
files: null,
};
}
onClose() {
if (this.props.onClose != null) {
this.props.onClose();
}
};
handleChange = event => {
this.setState({ [event.target.id]: event.target.value });
};
isEmpty(val) {
return (val === undefined || val == null || val.length <= 0);
};
componentDidUpdate(prevProps, prevState) {
if (this.props.resultId != prevProps.resultId || this.props.results != prevProps.results) {
let result = this.props.results[this.props.resultId];
if (result && Object.keys(result).length != 0) {
let hasFiles = !this.isEmpty(result.resultFileIDs);
if (hasFiles) {
this.setState({
id: result.id,
description: result.description,
files: FileStore.getState().filter(file => result.resultFileIDs.includes(file.id)),
})
} else {
this.setState({
id: result.id,
description: result.description,
files: null,
})
}
}
}
};
selectUploadFile(event) {
this.setState({ uploadFile: event.target.files[0] });
};
startFileUpload() {
const formData = new FormData();
formData.append("file", this.state.uploadFile);
AppDispatcher.dispatch({
type: 'resultfiles/start-upload',
data: formData,
resultID: this.state.id,
token: this.props.sessionToken,
progressCallback: this.updateUploadProgress,
finishedCallback: this.clearProgress,
scenarioID: this.props.scenarioID,
});
this.setState({ uploadFile: null });
};
clearProgress = (newFileID) => {
this.setState({ uploadProgress: 0 });
};
updateUploadProgress = (event) => {
this.setState({ uploadProgress: parseInt(event.percent.toFixed(), 10) });
};
deleteFile(index) {
let file = this.state.files[index];
AppDispatcher.dispatch({
type: 'resultfiles/start-remove',
resultID: this.state.id,
fileID: file.id,
token: this.props.sessionToken
});
}
submitDescription() {
let result = this.props.results[this.props.resultId];
if (!this.isEmpty(result)) {
result.description = this.state.description;
AppDispatcher.dispatch({
type: 'results/start-edit',
data: result,
token: this.props.sessionToken
});
}
}
render() {
return <Dialog show={this.props.show}
title={'Edit Result No. ' + this.state.id}
buttonTitle='Close'
onClose={() => this.onClose()}
blendOutCancel={true}
valid={true}
size='lg'>
<div>
<FormGroup as={Col} controlId='description'>
<Row style={{ float: 'center' }} >
<Col xs lg="2">
<FormLabel>Description</FormLabel>
</Col>
<Col xs lg="4">
<FormControl type='text' placeholder={this.state.description} value={this.state.description} onChange={this.handleChange} />
<FormControl.Feedback />
</Col>
<Col xs lg="2">
<Button
type="submit"
onClick={() => this.submitDescription()}>
Save
</Button>
</Col>
</Row>
</FormGroup>
<Table data={this.state.files}>
<TableColumn title='ID' dataKey='id' />
<TableColumn title='Name' dataKey='name' />
<TableColumn title='Size (bytes)' dataKey='size' />
<TableColumn title='Type' dataKey='type' />
<TableColumn
title=''
deleteButton
onDelete={(index) => this.deleteFile(index)}
/>
</Table>
<div style={{ float: 'center' }}>
<h5>Add result file</h5>
<Row>
<Col xs lg="4">
<FormControl type='file' onChange={(event) => this.selectUploadFile(event)} />
</Col>
<Col xs lg="2">
<Button
disabled={this.state.uploadFile === null}
onClick={() => this.startFileUpload()}>
Upload
</Button>
</Col>
</Row>
</div>
<br></br>
<FormGroup as={Col} >
<ProgressBar
striped={true}
animated={true}
now={this.state.uploadProgress}
label={this.state.uploadProgress + '%'}
/>
</FormGroup>
</div>
</Dialog>;
}
}
export default EditResultDialog;

70
src/result/new-result.js Normal file
View file

@ -0,0 +1,70 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { FormGroup, FormControl, FormLabel } from 'react-bootstrap';
import Dialog from '../common/dialogs/dialog';
class NewResultDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
ConfigSnapshots: '',
Description: '',
ResultFileIDs: [],
}
}
onClose(canceled) {
if (canceled === false) {
this.props.onClose(this.state);
} else {
this.props.onClose();
}
}
handleChange(e) {
this.setState({ [e.target.id]: e.target.value });
}
resetState() {
this.setState({
ConfigSnapshots: '',
Description: '',
ResultFileIDs: [],
});
}
render() {
return (
<Dialog show={this.props.show} title="New Result" buttonTitle="Add" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={true}>
<form>
<FormGroup controlId="Description">
<FormLabel>Description</FormLabel>
<FormControl type="text" placeholder="Enter description" value={this.state.Description} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
</form>
</Dialog>
);
}
}
export default NewResultDialog;

View file

@ -0,0 +1,64 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import Dialog from '../common/dialogs/dialog';
import ReactJson from 'react-json-view';
class ResultConfigDialog extends React.Component {
valid = true;
constructor(props) {
super(props);
this.state = {
confirmCommand: false,
command: '',
};
}
onClose(canceled) {
this.props.onClose();
}
render() {
return (
<Dialog
show={this.props.show}
title={"Component Configurations for Result No. " + this.props.resultNo}
buttonTitle="Close"
onClose={(c) => this.onClose(c)}
valid={true}
size="lg"
blendOutCancel={true}
>
<form>
<ReactJson
src={this.props.configs}
name={false}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard={false}
collapsed={false}
/>
</form>
</Dialog>
);
}
}
export default ResultConfigDialog;

View file

@ -0,0 +1,85 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import ArrayStore from '../common/array-store';
import ResultsDataManager from './results-data-manager';
import AppDispatcher from '../common/app-dispatcher';
class ResultStore extends ArrayStore {
constructor() {
super('results', ResultsDataManager);
}
simplify(timestamp) {
let parts = timestamp.split("T");
let datestr = parts[0];
let time = parts[1].split(".");
return datestr + ', ' + time[0];;
}
simplifyTimestamps(data) {
data.forEach((result) => {
result.createdAt = this.simplify(result.createdAt);
result.updatedAt = this.simplify(result.updatedAt);
});
}
reduce(state, action) {
switch (action.type) {
case 'results/loaded':
if (Array.isArray(action.data)) {
this.simplifyTimestamps(action.data);
} else {
this.simplifyTimestamps([action.data]);
}
return super.reduce(state, action);
case 'results/added':
this.simplifyTimestamps([action.data]);
return super.reduce(state, action);
case 'results/edited':
this.simplifyTimestamps([action.data]);
return super.reduce(state, action);
case 'results/removed':
// Remove files from filestore
action.data.resultFileIDs.forEach(fileid => {
AppDispatcher.dispatch({
type: 'files/removed',
data: fileid
});
});
return super.reduce(state, action);
case 'resultfiles/start-upload':
ResultsDataManager.uploadFile(action.data, action.resultID, action.token, action.progressCallback, action.finishedCallback, action.scenarioID);
return state;
case 'resultfiles/start-remove':
ResultsDataManager.removeFile(action.resultID, action.fileID, action.token);
return state;
default:
return super.reduce(state, action);
}
}
}
export default new ResultStore();

View file

@ -0,0 +1,79 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import RestDataManager from '../common/data-managers/rest-data-manager';
import RestAPI from '../common/api/rest-api';
import AppDispatcher from '../common/app-dispatcher';
class ResultsDataManager extends RestDataManager {
constructor() {
super('result', '/results');
}
uploadFile(file, resultID, token = null, progressCallback = null, finishedCallback = null, scenarioID) {
RestAPI.upload(this.makeURL(this.url + '/' + resultID + '/file'), file, token, progressCallback, scenarioID).then(response => {
AppDispatcher.dispatch({
type: 'files/uploaded',
});
// Trigger a result reload
AppDispatcher.dispatch({
type: 'results/start-load',
data: resultID,
token: token,
});
// Trigger a files reload
AppDispatcher.dispatch({
type: 'files/start-load',
param: '?scenarioID=' + scenarioID,
token: token
});
if (finishedCallback) {
finishedCallback(response.result.id);
}
}).catch(error => {
AppDispatcher.dispatch({
type: 'files/upload-error',
error
});
});
}
removeFile(resultID, fileID, token) {
RestAPI.delete(this.makeURL(this.url + '/' + resultID + '/file/' + fileID), token).then(response => {
// reload result
AppDispatcher.dispatch({
type: 'results/start-load',
data: resultID,
token: token,
});
// update files
AppDispatcher.dispatch({
type: 'files/removed',
data: fileID,
token: token,
});
});
}
}
export default new ResultsDataManager();

View file

@ -28,12 +28,15 @@ import Dashboard from './dashboard/dashboard'
import InfrastructureComponents from './ic/ics';
import Users from './user/users';
import User from "./user/user";
import LoginComplete from './user/login-complete'
class Root extends React.Component {
render() {
return (
<BrowserRouter>
<Switch>
<Route path='/login/complete' component={LoginComplete} />
<Route path='/login' component={Login} />
<Route path='/logout' component={Logout} />
<Route path='/' component={App} />

View file

@ -36,6 +36,10 @@ import ImportDashboardDialog from "../dashboard/import-dashboard";
import NewDashboardDialog from "../dashboard/new-dashboard";
import EditDashboardDialog from '../dashboard/edit-dashboard';
import EditFiles from '../file/edit-files'
import NewResultDialog from '../result/new-result';
import EditResultDialog from '../result/edit-result';
import ResultConfigDialog from '../result/result-configs-dialog';
import ICAction from '../ic/ic-action';
import DeleteDialog from '../common/dialogs/delete-dialog';
@ -43,13 +47,16 @@ import EditConfigDialog from "../componentconfig/edit-config";
import EditSignalMapping from "../signal/edit-signal-mapping";
import FileStore from "../file/file-store"
import WidgetStore from "../widget/widget-store";
import ResultStore from "../result/result-store"
import { Redirect } from 'react-router-dom';
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
var JSZip = require("jszip");
class Scenario extends React.Component {
static getStores() {
return [ScenarioStore, ConfigStore, DashboardStore, ICStore, SignalStore, FileStore, WidgetStore];
return [ScenarioStore, ConfigStore, DashboardStore, ICStore, SignalStore, FileStore, WidgetStore, ResultStore];
}
static calculateState(prevState, props) {
@ -68,7 +75,6 @@ class Scenario extends React.Component {
});
}
// obtain all component configurations of a scenario
let configs = ConfigStore.getState().filter(config => config.scenarioID === parseInt(props.match.params.scenario, 10));
let editConfigModal = prevState.editConfigModal || false;
@ -82,8 +88,11 @@ class Scenario extends React.Component {
modalConfigIndex = index;
}
let results = ResultStore.getState().filter(result => result.scenarioID === parseInt(props.match.params.scenario, 10));
return {
scenario,
results,
sessionToken,
configs,
editConfigModal,
@ -95,6 +104,7 @@ class Scenario extends React.Component {
files: FileStore.getState().filter(file => file.scenarioID === parseInt(props.match.params.scenario, 10)),
ics: ICStore.getState(),
ExternalICInUse: false,
deleteConfigModal: false,
importConfigModal: false,
@ -103,6 +113,17 @@ class Scenario extends React.Component {
filesEditModal: prevState.filesEditModal || false,
filesEditSaveState: prevState.filesEditSaveState || [],
editResultsModal: prevState.editResultsModal || false,
modalResultsData: {},
modalResultsIndex: prevState.modalResultsIndex,
newResultModal: false,
filesToDownload: prevState.filesToDownload,
zipfiles: prevState.zipfiles || false,
resultNodl: prevState.resultNodl,
resultConfigsModal: false,
modalResultConfigs: {},
modalResultConfigsIndex: 0,
editOutputSignalsModal: prevState.editOutputSignalsModal || false,
editInputSignalsModal: prevState.editInputSignalsModal || false,
@ -142,6 +163,35 @@ class Scenario extends React.Component {
});
}
componentDidUpdate(prevProps, prevState) {
// check whether file data has been loaded
if (this.state.filesToDownload && this.state.filesToDownload.length > 0) {
if (this.state.files != prevState.files) {
if (!this.state.zipfiles) {
let fileToDownload = FileStore.getState().filter(file => file.id === this.state.filesToDownload[0])
if (fileToDownload.length === 1 && fileToDownload[0].data) {
const blob = new Blob([fileToDownload[0].data], { type: fileToDownload[0].type });
FileSaver.saveAs(blob, fileToDownload[0].name);
this.setState({ filesToDownload: [] });
}
} else { // zip and save one or more files (download all button)
let filesToDownload = FileStore.getState().filter(file => this.state.filesToDownload.includes(file.id) && file.data);
if (filesToDownload.length === this.state.filesToDownload.length) { // all requested files have been loaded
var zip = new JSZip();
filesToDownload.forEach(file => {
zip.file(file.name, file.data);
});
let zipname = "result_" + this.state.resultNodl + "_" + (new Date()).toISOString();
zip.generateAsync({ type: "blob" }).then(function (content) {
saveAs(content, zipname);
});
this.setState({ filesToDownload: [] });
}
}
}
}
}
/* ##############################################
* User modification methods
############################################## */
@ -321,44 +371,28 @@ class Scenario extends React.Component {
this.setState({ selectedConfigs: selectedConfigs });
}
runAction(action, delay) {
// delay in seconds
usesExternalIC(index) {
let icID = this.state.configs[index].icID;
if (action.data.action === 'none') {
console.warn("No command selected. Nothing was sent.");
return;
let ic = null;
for (let component of this.state.ics) {
if (component.id === this.state.configs[index].icID) {
ic = component;
}
}
for (let index of this.state.selectedConfigs) {
// get IC for component config
let ic = null;
for (let component of this.state.ics) {
if (component.id === this.state.configs[index].icID) {
ic = component;
}
}
if (ic == null) {
continue;
}
if (action.data.action === 'start') {
action.data.parameters = this.state.configs[index].startParameters;
}
// Unix time stamp + delay
action.data.when = Date.now() / 1000.0 + delay
console.log("Sending action: ", action.data)
AppDispatcher.dispatch({
type: 'ics/start-action',
ic: ic,
data: action.data,
token: this.state.sessionToken
});
if (ic == null) {
return false;
}
};
if (ic.managedexternally === true) {
this.setState({ ExternalICInUse: true })
return true
}
return false
}
getICName(icID) {
for (let ic of this.state.ics) {
@ -375,6 +409,7 @@ class Scenario extends React.Component {
closeNewDashboardModal(data) {
this.setState({ newDashboardModal: false });
if (data) {
// TODO: 'newDashboard' not used, check this
let newDashboard = data;
// add default grid value and scenarioID
newDashboard["grid"] = 15;
@ -472,11 +507,21 @@ class Scenario extends React.Component {
############################################## */
closeEditSignalsModal(direction) {
// reload the config
AppDispatcher.dispatch({
type: 'configs/start-load',
data: this.state.modalConfigData.id,
token: this.state.sessionToken
});
if (direction === "in") {
this.setState({ editInputSignalsModal: false });
} else if (direction === "out") {
this.setState({ editOutputSignalsModal: false });
}
}
onEditFiles() {
@ -495,11 +540,11 @@ class Scenario extends React.Component {
// TODO do we need this if the dispatches happen in the dialog?
}
signalsAutoConf(index){
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")){
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);
@ -516,8 +561,8 @@ class Scenario extends React.Component {
AppDispatcher.dispatch({
type: 'signals/start-autoconfig',
url: ic.apiurl+"/nodes",
socketname: splitWebsocketURL[splitWebsocketURL.length -1],
url: ic.apiurl + "/nodes",
socketname: splitWebsocketURL[splitWebsocketURL.length - 1],
token: this.state.sessionToken,
configID: componentConfig.id
});
@ -525,7 +570,7 @@ class Scenario extends React.Component {
}
uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
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);
@ -554,6 +599,86 @@ class Scenario extends React.Component {
return fileList;
}
/* ##############################################
* Result modification methods
############################################## */
closeNewResultModal(data) {
this.setState({ newResultModal: false });
if (data) {
data["scenarioID"] = this.state.scenario.id;
AppDispatcher.dispatch({
type: 'results/start-add',
data,
token: this.state.sessionToken,
});
}
}
closeEditResultsModal() {
this.setState({ editResultsModal: false });
}
downloadResultData(param) {
let toDownload = [];
let zip = false;
if (typeof (param) === 'object') { // download all files
toDownload = param.resultFileIDs;
zip = true;
this.setState({ filesToDownload: toDownload, zipfiles: zip, resultNodl: param.id });
} else { // download one file
toDownload.push(param);
this.setState({ filesToDownload: toDownload, zipfiles: zip });
}
toDownload.forEach(fileid => {
AppDispatcher.dispatch({
type: 'files/start-download',
data: fileid,
token: this.state.sessionToken
});
});
}
closeDeleteResultsModal(confirmDelete) {
this.setState({ deleteResultsModal: false });
if (confirmDelete === false) {
return;
}
AppDispatcher.dispatch({
type: 'results/start-remove',
data: this.state.modalResultsData,
token: this.state.sessionToken,
});
}
openResultConfigSnaphots(result) {
if (result.configSnapshots === null || result.configSnapshots === undefined) {
this.setState({
modalResultConfigs: {"configs": []},
modalResultConfigsIndex: result.id,
resultConfigsModal: true
});
} else {
this.setState({
modalResultConfigs: result.configSnapshots,
modalResultConfigsIndex: result.id,
resultConfigsModal: true
});
}
}
closeResultConfigSnapshots() {
this.setState({ resultConfigsModal: false });
}
modifyResultNoColumn(id, result) {
return <Button variant="link" style={{ color: '#047cab' }} onClick={() => this.openResultConfigSnaphots(result)}>{id}</Button>
}
startPintura(configIndex) {
let config = this.state.configs[configIndex];
@ -585,6 +710,7 @@ class Scenario extends React.Component {
}
}
/* ##############################################
* Render method
############################################## */
@ -595,29 +721,78 @@ class Scenario extends React.Component {
return (<Redirect to="/scenarios" />);
}
const buttonStyle = {
marginLeft: '10px'
};
const altButtonStyle = {
marginLeft: '10px',
}
const tableHeadingStyle = {
paddingTop: '30px'
}
const iconStyle = {
color: '#007bff',
height: '25px',
width: '25px'
height: '30px',
width: '30px'
}
if (this.state.scenario === undefined) {
return <h1>Loading Scenario...</h1>;
}
let resulttable;
if (this.state.results && this.state.results.length > 0) {
resulttable = <div>
<Table data={this.state.results}>
<TableColumn
title='Result No.'
dataKey='id'
modifier={(id, result) => this.modifyResultNoColumn(id, result)}
/>
<TableColumn title='Description' dataKey='description' />
<TableColumn title='Created at' dataKey='createdAt' />
<TableColumn title='Last update' dataKey='updatedAt' />
<TableColumn
title='Files/Data'
dataKey='resultFileIDs'
linkKey='filebuttons'
data={this.state.files}
width='300'
onDownload={(index) => this.downloadResultData(index)}
/>
<TableColumn
title='Options'
width='300'
editButton
downloadAllButton
deleteButton
onEdit={index => this.setState({ editResultsModal: true, modalResultsIndex: index })}
onDownloadAll={(index) => this.downloadResultData(this.state.results[index])}
onDelete={(index) => this.setState({ deleteResultsModal: true, modalResultsData: this.state.results[index], modalResultsIndex: index })}
/>
</Table>
<EditResultDialog
sessionToken={this.state.sessionToken}
show={this.state.editResultsModal}
files={this.state.files}
results={this.state.results}
resultId={this.state.modalResultsIndex}
scenarioID={this.state.scenario.id}
onClose={this.closeEditResultsModal.bind(this)} />
<DeleteDialog title="result" name={this.state.modalResultsData.id} show={this.state.deleteResultsModal} onClose={(e) => this.closeDeleteResultsModal(e)} />
<ResultConfigDialog
show={this.state.resultConfigsModal}
configs={this.state.modalResultConfigs}
resultNo={this.state.modalResultConfigsIndex}
onClose={this.closeResultConfigSnapshots.bind(this)}
/>
</div>
}
return <div className='section'>
<div className='section-buttons-group-right'>
<OverlayTrigger key={0} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"file"}`}> Add, edit or delete files of scenario </Tooltip>} >
<Button key={0} variant='light' size="lg" onClick={this.onEditFiles.bind(this)} style={buttonStyle}>
<Icon icon="file" style={iconStyle} />
<Button variant='light' key={0} size="lg" onClick={this.onEditFiles.bind(this)}>
<Icon icon="file" classname={'icon-color'} style={iconStyle}/>
</Button>
</OverlayTrigger>
</div>
@ -632,12 +807,29 @@ class Scenario extends React.Component {
scenarioID={this.state.scenario.id}
/>
{/*Component Configurations table*/}
<h2 style={tableHeadingStyle}>Component Configurations</h2>
<h2 style={tableHeadingStyle}>Component Configurations
<span className='icon-button'>
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add Component Configuration </Tooltip>} >
<Button variant='light' onClick={() => this.addConfig()} style={altButtonStyle}><Icon icon="plus" classname={'icon-color'} style={iconStyle} /></Button>
</OverlayTrigger>
<OverlayTrigger
key={2}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"import"}`}> Import Component Configuration </Tooltip>} >
<Button variant='light' onClick={() => this.setState({ importConfigModal: true })} style={altButtonStyle}><Icon icon="upload" classname={'icon-color'} style={iconStyle}/></Button>
</OverlayTrigger>
</span>
</h2>
<Table data={this.state.configs}>
<TableColumn checkbox onChecked={(index, event) => this.onConfigChecked(index, event)} width='30' />
<TableColumn
checkbox
checkboxDisabled={(index) => this.usesExternalIC(index)}
onChecked={(index, event) => this.onConfigChecked(index, event)}
width='30' />
<TableColumn title='Name' dataKey='name' />
<TableColumn title='Configuration file(s)' dataKey='fileIDs' modifier={(fileIDs) => this.getListOfFiles(fileIDs, ['json', 'JSON'])} />
<TableColumn
@ -679,25 +871,27 @@ class Scenario extends React.Component {
/>
</Table>
<div style={{ float: 'left' }}>
<ICAction
runDisabled={this.state.selectedConfigs.length === 0}
runAction={(action, delay) => this.runAction(action, delay)}
actions={[
{ id: '-1', title: 'Select command', data: { action: 'none' } },
{ id: '0', title: 'Start', data: { action: 'start' } },
{ id: '1', title: 'Stop', data: { action: 'stop' } },
{ id: '2', title: 'Pause', data: { action: 'pause' } },
{ id: '3', title: 'Resume', data: { action: 'resume' } }
]} />
</div>
{this.state.ExternalICInUse ? (
<div style={{ float: 'left' }}>
<ICAction
hasConfigs={true}
ics={this.state.ics}
configs={this.state.configs}
selectedConfigs = {this.state.selectedConfigs}
snapshotConfig = {(index) => this.copyConfig(index)}
token = {this.state.sessionToken}
actions={[
{ id: '-1', title: 'Action', data: { action: 'none' } },
{ id: '0', title: 'Start', data: { action: 'start' } },
{ id: '1', title: 'Stop', data: { action: 'stop' } },
{ id: '2', title: 'Pause', data: { action: 'pause' } },
{ id: '3', title: 'Resume', data: { action: 'resume' } }
]} />
</div>
) : (<div />)
}
<div style={{ float: 'right' }}>
<Button onClick={() => this.addConfig()} style={buttonStyle}><Icon icon="plus" /> Component Configuration</Button>
<Button onClick={() => this.setState({ importConfigModal: true })} style={buttonStyle}><Icon icon="upload" /> Import</Button>
</div>
<div style={{ clear: 'both' }} />
< div style={{ clear: 'both' }} />
<EditConfigDialog
show={this.state.editConfigModal}
@ -729,7 +923,22 @@ class Scenario extends React.Component {
/>
{/*Dashboard table*/}
<h2 style={tableHeadingStyle}>Dashboards</h2>
<h2 style={tableHeadingStyle}>Dashboards
<span className='icon-button'>
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add Dashboard </Tooltip>} >
<Button variant='light' onClick={() => this.setState({ newDashboardModal: true })} style={altButtonStyle}><Icon icon="plus" classname={'icon-color'} style={iconStyle} /></Button>
</OverlayTrigger>
<OverlayTrigger
key={2}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"import"}`}> Import Dashboard </Tooltip>} >
<Button variant='light' onClick={() => this.setState({ importDashboardModal: true })} style={altButtonStyle}><Icon icon="upload" classname={'icon-color'} style={iconStyle} /></Button>
</OverlayTrigger>
</span>
</h2>
<Table data={this.state.dashboards}>
<TableColumn title='Name' dataKey='name' link='/dashboards/' linkKey='id' />
<TableColumn title='Grid' dataKey='grid' />
@ -747,19 +956,25 @@ class Scenario extends React.Component {
/>
</Table>
<div style={{ float: 'right' }}>
<Button onClick={() => this.setState({ newDashboardModal: true })} style={buttonStyle}><Icon icon="plus" /> Dashboard</Button>
<Button onClick={() => this.setState({ importDashboardModal: true })} style={buttonStyle}><Icon icon="upload" /> Import</Button>
</div>
<div style={{ clear: 'both' }} />
<NewDashboardDialog show={this.state.newDashboardModal} onClose={data => this.closeNewDashboardModal(data)} />
<EditDashboardDialog show={this.state.dashboardEditModal} dashboard={this.state.modalDashboardData} onClose={data => this.closeEditDashboardModal(data)} />
<ImportDashboardDialog show={this.state.importDashboardModal} onClose={data => this.closeImportDashboardModal(data)} />
<DeleteDialog title="dashboard" name={this.state.modalDashboardData.name} show={this.state.deleteDashboardModal} onClose={(e) => this.closeDeleteDashboardModal(e)} />
{/*Result table*/}
<h2 style={tableHeadingStyle}>Results
<span className='icon-button'>
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add Result </Tooltip>} >
<Button variant='light' onClick={() => this.setState({ newResultModal: true })} style={altButtonStyle}><Icon icon="plus" classname={'icon-color'} style={iconStyle} /></Button>
</OverlayTrigger>
</span>
</h2>
{resulttable}
<NewResultDialog show={this.state.newResultModal} onClose={data => this.closeNewResultModal(data)} />
{/*Scenario Users table*/}
<h2 style={tableHeadingStyle}>Users sharing this scenario</h2>
@ -783,11 +998,15 @@ class Scenario extends React.Component {
type="text"
/>
<InputGroup.Append>
<span className='icon-button'>
<Button
variant='light'
type="submit"
style={altButtonStyle}
onClick={() => this.addUser()}>
Add User
<Icon icon="plus" classname={'icon-color'} style={iconStyle} />
</Button>
</span>
</InputGroup.Append>
</InputGroup><br /><br />
</div>

View file

@ -108,6 +108,12 @@ class ScenariosDataManager extends RestDataManager {
token: token,
param: '?scenarioID=' + scenario.id,
});
AppDispatcher.dispatch({
type: 'results/start-load',
token: token,
param: '?scenarioID=' + scenario.id
})
}
}
}

View file

@ -17,7 +17,7 @@
import React, { Component } from 'react';
import { Container } from 'flux/utils';
import { Button } from 'react-bootstrap';
import {Button, OverlayTrigger, Tooltip} from 'react-bootstrap';
import FileSaver from 'file-saver';
import AppDispatcher from '../common/app-dispatcher';
@ -236,12 +236,32 @@ class Scenarios extends Component {
render() {
const buttonStyle = {
marginLeft: '10px'
marginLeft: '10px',
};
const iconStyle = {
height: '30px',
width: '30px'
}
return (
<div className='section'>
<h1>Scenarios</h1>
<h1>Scenarios
<span className='icon-button'>
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add Scenario </Tooltip>} >
<Button variant='light' onClick={() => this.setState({ newModal: true })} style={buttonStyle}><Icon icon="plus" classname='icon-color' style={iconStyle} /></Button>
</OverlayTrigger>
<OverlayTrigger
key={2}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"import"}`}> Import Scenario </Tooltip>} >
<Button variant='light' onClick={() => this.setState({ importModal: true })} style={buttonStyle}><Icon icon="upload" classname='icon-color' style={iconStyle} /></Button>
</OverlayTrigger>
</span>
</h1>
<Table data={this.state.scenarios}>
<TableColumn title='Name' dataKey='name' link='/scenarios/' linkKey='id' />
@ -260,13 +280,6 @@ class Scenarios extends Component {
/>
</Table>
<div style={{ float: 'right' }}>
<Button onClick={() => this.setState({ newModal: true })} style={buttonStyle}><Icon icon="plus" /> Scenario</Button>
<Button onClick={() => this.setState({ importModal: true })} style={buttonStyle}><Icon icon="upload" /> Import</Button>
</div>
<div style={{ clear: 'both' }} />
<NewScenarioDialog show={this.state.newModal} onClose={(data) => this.closeNewModal(data)} />
<EditScenarioDialog show={this.state.editModal} onClose={(data) => this.closeEditModal(data)} scenario={this.state.modalScenario} />
<ImportScenarioDialog show={this.state.importModal} onClose={data => this.closeImportModal(data)} nodes={this.state.nodes} />

View file

@ -17,7 +17,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button, FormGroup, FormLabel, FormText} from 'react-bootstrap';
import {Button, FormGroup, FormLabel, FormText, OverlayTrigger, Tooltip} from 'react-bootstrap';
import {Collapse} from 'react-collapse';
import Table from '../common/table';
import TableColumn from '../common/table-column';
@ -64,6 +64,11 @@ class EditSignalMapping extends React.Component {
}
}
signals.forEach(signal => {
if(signal.checked === undefined) signal.checked = false
});
return {
signals: signals,
};
@ -92,23 +97,24 @@ class EditSignalMapping extends React.Component {
let signals = this.state.signals;
let modifiedSignals = this.state.modifiedSignalIDs;
if (column === 1) { // Name change
console.log("HandleMappingChange", row, column)
if (column === 2) { // Name change
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
} else if (column === 3) { // unit change
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
} else if (column === 4) { // 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
} else if (column === 1) { //index change
console.log("Index change")
signals[row].index =parseInt(event.target.value, 10);
if (modifiedSignals.find(id => id === signals[row].id) === undefined){
modifiedSignals.push(signals[row].id);
@ -134,6 +140,21 @@ class EditSignalMapping extends React.Component {
};
handleRemove = () => {
let checkedSignals = this.state.signals.filter(signal => signal.checked === true);
checkedSignals.forEach(signal => {
AppDispatcher.dispatch({
type: 'signals/start-remove',
data: signal,
token: this.props.sessionToken
});
})
}
handleAdd = (configID = null) => {
if(typeof this.props.configs !== "undefined"){
@ -173,10 +194,38 @@ class EditSignalMapping extends React.Component {
this.setState({signals: signals})
}
onSignalChecked(signal) {
let tempSignals = this.state.signals;
const index = tempSignals.indexOf(signal);
tempSignals[index].checked = !tempSignals[index].checked;
this.setState({signals: tempSignals});
}
checkAll(){
let tempSignals = this.state.signals;
let allChecked = true;
tempSignals.forEach(signal =>
{
if(signal.checked === false){
signal.checked = true;
allChecked = false;
}
});
if(allChecked){
tempSignals.forEach(signal => signal.checked = false);
}
this.setState({signals: tempSignals});
}
render() {
const buttonStyle = {
marginLeft: '10px'
marginLeft: '10px',
};
return(
@ -185,7 +234,7 @@ class EditSignalMapping extends React.Component {
show={this.props.show}
title="Edit Signal Mapping"
buttonTitle="Save"
blendOutCancel = {true}
blendOutCancel = {false}
onClose={(c) => this.onClose(c)}
onReset={() => this.resetState()}
valid={true}
@ -194,7 +243,8 @@ class EditSignalMapping extends React.Component {
<FormGroup>
<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}>
<Table checkbox onChecked={(signal) => this.onSignalChecked(signal)} data={this.state.signals}>
<TableColumn checkbox onChecked={(index, event) => this.onSignalChecked(index, event)} checkboxKey='checked' width='30' />
<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)} />
@ -202,20 +252,24 @@ class EditSignalMapping extends React.Component {
<TableColumn title='Remove' deleteButton onDelete={(index) => this.handleDelete(index)} />
</Table>
<div style={{ float: 'right' }}>
<Button key={50} onClick={() => this.handleAdd()} style={buttonStyle}><Icon icon="plus" /> Signal</Button>
<div className='solid-button' style={{ float: 'right' }}>
<OverlayTrigger key={0} placement={'left'} overlay={<Tooltip id={`tooltip-${"check"}`}> Check/Uncheck All </Tooltip>} >
<Button variant='secondary' key={50} onClick={() => this.checkAll()} style={buttonStyle}> <Icon icon="check" /> </Button>
</OverlayTrigger>
<Button variant='secondary' key={51} onClick={() => this.handleRemove()} style={buttonStyle}> Remove </Button>
<Button variant='secondary' key={52} onClick={() => this.handleAdd()} style={buttonStyle}><Icon icon="plus" /> Signal </Button>
</div>
<div>
<Collapse isOpened={this.state.openCollapse}>
<h6>Choose a Component Configuration to add the signal to: </h6>
<div>
<div className='solid-button'>
{typeof this.props.configs !== "undefined" && this.props.configs.map(config => (
<Button key ={config.id} onClick={() => this.handleAdd(config.id)} style={buttonStyle}>{config.name}</Button>
<Button variant='secondary' key ={config.id} onClick={() => this.handleAdd(config.id)} style={buttonStyle}>{config.name}</Button>
))}
</div>
</Collapse>
</Collapse>
</div>
</FormGroup>
</Dialog>

View file

@ -27,11 +27,9 @@ class SignalStore extends ArrayStore{
reduce(state, action) {
switch (action.type) {
case 'signals/added':
this.dataManager.reloadConfig(action.token, action.data);
return super.reduce(state, action);
case 'signals/removed':
this.dataManager.reloadConfig(action.token, action.data);
this.dataManager.reloadConfig(action.token, action.data.configID);
return super.reduce(state, action);
case 'signals/start-autoconfig':
@ -41,7 +39,6 @@ class SignalStore extends ArrayStore{
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':

View file

@ -27,9 +27,9 @@ class SignalsDataManager extends RestDataManager{
super('signal', '/signals');
}
reloadConfig(token, data){
reloadConfig(token, id){
// request in signals
RestAPI.get(this.makeURL('/configs/' + data.configID), token).then(response => {
RestAPI.get(this.makeURL('/configs/' + id), token).then(response => {
AppDispatcher.dispatch({
type: 'configs/edited',
data: response.config

View file

@ -112,6 +112,13 @@ body {
}
}
.verticalhorizontal {
display: flex;
justify-content: center;
align-items: center;
height: 800px;
}
/**
* Menus
*/
@ -219,6 +226,26 @@ body {
background-color: white;
}
/**
* Login select
*/
.login-select {
position: sticky;
width: 300px;
height: 150px;
top: 50%;
left: 50%;
margin-top: 50px;
margin-bottom: 100px;
transform: translate(-50%);
padding: 20px 20px;
background-color: #a8c7cf;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 9px 18px 0 rgba(0, 0, 0, 0.1);
}
/**
* Login form
@ -234,6 +261,13 @@ body {
0 9px 18px 0 rgba(0, 0, 0, 0.1);
}
hr {
margin-top: 1rem;
margin-bottom: 1rem;
border:0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
/**
* Tables
*/
@ -323,6 +357,7 @@ body {
display: -webkit-flex;
display: flex;
flex-direction: column;
background-color: #FFFFFF;
}
.box-header {
@ -368,6 +403,18 @@ body {
height: auto !important;
padding: 5px;
float: right;
border-color: #ffffff;
background-color: #ffffff;
}
.section-buttons-group-right .btn{
border-color: #ffffff;
background-color: #ffffff;
}
.section-buttons-group-right .btn:hover{
border-color: #e3e3e3;
background-color: #e3e3e3;
}
.section-buttons-group-left {
@ -376,10 +423,60 @@ body {
float: left;
}
.section-buttons-group-left .btn{
background-color: #527984;
border-color: #527984;
}
.section-buttons-group-left .btn:hover{
background-color: #31484f;
border-color: #31484f;
}
.drag-and-drop .btn{
color: #527984;
border-color: #527984;
}
.drag-and-drop .btn:hover{
color: #527984;
border-color: #527984;
}
.section-buttons-group-right .rc-slider {
margin-left: 12px;
}
.solid-button .btn{
background-color: #527984;
border-color: #527984;
}
.solid-button .btn:hover{
background-color: #31484f;
border-color: #31484f;
}
.solid-button .btn:disabled{
background-color: #527984;
border-color: #527984;
}
.icon-button .btn{
border-color: #ffffff;
background-color: #ffffff;
}
.icon-button .btn:hover{
border-color: #e3e3e3;
background-color: #e3e3e3;
}
.icon-color {
color: #527984;
}
.form-horizontal .form-group {
margin-left: 0 !important;
margin-right: 0 !important;

View file

@ -0,0 +1,3 @@
.swagger-ui div.scheme-container {
display: none
}

View file

@ -398,6 +398,8 @@ div[class*="-widget"] label {
/* Begin time offset widget */
.time-offset {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-around;

107
src/user/login-complete.js Normal file
View file

@ -0,0 +1,107 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { Redirect } from 'react-router-dom';
import AppDispatcher from '../common/app-dispatcher';
import LoginStore from './login-store'
import { Container } from 'flux/utils';
class LoginComplete extends React.Component {
constructor(props) {
console.log("LoginComplete constructor");
super(props);
AppDispatcher.dispatch({
type: 'users/extlogin',
});
this.state = {
loginMessage: '',
token: '',
currentUser: '',
secondsToWait: 99,
}
this.timer = 0;
this.startTimer = this.startTimer.bind(this);
this.countDown = this.countDown.bind(this);
this.stopTimer = this.stopTimer.bind(this);
}
componentDidMount() {
this.startTimer();
this.setState({secondsToWait: 5});
}
static getStores(){
return [LoginStore]
}
static calculateState(prevState, props) {
// We need to work with the login store here to trigger the re-render upon state change after login
// Upon successful login, the token and currentUser are stored in the local storage as strings
return {
loginMessage: LoginStore.getState().loginMessage,
token: LoginStore.getState().token,
currentUser: LoginStore.getState().currentUser,
}
}
stopTimer() {
console.log("stop timer");
clearInterval(this.timer);
}
startTimer() {
if (this.timer == 0 && this.state.secondsToWait > 0) {
// call function 'countDown' every 1000ms
this.timer = setInterval(this.countDown, 1000);
}
}
countDown() {
let seconds = this.state.secondsToWait - 1;
this.setState({secondsToWait: seconds});
// waiting time over, stop counting down
if (seconds == 0) {
clearInterval(this.timer);
}
}
render() {
if (this.state.currentUser && this.state.currentUser !== "") {
this.stopTimer();
return (<Redirect to="/home" />);
}
else if (this.state.secondsToWait == 0) {
this.stopTimer();
return (<Redirect to="/login" />);
} else {
return (<div class="verticalhorizontal">
<img style={{height: 300}}src={require('../img/dog-waiting-bw.jpg').default} alt="Waiting Dog" /></div>);
}
}
}
let fluxContainerConverter = require('../common/FluxContainerConverter');
export default Container.create(fluxContainerConverter.convert(LoginComplete));

View file

@ -19,6 +19,8 @@ import React, { Component } from 'react';
import { Form, Button, FormGroup, FormControl, FormLabel, Col } from 'react-bootstrap';
import RecoverPassword from './recover-password'
import AppDispatcher from '../common/app-dispatcher';
import _ from 'lodash';
class LoginForm extends Component {
constructor(props) {
@ -56,28 +58,28 @@ class LoginForm extends Component {
this.setState({ [event.target.id]: event.target.value, disableLogin });
}
openRecoverPassword(){
this.setState({forgottenPassword: true});
openRecoverPassword() {
this.setState({ forgottenPassword: true });
}
closeRecoverPassword(){
this.setState({forgottenPassword: false});
closeRecoverPassword() {
this.setState({ forgottenPassword: false });
}
render() {
villaslogin() {
return (
<Form>
<Form key="login_a">
<FormGroup controlId="username">
<FormLabel column={true}>Username</FormLabel>
<Col>
<FormControl type="text" placeholder="Username" onChange={(e) => this.handleChange(e)} />
<FormControl type="text" placeholder="Username" autoComplete="username" onChange={(e) => this.handleChange(e)} />
</Col>
</FormGroup>
<FormGroup controlId="password">
<FormLabel column={true}>Password</FormLabel>
<Col >
<FormControl type="password" placeholder="Password" onChange={(e) => this.handleChange(e)} />
<FormControl type="password" placeholder="Password" autoComplete="current-password" onChange={(e) => this.handleChange(e)} />
</Col>
</FormGroup>
@ -89,19 +91,40 @@ class LoginForm extends Component {
</div>
}
<FormGroup style={{paddingTop: 15, paddingBottom: 5}}>
<FormGroup style={{ paddingTop: 15, paddingBottom: 5 }}>
<Col>
<Button style={{width: 90}} type="submit" disabled={this.state.disableLogin} onClick={e => this.login(e)}>Login</Button>
<span className='solid-button'>
<Button variant='secondary' style={{width: 90}} type="submit" disabled={this.state.disableLogin} onClick={e => this.login(e)}>Login</Button>
</span>
<Button variant="link" size="sm" style={{marginLeft: 85}} onClick={() => this.openRecoverPassword()}>Forgot your password?</Button>
</Col>
</FormGroup>
<RecoverPassword show={this.state.forgottenPassword} onClose={() => this.closeRecoverPassword()} sessionToken={this.props.sessionToken} />
</Form>
);
}
render() {
let villasLogin = this.villaslogin();
if (this.props.config) {
let externalLogin = _.get(this.props.config, ['authentication', 'external', 'enabled'])
let provider = _.get(this.props.config, ['authentication', 'external', 'provider_name'])
let url = _.get(this.props.config, ['authentication', 'external', 'authorize_url']) + "?rd=/login/complete"
if (externalLogin && provider && url) {
return [
villasLogin,
<hr key="login_b"/>,
<Button key="login_c" onClick={e => window.location = url } block>Sign in with {provider}</Button>
];
}
}
return villasLogin;
}
}
export default LoginForm;

View file

@ -20,6 +20,7 @@ import { ReduceStore } from 'flux/utils';
import AppDispatcher from '../common/app-dispatcher';
import UsersDataManager from './users-data-manager';
import ICDataDataManager from '../ic/ic-data-data-manager';
import ConfigReader from '../config-reader';
class LoginStore extends ReduceStore {
constructor() {
@ -31,15 +32,30 @@ class LoginStore extends ReduceStore {
currentUser: null,
token: null,
loginMessage: null,
config: null,
};
}
reduce(state, action) {
switch (action.type) {
case 'config/load':
ConfigReader.loadConfig();
return state;
case 'config/loaded':
return Object.assign({}, state, { config: action.data });
case 'config/load-error':
return Object.assign({}, state, { config: null});
case 'users/login':
UsersDataManager.login(action.username, action.password);
return Object.assign({}, state, { loginMessage: null });
case 'users/extlogin':
UsersDataManager.login();
return Object.assign({}, state, { loginMessage: null });
case 'users/logout':
// disconnect from all infrastructure components
ICDataDataManager.closeAll();

View file

@ -26,10 +26,20 @@ import Header from '../common/header';
import Footer from '../common/footer';
import NotificationsDataManager from '../common/data-managers/notifications-data-manager';
import LoginStore from './login-store'
import AppDispatcher from '../common/app-dispatcher';
class Login extends Component {
constructor(props) {
super(props);
static getStores(){
// load config in case the user goes directly to /login
// otherwise it will be loaded in app constructor
AppDispatcher.dispatch({
type: 'config/load',
});
}
static getStores() {
return [LoginStore]
}
@ -40,6 +50,7 @@ class Login extends Component {
loginMessage: LoginStore.getState().loginMessage,
token: LoginStore.getState().token,
currentUser: LoginStore.getState().currentUser,
config: LoginStore.getState().config,
}
}
@ -62,7 +73,7 @@ class Login extends Component {
<div className="login-container">
<NavbarBrand>Login</NavbarBrand>
<LoginForm loginMessage={this.state.loginMessage} />
<LoginForm loginMessage={this.state.loginMessage} config={this.state.config}/>
</div>
<Footer />

View file

@ -17,10 +17,10 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import AppDispatcher from '../common/app-dispatcher';
class Logout extends React.Component {
componentDidMount() {
AppDispatcher.dispatch({
type: 'users/logout'
@ -40,4 +40,4 @@ class Logout extends React.Component {
}
}
export default Logout;
export default Logout;

View file

@ -17,7 +17,7 @@
import React from 'react';
import Dialog from '../common/dialogs/dialog';
import Config from '../config.js';
import Config from '../config';
class RecoverPassword extends React.Component {
@ -29,13 +29,10 @@ class RecoverPassword extends React.Component {
}
}
onClose() {
this.props.onClose();
}
render() {
return (
<Dialog show={this.props.show} title="Recover password" buttonTitle="Close" onClose={(c) => this.onClose(c)} blendOutCancel = {true} valid={true} size = 'lg'>
@ -43,7 +40,7 @@ class RecoverPassword extends React.Component {
<div>Please contact:</div>
<div>{this.state.admin.name}</div>
<div>E-Mail:</div>
<a href={`mailto:${this.state.admin.mail}`}>{this.state.admin.mail}</a>
<a href={`mailto:${this.state.admin.mail}`}>{this.state.admin.mail}</a>
</div>
</Dialog>
);

View file

@ -17,7 +17,7 @@
import React, { Component } from 'react';
import { Container } from 'flux/utils';
import {Button, Col, Row} from 'react-bootstrap';
import {Button, Col, Row, FormGroup} from 'react-bootstrap';
import AppDispatcher from '../common/app-dispatcher';
import UsersStore from './users-store';
@ -112,6 +112,7 @@ class User extends Component {
render() {
return (
<div>
<h1>Your User Account</h1>
@ -119,27 +120,25 @@ class User extends Component {
{this.state.currentUser !== undefined && this.state.currentUser !== null ?
<form>
<Row>
<Col xs={3}>Username: </Col>
<Col xs={3}> {this.state.currentUser.username} </Col>
<FormGroup as={Col} sm={2} controlId="details">
<div style={{ alignItems: 'right' }}>Username:</div>
<div style={{ alignItems: 'right' }}>E-mail:</div>
<div style={{ alignItems: 'right' }}>Role:</div>
</FormGroup>
<FormGroup as={Col} sm={3} controlId="information" >
<div> {this.state.currentUser.username}</div>
<div>{this.state.currentUser.mail}</div>
<div>{this.state.currentUser.role}</div>
<span className='icon-button'>
<Button variant='light' size='lg' variant='light' style={{margin: '10px'}} onClick={() => this.setState({ editModal: true })}><Icon size='lg' classname='icon-color' icon='edit' /> </Button>
</span>
</FormGroup>
</Row>
<Row as={Col}>
<Col xs={3}>E-mail: </Col>
<Col xs={3}> {this.state.currentUser.mail} </Col>
</Row>
<Row as={Col}>
<Col xs={3}>Role: </Col>
<Col xs={3}> {this.state.currentUser.role} </Col>
</Row>
<Button onClick={() => this.setState({editModal: true})}><Icon icon='edit'/> Edit</Button>
<EditOwnUserDialog show={this.state.editModal} onClose={(data) => this.closeEditModal(data)}
user={this.state.currentUser}/>
user={this.state.currentUser} />
</form> : "Loading user data..."
}
@ -148,8 +147,5 @@ class User extends Component {
}
}
let fluxContainerConverter = require('../common/FluxContainerConverter');
export default Container.create(fluxContainerConverter.convert(User));

View file

@ -25,18 +25,34 @@ class UsersDataManager extends RestDataManager {
}
login(username, password) {
RestAPI.post(this.makeURL('/authenticate'), { username: username, password: password }).then(response => {
AppDispatcher.dispatch({
type: 'users/logged-in',
token: response.token,
currentUser: response.user,
if (username && password) {
RestAPI.post(this.makeURL('/authenticate/internal'), { username: username, password: password }).then(response => {
AppDispatcher.dispatch({
type: 'users/logged-in',
token: response.token,
currentUser: response.user,
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'users/login-error',
error: error
});
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'users/login-error',
error: error
} else { // external authentication
RestAPI.post(this.makeURL('/authenticate/external'),).then(response => {
console.log(response);
AppDispatcher.dispatch({
type: 'users/logged-in',
token: response.token,
currentUser: response.user,
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'users/login-error',
error: error
});
});
});
}
}
}

View file

@ -17,7 +17,7 @@
import React, { Component } from 'react';
import { Container } from 'flux/utils';
import { Button } from 'react-bootstrap';
import {Button, OverlayTrigger, Tooltip} from 'react-bootstrap';
import AppDispatcher from '../common/app-dispatcher';
import UsersStore from './users-store';
@ -130,9 +130,27 @@ class Users extends Component {
render() {
const buttonStyle = {
marginLeft: '10px',
};
const iconStyle = {
height: '30px',
width: '30px'
}
return (
<div>
<h1>Users</h1>
<h1>Users
<span className='icon-button'>
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add User </Tooltip>} >
<Button variant='light' style={buttonStyle} onClick={() => this.setState({ newModal: true })}><Icon icon='plus' classname='icon-color' style={iconStyle} /> </Button>
</OverlayTrigger>
</span>
</h1>
<Table data={this.state.users}>
<TableColumn title='Username' width='150' dataKey='username' />
@ -143,8 +161,6 @@ class Users extends Component {
<TableColumn width='200' editButton deleteButton onEdit={index => this.setState({ editModal: true, modalData: this.state.users[index] })} onDelete={index => this.setState({ deleteModal: true, modalData: this.state.users[index] })} />
</Table>
<Button onClick={() => this.setState({ newModal: true })}><Icon icon='plus' /> User</Button>
<NewUserDialog show={this.state.newModal} onClose={(data) => this.closeNewModal(data)} />
<EditUserDialog show={this.state.editModal} onClose={(data) => this.closeEditModal(data)} user={this.state.modalData} />

View file

@ -175,6 +175,15 @@ class EditWidgetColorZonesControl extends React.Component {
render() {
const buttonStyle = {
marginBottom: '10px',
marginLeft: '120px',
};
const iconStyle = {
height: '25px',
width : '25px'
}
let tempColor = 'FFFFFF';
let collapse = false;
@ -191,9 +200,10 @@ class EditWidgetColorZonesControl extends React.Component {
}
return <FormGroup>
<FormLabel>Color Zones</FormLabel>
<Button onClick={this.addZone} style={{marginBottom: '10px', marginLeft: '120px'}} disabled={!this.props.widget.customProperties.colorZones}><Icon size='xs' icon="plus" /></Button>
<FormLabel>Color zones</FormLabel>
<span className='icon-button'>
<Button variant='light' onClick={this.addZone} style={buttonStyle} disabled={!this.props.widget.customProperties.colorZones}><Icon icon="plus" className='icon-color' style={iconStyle} /></Button>
</span>
<div>
{
this.state.widget.customProperties.zones.map((zone, idx) => {
@ -243,7 +253,9 @@ class EditWidgetColorZonesControl extends React.Component {
</tr>
</tbody>
</Table>
<Button onClick={this.removeZones}><Icon size='xs' icon="trash-alt" /></Button>
<span className='icon-button'>
<Button variant='light' onClick={this.removeZones} ><Icon style={iconStyle} classname='icon-color' icon="trash-alt" /></Button>
</span>
</Collapse>
<ColorPicker show={this.state.showColorPicker} onClose={(data) => this.closeEditModal(data)} widget={this.state.widget} zoneIndex={this.state.selectedIndex} controlId={'strokeStyle'} />

View file

@ -170,7 +170,8 @@ export default function CreateControls(widgetType = null, widget = null, session
<EditWidgetNumberControl key={1} widget={widget} controlId={'customProperties.threshold_yellow'} label={'Threshold yellow'} defaultValue={0} handleChange={(e) => handleChange(e)} />,
<EditWidgetNumberControl key={2} widget={widget} controlId={'customProperties.threshold_red'} label={'Threshold red'} defaultValue={0} handleChange={(e) => handleChange(e)} />,
<EditWidgetCheckboxControl key={3} widget={widget} controlId={'customProperties.horizontal'} input text="Horizontal" handleChange={e => handleChange(e)} />,
<EditWidgetCheckboxControl key={4} widget={widget} controlId={'customProperties.showOffset'} input text="showOffset" handleChange={e => handleChange(e)} />,
<EditWidgetCheckboxControl key={4} widget={widget} controlId={'customProperties.showName'} input text="showName" handleChange={e => handleChange(e)} />,
<EditWidgetCheckboxControl key={5} widget={widget} controlId={'customProperties.showOffset'} input text="showOffset" handleChange={e => handleChange(e)} />,
);
break;

View file

@ -19,6 +19,8 @@ import React, { Component } from 'react';
import { FormGroup, OverlayTrigger, Tooltip , FormLabel, Button } from 'react-bootstrap';
import ColorPicker from './color-picker'
import Icon from "../../common/icon";
import {scaleOrdinal} from "d3-scale";
import {schemeCategory10} from "d3-scale-chromatic";
// schemeCategory20 no longer available in d3
@ -36,8 +38,20 @@ class EditWidgetPlotColorsControl extends Component {
}
static getDerivedStateFromProps(props, state){
let widget = props.widget;
if(widget.customProperties.lineColors === undefined || widget.customProperties.lineColors === null){
// for backwards compatibility with old plots
widget.customProperties.lineColors = []
const newLineColor = scaleOrdinal(schemeCategory10);
for (let signalID of widget.signalIDs){
widget.customProperties.lineColors.push(newLineColor(signalID))
}
}
return {
widget: props.widget
widget: widget
};
}
@ -62,9 +76,9 @@ class EditWidgetPlotColorsControl extends Component {
this.setState({selectedIndex: null});
}
}
render() {
return (
<FormGroup>
<FormLabel>Line Colors</FormLabel>
@ -81,7 +95,7 @@ class EditWidgetPlotColorsControl extends Component {
}
let signal = this.props.signals.find(signal => signal.id === signalID);
return (<OverlayTrigger key={idx} placement={'bottom'} overlay={<Tooltip id={'tooltip-${"signal name"}'}>{signal.name}</Tooltip>}>
<Button
style={style} key={idx} onClick={i => this.editLineColor(signalID)} ><Icon icon="pen" /></Button>

View file

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

View file

@ -168,6 +168,7 @@ class WidgetFactory {
widget.customProperties.valueMin = 0;
widget.customProperties.valueMax = 1;
widget.customProperties.valueUseMinMax = false;
widget.customProperties.lockAspect = true;
break;
case 'Box':
widget.minWidth = 50;
@ -199,16 +200,17 @@ class WidgetFactory {
break;
case 'TimeOffset':
widget.minWidth = 20;
widget.minHeight = 20;
widget.width = 100;
widget.height = 40;
widget.minWidth = 200;
widget.minHeight = 80;
widget.width = 200;
widget.height = 80;
widget.customProperties.threshold_yellow = 1;
widget.customProperties.threshold_red = 2;
widget.customProperties.icID = -1;
widget.customProperties.horizontal = true;
widget.customProperties.showOffset = true;
widget.customProperties.lockAspect = true;
widget.customProperties.showName = true;
break;
default:

View file

@ -20,7 +20,7 @@ import { scaleOrdinal} from 'd3-scale';
import {schemeCategory10} from 'd3-scale-chromatic'
function Legend(props){
const signal = props.sig;
const hasScalingFactor = (signal.scalingFactor !== 1);
@ -52,10 +52,15 @@ class PlotLegend extends React.Component {
return <div className="plot-legend">
<ul>
{
{ this.props.lineColors !== undefined && this.props.lineColors != null ? (
this.props.signals.map( signal =>
<Legend key={signal.id} sig={signal} lineColor={this.props.lineColors[signal.id]}/>
)}
)) : (
this.props.signals.map( signal =>
<Legend key={signal.id} sig={signal} lineColor={"undefined"}/>
))
}
</ul>
</div>;
}

View file

@ -207,6 +207,11 @@ class Plot extends React.Component {
const lines = this.state.data.map((values, index) => {
let signalID = this.props.signalIDs[index];
if(this.props.lineColors === undefined || this.props.lineColors === null){
this.props.lineColors = [] // for backwards compatibility
}
if (typeof this.props.lineColors[signalID] === "undefined") {
this.props.lineColors[signalID] = newLineColor(signalID);
}

View file

@ -96,11 +96,15 @@ class WidgetToolbox extends React.Component {
const disableDecrease = this.disableDecrease();
// Only one topology widget at the time is supported
const iconStyle = {
color: '#007bff',
height: '25px',
width : '25px'
}
const buttonStyle = {
marginRight: '3px',
height: '40px',
}
const thereIsTopologyWidget = this.props.widgets != null && Object.values(this.props.widgets).filter(w => w.type === 'Topology').length > 0;
const topologyItemMsg = thereIsTopologyWidget? 'Currently only one is supported' : '';
@ -110,16 +114,16 @@ class WidgetToolbox extends React.Component {
<div className='section-buttons-group-left'>
<div>
<OverlayTrigger key={2} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"cosmetic"}`}> Show/ hide available Cosmetic Widgets </Tooltip>} >
<Button variant="light" key={2} style={{ marginRight: '3px', height: '40px' }} onClick={() => this.showWidgets('cosmetic')} >
<Icon icon={cosmeticIcon} style={{color: '#007bff'}}/> Cosmetic Widgets</Button>
<Button key={2} variant="secondary" style={buttonStyle} onClick={() => this.showWidgets('cosmetic')} >
<Icon icon={cosmeticIcon}/> Cosmetic Widgets</Button>
</OverlayTrigger>
<OverlayTrigger key={3} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"displaying"}`}> Show/ hide available Displaying Widgets </Tooltip>} >
<Button variant="light" key={3} style={{ marginRight: '3px', height: '40px' }} onClick={() => this.showWidgets('displaying')} >
<Icon icon={displayingIcon} style={{color: '#007bff'}} /> Displaying Widgets</Button>
<Button key={3} variant="secondary" style={buttonStyle} onClick={() => this.showWidgets('displaying')} >
<Icon icon={displayingIcon}/> Displaying Widgets</Button>
</OverlayTrigger>
<OverlayTrigger key={4} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"manipulation"}`}> Show/ hide available Manipulation Widgets </Tooltip>} >
<Button variant="light" key={2} style={{ marginRight: '3px', height: '40px' }} onClick={() => this.showWidgets('manipulation')} >
<Icon icon={manipulationIcon} style={{color: '#007bff'}} /> Manipulation Widgets</Button>
<Button key={2} variant="secondary" style={buttonStyle} onClick={() => this.showWidgets('manipulation')} >
<Icon icon={manipulationIcon}/> Manipulation Widgets</Button>
</OverlayTrigger>
</div>
</div>
@ -128,13 +132,13 @@ class WidgetToolbox extends React.Component {
<span>Grid: { this.props.grid > 1 ? this.props.grid : 'Disabled' }</span>
<Slider value={this.props.grid} style={{ width: '80px' }} step={5} onChange={this.onGridChange} />
<OverlayTrigger key={0} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"increase"}`}> Increase dashboard height </Tooltip>} >
<Button variant="light" key={0} style={{marginRight: '3px', height: '40px'}} onClick={() => this.props.onDashboardSizeChange(1)} >
<Icon icon="plus" style={iconStyle}/>
<Button variant="light" key={0} style={buttonStyle} onClick={() => this.props.onDashboardSizeChange(1)} >
<Icon icon="plus" classname='icon-color' style={iconStyle}/>
</Button>
</OverlayTrigger>
<OverlayTrigger key={1} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"decrease"}`}> Decrease dashboard height </Tooltip>} >
<Button variant="light" key={1} disabled={disableDecrease} style={{marginRight: '3px', height: '40px'}} onClick={() => this.props.onDashboardSizeChange(-1)} >
<Icon icon="minus" style={iconStyle}/>
<Button variant="light" key={1} disabled={disableDecrease} style={buttonStyle} onClick={() => this.props.onDashboardSizeChange(-1)} >
<Icon icon="minus" classname='icon-color' style={iconStyle}/>
</Button>
</OverlayTrigger>
@ -143,7 +147,7 @@ class WidgetToolbox extends React.Component {
<br></br>
<br></br>
<br></br>
<div className= 'section-buttons-group-left'>
<div className= 'drag-and-drop'>
<span>
<Collapse isOpened={this.state.showCosmeticWidgets} >
<ToolboxItem name='Line' type='widget' icon='plus'/>

View file

@ -66,19 +66,21 @@ class WidgetTimeOffset extends Component {
} else if (this.props.widget.customProperties.showOffset){
icSelected = this.state.timeOffset + 's';
}
return (
<div className="time-offset">
{this.props.widget.customProperties.icID !== -1 ?
(<span></span>) : (<span>no IC</span>)
}
}
{this.props.widget.customProperties.icID !== -1 && this.props.widget.customProperties.showName ?
(<span>{this.state.icName}</span>) : (<span></span>)
}
<OverlayTrigger key={0} placement={'left'} overlay={<Tooltip id={`tooltip-${"traffic-light"}`}>
{this.props.widget.customProperties.icID !== -1 ?
(<span>{this.state.icName}<br></br>Offset: {this.state.timeOffset + "s"}</span>)
:
(<span>Please select Infrastructure Component</span>)}
</Tooltip>}>
<TrafficLight Horizontal={this.props.widget.customProperties.horizontal} width={this.props.widget.width} height={this.props.widget.height}
<TrafficLight Horizontal={this.props.widget.customProperties.horizontal} width={this.props.widget.width - 40} height={this.props.widget.height - 40}
RedOn={(this.props.widget.customProperties.threshold_red <= this.state.timeOffset) || !this.state.websocketOpen}
YellowOn={(this.props.widget.customProperties.threshold_yellow <= this.state.timeOffset) && (this.state.timeOffset < this.props.widget.customProperties.threshold_red) && this.state.websocketOpen}
GreenOn={(this.state.timeOffset < this.props.widget.customProperties.threshold_yellow) && this.state.websocketOpen}