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:
commit
fc0c535d43
72 changed files with 5152 additions and 2516 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -17,3 +17,4 @@ yarn-debug.log*
|
|||
yarn-error.log*
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
package-lock.json
|
||||
|
|
|
@ -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
|
||||
|
|
32
README.md
32
README.md
|
@ -2,39 +2,15 @@
|
|||
[](https://git.rwth-aachen.de/acs/public/villas/web/-/commits/master)
|
||||
[](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
|
||||

|
||||
|
||||
## 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
|
|||
|
||||
[](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)
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
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.
|
|
@ -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
3940
package-lock.json
generated
File diff suppressed because it is too large
Load diff
47
package.json
47
package.json
|
@ -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",
|
||||
|
|
|
@ -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
82
src/common/api-browser.js
Normal 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;
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 Union’s 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" />
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
<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
43
src/config-reader.js
Normal 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();
|
|
@ -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
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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
48
src/ic/confirm-command.js
Normal 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;
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
324
src/ic/ics.js
324
src/ic/ics.js
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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
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
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
208
src/result/edit-result.js
Normal 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
70
src/result/new-result.js
Normal 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;
|
64
src/result/result-configs-dialog.js
Normal file
64
src/result/result-configs-dialog.js
Normal 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;
|
85
src/result/result-store.js
Normal file
85
src/result/result-store.js
Normal 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();
|
79
src/result/results-data-manager.js
Normal file
79
src/result/results-data-manager.js
Normal 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();
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
3
src/styles/swagger-ui.css
Normal file
3
src/styles/swagger-ui.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.swagger-ui div.scheme-container {
|
||||
display: none
|
||||
}
|
|
@ -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
107
src/user/login-complete.js
Normal 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));
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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} />
|
||||
|
||||
|
|
|
@ -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'} />
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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'/>
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Reference in a new issue