mirror of
https://git.rwth-aachen.de/acs/public/villas/web/
synced 2025-03-09 00:00:01 +01:00
Merge branch 'master' into feature-external-auth
This commit is contained in:
commit
948c573236
54 changed files with 2862 additions and 2086 deletions
|
@ -1,6 +1,6 @@
|
|||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: normal
|
||||
DOCKER_TAG: ${CI_COMMIT_SHORT_SHA}
|
||||
DOCKER_TAG: ${CI_COMMIT_BRANCH}
|
||||
DOCKER_IMAGE: ${CI_REGISTRY_IMAGE}
|
||||
|
||||
cache:
|
||||
|
@ -42,8 +42,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.
|
||||
|
||||
|
2717
package-lock.json
generated
2717
package-lock.json
generated
File diff suppressed because it is too large
Load diff
23
package.json
23
package.json
|
@ -5,10 +5,10 @@
|
|||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.12",
|
||||
"@fortawesome/react-fontawesome": "^0.1.13",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bootstrap": "^4.5.3",
|
||||
"bufferutil": "^4.0.1",
|
||||
"bufferutil": "^4.0.2",
|
||||
"canvas": "^2.6.1",
|
||||
"classnames": "^2.2.6",
|
||||
"d3-array": "^2.8.0",
|
||||
|
@ -20,7 +20,7 @@
|
|||
"d3-time-format": "^3.0.0",
|
||||
"es6-promise": "^4.2.8",
|
||||
"fibers": "^5.0.0",
|
||||
"file-saver": "^2.0.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"flux": "^3.1.3",
|
||||
"gaugeJS": "^1.3.7",
|
||||
"handlebars": "^4.7.6",
|
||||
|
@ -29,15 +29,15 @@
|
|||
"libcimsvg": "git+https://git.rwth-aachen.de/acs/public/cim/pintura-npm-package.git",
|
||||
"lodash": "^4.17.20",
|
||||
"moment": "^2.29.1",
|
||||
"multiselect-react-dropdown": "^1.6.1",
|
||||
"multiselect-react-dropdown": "^1.6.2",
|
||||
"node-sass": "^4.14.1",
|
||||
"popper.js": "^1.16.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"rc-slider": "^9.6.0",
|
||||
"rc-slider": "^9.6.4",
|
||||
"react": "^16.14.0",
|
||||
"react-bootstrap": "^1.4.0",
|
||||
"react-bootstrap-time-picker": "^2.0.1",
|
||||
"react-collapse": "^5.0.1",
|
||||
"react-collapse": "^5.1.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-contexify": "^4.1.1",
|
||||
"react-d3": "^0.4.0",
|
||||
|
@ -51,17 +51,18 @@
|
|||
"react-rnd": "^10.2.3",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "^4.0.0",
|
||||
"react-scripts": "^4.0.1",
|
||||
"react-svg-pan-zoom": "^3.8.1",
|
||||
"sass": "^1.28.0",
|
||||
"react-trafficlight": "^5.2.1",
|
||||
"sass": "^1.29.0",
|
||||
"superagent": "^6.1.0",
|
||||
"ts-node": "^9.0.0",
|
||||
"type-fest": "^0.13.1",
|
||||
"typescript": "^4.0.5",
|
||||
"utf-8-validate": "^5.0.2",
|
||||
"typescript": "^4.1.2",
|
||||
"utf-8-validate": "^5.0.3",
|
||||
"validator": "^13.1.17",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-plugin-serve": "^1.2.0"
|
||||
"webpack-plugin-serve": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.2.0"
|
||||
|
|
|
@ -18,30 +18,19 @@
|
|||
import request from 'superagent/lib/client';
|
||||
import Promise from 'es6-promise';
|
||||
import NotificationsDataManager from '../data-managers/notifications-data-manager';
|
||||
import NotificationsFactory from "../data-managers/notifications-factory";
|
||||
|
||||
// TODO: Add this to a central pool of notifications
|
||||
const SERVER_NOT_REACHABLE_NOTIFICATION = {
|
||||
title: 'Server not reachable',
|
||||
message: 'The server could not be reached. Please try again later.',
|
||||
level: 'error'
|
||||
};
|
||||
|
||||
const REQUEST_TIMEOUT_NOTIFICATION = {
|
||||
title: 'Request timeout',
|
||||
message: 'Request timed out. Please try again later.',
|
||||
level: 'error'
|
||||
};
|
||||
|
||||
// Check if the error was due to network failure, timeouts, etc.
|
||||
// Can be used for the rest of requests
|
||||
function isNetworkError(err) {
|
||||
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;
|
||||
|
||||
let notification = err.timeout? REQUEST_TIMEOUT_NOTIFICATION : SERVER_NOT_REACHABLE_NOTIFICATION;
|
||||
let notification = err.timeout? NotificationsFactory.REQUEST_TIMEOUT : NotificationsFactory.SERVER_NOT_REACHABLE(url);
|
||||
NotificationsDataManager.addNotification(notification);
|
||||
}
|
||||
return result;
|
||||
|
@ -52,7 +41,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);
|
||||
|
@ -60,7 +50,7 @@ class RestAPI {
|
|||
|
||||
req.end(function (error, res) {
|
||||
if (res == null || res.status !== 200) {
|
||||
if (req.url !== prevURL) error.handled = isNetworkError(error);
|
||||
if (req.url !== prevURL) error.handled = isNetworkError(error, url);
|
||||
prevURL = req.url;
|
||||
reject(error);
|
||||
} else {
|
||||
|
@ -72,7 +62,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);
|
||||
|
@ -93,7 +83,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);
|
||||
|
@ -112,7 +102,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);
|
||||
|
@ -151,11 +141,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) {
|
||||
|
@ -170,6 +163,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();
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
|
||||
******************************************************************************/
|
||||
import NotificationsDataManager from "../data-managers/notifications-data-manager";
|
||||
import NotificationsFactory from "../data-managers/notifications-factory";
|
||||
import AppDispatcher from '../app-dispatcher';
|
||||
|
||||
class WebsocketAPI {
|
||||
constructor(websocketurl, callbacks) {
|
||||
|
@ -65,6 +68,10 @@ class WebsocketAPI {
|
|||
}
|
||||
|
||||
onOpen = e => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'websocket/connected',
|
||||
data: this.websocketurl,
|
||||
});
|
||||
this.wasConnected = true;
|
||||
|
||||
if ('onOpen' in this.callbacks)
|
||||
|
@ -78,6 +85,12 @@ class WebsocketAPI {
|
|||
}
|
||||
else {
|
||||
if (this.wasConnected) {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'websocket/connection-error',
|
||||
data: this.websocketurl,
|
||||
});
|
||||
|
||||
NotificationsDataManager.addNotification(NotificationsFactory.WEBSOCKET_CONNECTION_WARN(this.websocketurl));
|
||||
console.log("Connection to " + this.websocketurl + " dropped. Attempt reconnect in 1 sec");
|
||||
window.setTimeout(() => { this.reconnect(); }, 1000);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import { ReduceStore } from 'flux/utils';
|
|||
|
||||
import AppDispatcher from './app-dispatcher';
|
||||
import NotificationsDataManager from '../common/data-managers/notifications-data-manager';
|
||||
import NotificationsFactory from "./data-managers/notifications-factory";
|
||||
|
||||
class ArrayStore extends ReduceStore {
|
||||
constructor(type, dataManager) {
|
||||
|
@ -83,13 +84,7 @@ class ArrayStore extends ReduceStore {
|
|||
case this.type + '/load-error':
|
||||
if (action.error && !action.error.handled && action.error.response) {
|
||||
|
||||
const USER_LOAD_ERROR_NOTIFICATION = {
|
||||
title: 'Failed to load',
|
||||
message: action.error.response.body.message,
|
||||
level: 'error'
|
||||
};
|
||||
NotificationsDataManager.addNotification(USER_LOAD_ERROR_NOTIFICATION);
|
||||
|
||||
NotificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(action.error.response.body.message));
|
||||
}
|
||||
return super.reduce(state, action);
|
||||
|
||||
|
@ -120,17 +115,10 @@ class ArrayStore extends ReduceStore {
|
|||
return (item.id !== action.data);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
case this.type + '/remove-error':
|
||||
if (action.error && !action.error.handled && action.error.response) {
|
||||
|
||||
const USER_REMOVE_ERROR_NOTIFICATION = {
|
||||
title: 'Failed to add remove ',
|
||||
message: action.error.response.body.message,
|
||||
level: 'error'
|
||||
};
|
||||
NotificationsDataManager.addNotification(USER_REMOVE_ERROR_NOTIFICATION);
|
||||
|
||||
NotificationsDataManager.addNotification(NotificationsFactory.DELETE_ERROR(action.error.response.body.message));
|
||||
}
|
||||
return super.reduce(state, action);
|
||||
|
||||
|
|
|
@ -18,13 +18,117 @@
|
|||
class NotificationsFactory {
|
||||
|
||||
// This is an example
|
||||
static get EXAMPLE_NOTIFICATION() {
|
||||
return {
|
||||
title: 'Example notification',
|
||||
message: 'Write something here that describes what happend.',
|
||||
level: 'warning'
|
||||
};
|
||||
}
|
||||
static get EXAMPLE_NOTIFICATION() {
|
||||
return {
|
||||
title: 'Example notification',
|
||||
message: 'Write something here that describes what happend.',
|
||||
level: 'warning'
|
||||
};
|
||||
}
|
||||
|
||||
static SERVER_NOT_REACHABLE(url) {
|
||||
return {
|
||||
title: 'Server not reachable',
|
||||
message: 'The url ' + url +' could not be reached. Please try again later.',
|
||||
level: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
static REQUEST_TIMEOUT(url) {
|
||||
return {
|
||||
title: 'Request timeout',
|
||||
message: 'Request to ' + url + ' timed out. Please try again later.',
|
||||
level: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
static ADD_ERROR(message) {
|
||||
return {
|
||||
title: "Add Error",
|
||||
message: message,
|
||||
level: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
static UPDATE_ERROR(message) {
|
||||
return {
|
||||
title: 'Update Error ',
|
||||
message: message,
|
||||
level: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
static UPDATE_WARNING(message) {
|
||||
return {
|
||||
title: 'Update Warning ',
|
||||
message: message,
|
||||
level: 'warning'
|
||||
};
|
||||
}
|
||||
|
||||
static LOAD_ERROR(message) {
|
||||
return {
|
||||
title: 'Failed to load',
|
||||
message: message,
|
||||
level: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
static DELETE_ERROR(message) {
|
||||
return {
|
||||
title: 'Failed to delete ',
|
||||
message: message,
|
||||
level: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
static WEBSOCKET_CONNECTION_WARN(websocket_url) {
|
||||
return {
|
||||
title: 'Websocket connection warning',
|
||||
message: "Connection to " + websocket_url + " dropped. Attempt reconnect in 1 sec",
|
||||
level: 'warning'
|
||||
};
|
||||
}
|
||||
|
||||
static WEBSOCKET_URL_WARN(ic_name, ic_uuid) {
|
||||
return {
|
||||
title: 'Websocket connection warning',
|
||||
message: "Websocket URL parameter not available for IC " + ic_name + "(" + ic_uuid + "), connection not possible",
|
||||
level: 'warning'
|
||||
};
|
||||
}
|
||||
|
||||
static SCENARIO_USERS_ERROR(message) {
|
||||
return {
|
||||
title: 'Failed to modify scenario users ',
|
||||
message: message,
|
||||
level: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
static AUTOCONF_INFO() {
|
||||
return {
|
||||
title: 'Auto-configuration info',
|
||||
message: 'Signal configuration loaded successfully.',
|
||||
level: 'info'
|
||||
};
|
||||
}
|
||||
|
||||
static AUTOCONF_WARN(message) {
|
||||
return {
|
||||
title: 'Auto-configuration warning',
|
||||
message: message,
|
||||
level: 'warning'
|
||||
};
|
||||
}
|
||||
|
||||
static AUTOCONF_ERROR(message) {
|
||||
return {
|
||||
title: 'Auto-configuration error',
|
||||
message: message,
|
||||
level: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ class CustomTable extends Component {
|
|||
|
||||
this.state = {
|
||||
rows: CustomTable.getRows(props),
|
||||
editCell: [ -1, -1 ]
|
||||
editCell: [-1, -1]
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ class CustomTable extends Component {
|
|||
};
|
||||
|
||||
onClick(event, row, column) {
|
||||
this.setState({ editCell: [ column, row ]}); // x, y
|
||||
this.setState({ editCell: [column, row] }); // x, y
|
||||
}
|
||||
|
||||
static addCell(data, index, child) {
|
||||
|
@ -71,6 +71,12 @@ class CustomTable extends Component {
|
|||
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(element => {
|
||||
cell.push(<OverlayTrigger key={element} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"export"}`}>Download {element}</Tooltip>} >
|
||||
<Button variant='table-control-button' onClick={() => child.props.onDownload(element)} disabled={child.props.onDownload == null}>{element + ' ' }
|
||||
<Icon icon='file-download' /></Button></OverlayTrigger>);
|
||||
});
|
||||
} else {
|
||||
cell.push(content);
|
||||
}
|
||||
|
@ -90,8 +96,8 @@ class CustomTable extends Component {
|
|||
cell.push(<span>
|
||||
|
||||
<FormLabel column={false} className={labelStyle}>
|
||||
{labelContent}
|
||||
</FormLabel>
|
||||
{labelContent}
|
||||
</FormLabel>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -103,14 +109,9 @@ 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>);
|
||||
}
|
||||
|
||||
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>);
|
||||
let disable = (typeof data.managedexternally !== "undefined" && data.managedexternally);
|
||||
cell.push(<OverlayTrigger key={0} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"edit"}`}>{disable ? "Externally managed ICs cannot be edited" : "edit"} </Tooltip>} >
|
||||
<Button variant='table-control-button' onClick={() => child.props.onEdit(index)} disabled={disable || child.props.onEdit == null}><Icon icon='edit' /></Button></OverlayTrigger>);
|
||||
}
|
||||
|
||||
if (child.props.checkbox) {
|
||||
|
@ -120,19 +121,34 @@ class CustomTable extends Component {
|
|||
}
|
||||
|
||||
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>);
|
||||
}
|
||||
|
||||
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 +163,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 +180,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 +197,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 +216,28 @@ 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 ) ? (
|
||||
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>)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -24,28 +24,28 @@ class DashboardButtonGroup extends React.Component {
|
|||
render() {
|
||||
const buttonStyle = {
|
||||
marginLeft: '12px',
|
||||
height: '44px',
|
||||
height: '44px',
|
||||
width : '35px'
|
||||
};
|
||||
|
||||
const iconStyle = {
|
||||
color: '#007bff',
|
||||
height: '25px',
|
||||
height: '25px',
|
||||
width : '25px'
|
||||
}
|
||||
|
||||
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" style={iconStyle} />
|
||||
</Button>
|
||||
</OverlayTrigger>,
|
||||
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"cancel"}`}> Discard changes </Tooltip>} >
|
||||
|
@ -59,17 +59,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" 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" 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" style={iconStyle}/>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
@ -77,44 +85,49 @@ 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" 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" 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" 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" 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <div className='section-buttons-group-right'>
|
||||
|
|
|
@ -557,6 +557,7 @@ class Dashboard extends Component {
|
|||
widget={this.state.modalData}
|
||||
signals={this.state.signals}
|
||||
files={this.state.files}
|
||||
ics={this.state.ics}
|
||||
/>
|
||||
|
||||
<EditFiles
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
||||
|
@ -167,7 +172,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;
|
||||
|
@ -120,7 +120,7 @@ class EditICDialog extends React.Component {
|
|||
let typeOptions = [];
|
||||
switch(this.state.category){
|
||||
case "simulator":
|
||||
typeOptions = ["dummy","generic","dpsim","rtlab","rscad"];
|
||||
typeOptions = ["dummy","generic","dpsim","rtlab","rscad", "opalrt"];
|
||||
break;
|
||||
case "controller":
|
||||
typeOptions = ["kubernetes","villas-controller"];
|
||||
|
@ -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">
|
||||
|
|
|
@ -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,96 @@ 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>Status:</h5>
|
||||
|
||||
<ReactJson
|
||||
src={this.props.ic.statusupdateraw}
|
||||
name={false}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={false}
|
||||
enableClipboard={false}
|
||||
collapsed={1}
|
||||
/>
|
||||
|
||||
</Col>
|
||||
|
||||
{this.props.ic.type === "villas-node" || this.props.ic.type === "villas-relay" ? (
|
||||
<Col>
|
||||
<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 URL"} src={graphURL}/>
|
||||
</div>
|
||||
|
||||
{this.props.userRole === "Admin" ? (
|
||||
<div>
|
||||
<h5>Controls:</h5>
|
||||
<div>
|
||||
<Button style={{margin: '5px'}} size='lg'
|
||||
onClick={() => this.setState({confirmCommand: true, command: 'restart'})}>Restart</Button>
|
||||
<Button 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)}/>
|
||||
</Col>
|
||||
): (<div/>)}
|
||||
</Row>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import ArrayStore from '../common/array-store';
|
|||
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() {
|
||||
|
@ -47,14 +49,7 @@ class InfrastructureComponentStore extends ArrayStore {
|
|||
if (ic.websocketurl != null && ic.websocketurl !== '') {
|
||||
ICDataDataManager.open(ic.websocketurl, ic.id);
|
||||
} else {
|
||||
|
||||
// TODO add to pool of notifications
|
||||
const IC_WEBSOCKET_URL_ERROR = {
|
||||
title: 'Websocket connection warning',
|
||||
message: "Websocket URL parameter not available for IC " + ic.name + "(" + ic.uuid + "), connection not possible",
|
||||
level: 'warning'
|
||||
};
|
||||
NotificationsDataManager.addNotification(IC_WEBSOCKET_URL_ERROR);
|
||||
NotificationsDataManager.addNotification(NotificationsFactory.WEBSOCKET_URL_WARN(ic.name, ic.uuid));
|
||||
}
|
||||
}
|
||||
return super.reduce(state, action);
|
||||
|
@ -81,6 +76,51 @@ class InfrastructureComponentStore extends ArrayStore {
|
|||
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.statusupdateraw = 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/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,69 @@ 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({
|
||||
type: 'ics/action-started',
|
||||
data: response
|
||||
});
|
||||
}).catch(error => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'ics/action-error',
|
||||
error
|
||||
});
|
||||
});
|
||||
}
|
||||
doActions(ic, action, token = null) {
|
||||
RestAPI.post(this.makeURL(this.url + '/' + ic.id + '/action'), action, token).then(response => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'ics/action-started',
|
||||
data: response
|
||||
});
|
||||
}).catch(error => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'ics/action-error',
|
||||
error
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getStatus(url,token,ic){
|
||||
RestAPI.get(url, 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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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();
|
||||
|
|
|
@ -38,7 +38,7 @@ import DeleteDialog from '../common/dialogs/delete-dialog';
|
|||
|
||||
class InfrastructureComponents extends Component {
|
||||
static getStores() {
|
||||
return [ InfrastructureComponentStore ];
|
||||
return [ InfrastructureComponentStore];
|
||||
}
|
||||
|
||||
static statePrio(state) {
|
||||
|
@ -91,7 +91,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,6 +109,20 @@ 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 + "/status",
|
||||
token: this.state.sessionToken,
|
||||
ic: ic
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,12 +232,12 @@ class InfrastructureComponents extends Component {
|
|||
}
|
||||
|
||||
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){
|
||||
|
@ -292,7 +306,7 @@ class InfrastructureComponents extends Component {
|
|||
}
|
||||
|
||||
stateUpdateModifier(updatedAt) {
|
||||
let dateFormat = 'DD MMM YYYY HH:mm:ss';
|
||||
let dateFormat = 'ddd, DD MMM YYYY HH:mm:ss ZZ';
|
||||
let dateTime = moment(updatedAt, dateFormat);
|
||||
return dateTime.fromNow()
|
||||
}
|
||||
|
@ -302,7 +316,7 @@ class InfrastructureComponents extends Component {
|
|||
if(managedExternally){
|
||||
return <Icon icon='check' />
|
||||
} else {
|
||||
return <Icon icon='times' />
|
||||
return ""
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -318,14 +332,39 @@ class InfrastructureComponents extends Component {
|
|||
|
||||
modifyNameColumn(name){
|
||||
let ic = this.state.ics.find(ic => ic.name === name);
|
||||
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> }
|
||||
|
||||
if(ic.type === "villas-node" || ic.type === "villas-relay" || ic.managedexternally){
|
||||
return <Button variant="link" onClick={() => this.openICStatus(ic)}>{name}</Button> }
|
||||
else{
|
||||
return <span>{name}</span>
|
||||
}
|
||||
}
|
||||
|
||||
openICStatus(ic){
|
||||
|
||||
let index = this.state.ics.indexOf(ic);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const buttonStyle = {
|
||||
marginLeft: '10px'
|
||||
|
@ -396,7 +435,13 @@ class InfrastructureComponents extends Component {
|
|||
<NewICDialog show={this.state.newModal} onClose={data => this.closeNewModal(data)} />
|
||||
<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} />
|
||||
<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)}/>
|
||||
|
||||
<DeleteDialog title="infrastructure-component" name={this.state.modalIC.name || 'Unknown'} show={this.state.deleteModal} onClose={(e) => this.closeDeleteModal(e)} />
|
||||
</div>
|
||||
|
|
|
@ -28,10 +28,13 @@ class NewICDialog extends React.Component {
|
|||
this.state = {
|
||||
name: '',
|
||||
websocketurl: '',
|
||||
apiurl: '',
|
||||
uuid: '',
|
||||
type: '',
|
||||
category: '',
|
||||
managedexternally: false,
|
||||
description: '',
|
||||
location: ''
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -44,12 +47,18 @@ class NewICDialog extends React.Component {
|
|||
category: this.state.category,
|
||||
uuid: this.state.uuid,
|
||||
managedexternally: this.state.managedexternally,
|
||||
location: this.state.location,
|
||||
description: this.state.description
|
||||
};
|
||||
|
||||
if (this.state.websocketurl != null && this.state.websocketurl !== "" && this.state.websocketurl !== 'http://') {
|
||||
data.websocketurl = this.state.websocketurl;
|
||||
}
|
||||
|
||||
if (this.state.apiurl != null && this.state.apiurl !== "" && this.state.apiurl !== 'http://') {
|
||||
data.apiurl = this.state.apiurl;
|
||||
}
|
||||
|
||||
this.props.onClose(data);
|
||||
this.setState({managedexternally: false});
|
||||
}
|
||||
|
@ -69,7 +78,7 @@ class NewICDialog extends React.Component {
|
|||
}
|
||||
|
||||
resetState() {
|
||||
this.setState({ name: '', websocketurl: 'http://', uuid: this.uuidv4(), type: '', category: '', managedexternally: false});
|
||||
this.setState({ name: '', websocketurl: 'http://', apiurl: 'http://', uuid: this.uuidv4(), type: '', category: '', managedexternally: false, description: '', location: ''});
|
||||
}
|
||||
|
||||
validateForm(target) {
|
||||
|
@ -120,7 +129,7 @@ class NewICDialog extends React.Component {
|
|||
let typeOptions = [];
|
||||
switch(this.state.category){
|
||||
case "simulator":
|
||||
typeOptions = ["dummy","generic","dpsim","rtlab","rscad"];
|
||||
typeOptions = ["dummy","generic","dpsim","rtlab","rscad","opalrt"];
|
||||
break;
|
||||
case "controller":
|
||||
typeOptions = ["kubernetes","villas-controller"];
|
||||
|
|
167
src/result/edit-result.js
Normal file
167
src/result/edit-result.js
Normal file
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* 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, 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,
|
||||
result: null,
|
||||
resultExists: false,
|
||||
};
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.props.onClose != null) {
|
||||
this.props.onClose();
|
||||
}
|
||||
};
|
||||
|
||||
handleChange = event => {
|
||||
this.setState({ [event.target.id]: event.target.value });
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.resultExists && this.props.files != prevProps.files) {
|
||||
this.setState({files: FileStore.getState().filter(file => this.state.result.resultFileIDs.includes(file.id))});
|
||||
}
|
||||
if (this.props.result != prevProps.result && Object.keys(this.props.result).length != 0) {
|
||||
this.setState({
|
||||
id: this.props.result.id,
|
||||
description: this.props.result.description,
|
||||
result: this.props.result,
|
||||
resultExists: true,
|
||||
files: FileStore.getState().filter(file => this.props.result.resultFileIDs.includes(file.id)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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: 'files/start-remove',
|
||||
data: file,
|
||||
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'>
|
||||
<FormLabel column={false}>Description</FormLabel>
|
||||
<FormControl type='text' placeholder='Enter description' value={this.state.description} onChange={this.handleChange} />
|
||||
<FormControl.Feedback />
|
||||
</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>
|
||||
|
||||
|
||||
<FormGroup controlId='resultfile'>
|
||||
<FormLabel>Add Result File</FormLabel>
|
||||
<FormControl type='file' onChange={(event) => this.selectUploadFile(event)} />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup as={Col} >
|
||||
<Button
|
||||
disabled={this.state.uploadFile === null}
|
||||
onClick={() => this.startFileUpload()}>
|
||||
Upload
|
||||
</Button>
|
||||
</FormGroup>
|
||||
|
||||
<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;
|
88
src/result/result-store.js
Normal file
88
src/result/result-store.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* 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 FilesDataManager from '../file/files-data-manager'
|
||||
|
||||
class ResultStore extends ArrayStore {
|
||||
constructor() {
|
||||
super('results', ResultsDataManager);
|
||||
}
|
||||
|
||||
saveFile(state, action){
|
||||
|
||||
let fileID = parseInt(action.id)
|
||||
state.forEach((element, index, array) => {
|
||||
if (element.id === fileID) {
|
||||
// save blob object
|
||||
array[index]["data"] = new Blob([action.data.data], {type: action.data.type});
|
||||
// update file type
|
||||
array[index]["type"] = action.data.type;
|
||||
|
||||
if (array[index]["objectURL"] !== ''){
|
||||
// free memory of previously generated object URL
|
||||
URL.revokeObjectURL(array[index]["objectURL"]);
|
||||
}
|
||||
// create an object URL for the file
|
||||
array[index]["objectURL"] = URL.createObjectURL(array[index]["data"])
|
||||
}
|
||||
});
|
||||
|
||||
// announce change to listeners
|
||||
this.__emitChange();
|
||||
return state
|
||||
}
|
||||
|
||||
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':
|
||||
this.simplifyTimestamps(action.data);
|
||||
return super.reduce(state, action);
|
||||
|
||||
case 'results/added':
|
||||
this.simplifyTimestamps([action.data]);
|
||||
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;
|
||||
|
||||
default:
|
||||
return super.reduce(state, action);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new ResultStore();
|
62
src/result/results-data-manager.js
Normal file
62
src/result/results-data-manager.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* 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 results reload
|
||||
AppDispatcher.dispatch({
|
||||
type: 'results/start-load',
|
||||
param: '?scenarioID=' + scenarioID,
|
||||
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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new ResultsDataManager();
|
|
@ -19,6 +19,7 @@
|
|||
import ScenariosDataManager from './scenarios-data-manager';
|
||||
import ArrayStore from '../common/array-store';
|
||||
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
|
||||
import NotificationsFactory from "../common/data-managers/notifications-factory";
|
||||
|
||||
|
||||
class ScenarioStore extends ArrayStore{
|
||||
|
@ -89,14 +90,7 @@ class ScenarioStore extends ArrayStore{
|
|||
|
||||
case 'scenarios/users-error':
|
||||
if (action.error && !action.error.handled && action.error.response) {
|
||||
|
||||
const SCENARIO_USERS_ERROR_NOTIFICATION = {
|
||||
title: 'Failed to modify scenario users ',
|
||||
message: action.error.response.body.message,
|
||||
level: 'error'
|
||||
};
|
||||
NotificationsDataManager.addNotification(SCENARIO_USERS_ERROR_NOTIFICATION);
|
||||
|
||||
NotificationsDataManager.addNotification(NotificationsFactory.SCENARIO_USERS_ERROR(action.error.response.body.message));
|
||||
}
|
||||
return super.reduce(state, action);
|
||||
|
||||
|
|
|
@ -36,6 +36,9 @@ 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 ICAction from '../ic/ic-action';
|
||||
import DeleteDialog from '../common/dialogs/delete-dialog';
|
||||
|
@ -43,13 +46,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 +74,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 +87,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,
|
||||
|
@ -103,6 +111,12 @@ class Scenario extends React.Component {
|
|||
filesEditModal: prevState.filesEditModal || false,
|
||||
filesEditSaveState: prevState.filesEditSaveState || [],
|
||||
|
||||
editResultsModal: prevState.editResultsModal || false,
|
||||
modalResultsData: {},
|
||||
modalResultsIndex: 0,
|
||||
newResultModal: false,
|
||||
filesToDownload: [],
|
||||
|
||||
editOutputSignalsModal: prevState.editOutputSignalsModal || false,
|
||||
editInputSignalsModal: prevState.editInputSignalsModal || false,
|
||||
|
||||
|
@ -142,6 +156,32 @@ class Scenario extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
// check whether file data has been loaded
|
||||
if (this.state.filesToDownload.length > 0 ) {
|
||||
if (this.state.filesToDownload.length === 1) {
|
||||
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 several files
|
||||
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);
|
||||
});
|
||||
zip.generateAsync({type: "blob"}).then(function(content) {
|
||||
saveAs(content, "results.zip");
|
||||
});
|
||||
this.setState({ filesToDownload: [] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ##############################################
|
||||
* User modification methods
|
||||
############################################## */
|
||||
|
@ -347,7 +387,7 @@ class Scenario extends React.Component {
|
|||
}
|
||||
|
||||
// Unix time stamp + delay
|
||||
action.data.when = Date.now() / 1000.0 + delay
|
||||
action.data.when = Math.round(Date.now() / 1000.0 + delay)
|
||||
|
||||
console.log("Sending action: ", action.data)
|
||||
|
||||
|
@ -375,6 +415,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;
|
||||
|
@ -495,11 +536,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 +557,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 +566,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 +595,66 @@ 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 });
|
||||
|
||||
AppDispatcher.dispatch({
|
||||
type: 'results/start-load',
|
||||
token: this.state.sessionToken,
|
||||
param: '?scenarioID=' + this.state.scenario.id
|
||||
})
|
||||
}
|
||||
|
||||
downloadResultData(param) {
|
||||
let toDownload = [];
|
||||
|
||||
if (typeof(param) === 'object') {
|
||||
toDownload = param.resultFileIDs;
|
||||
} else {
|
||||
toDownload.push(param);
|
||||
}
|
||||
|
||||
toDownload.forEach(fileid => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'files/start-download',
|
||||
data: fileid,
|
||||
token: this.state.sessionToken
|
||||
});
|
||||
});
|
||||
|
||||
this.setState({ filesToDownload: toDownload });
|
||||
}
|
||||
|
||||
closeDeleteResultsModal(confirmDelete) {
|
||||
this.setState({ deleteResultsModal: false });
|
||||
|
||||
if (confirmDelete === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppDispatcher.dispatch({
|
||||
type: 'results/start-remove',
|
||||
data: this.state.modalResultsData,
|
||||
token: this.state.sessionToken,
|
||||
});
|
||||
}
|
||||
|
||||
startPintura(configIndex) {
|
||||
let config = this.state.configs[configIndex];
|
||||
|
||||
|
@ -585,6 +686,7 @@ class Scenario extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/* ##############################################
|
||||
* Render method
|
||||
############################################## */
|
||||
|
@ -613,6 +715,44 @@ class Scenario extends React.Component {
|
|||
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' />
|
||||
<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'
|
||||
width='300'
|
||||
onDownload={(index) => this.downloadResultData(index)}
|
||||
/>
|
||||
<TableColumn
|
||||
title='Options'
|
||||
width='300'
|
||||
editButton
|
||||
downloadAllButton
|
||||
deleteButton
|
||||
onEdit={index => this.setState({ editResultsModal: true, modalResultsData: this.state.results[index], 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}
|
||||
result={this.state.modalResultsData}
|
||||
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)} />
|
||||
</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>} >
|
||||
|
@ -760,6 +900,20 @@ class Scenario extends React.Component {
|
|||
|
||||
<DeleteDialog title="dashboard" name={this.state.modalDashboardData.name} show={this.state.deleteDashboardModal} onClose={(e) => this.closeDeleteDashboardModal(e)} />
|
||||
|
||||
{/*Result table*/}
|
||||
<div>
|
||||
<h2 style={tableHeadingStyle}>Results
|
||||
<Button onClick={() => this.setState({ newResultModal: true })} style={buttonStyle}><Icon icon="plus" /></Button>
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
{resulttable}
|
||||
{/*
|
||||
<div style={{ float: 'right' }}>
|
||||
<Button onClick={() => this.setState({ newResultModal: true })} style={buttonStyle}><Icon icon="plus" /> Result</Button>
|
||||
</div>
|
||||
*/}
|
||||
<NewResultDialog show={this.state.newResultModal} onClose={data => this.closeNewResultModal(data)} />
|
||||
|
||||
{/*Scenario Users table*/}
|
||||
<h2 style={tableHeadingStyle}>Users sharing this scenario</h2>
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
import ArrayStore from '../common/array-store';
|
||||
import SignalsDataManager from './signals-data-manager'
|
||||
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
|
||||
import NotificationsFactory from "../common/data-managers/notifications-factory";
|
||||
|
||||
class SignalStore extends ArrayStore{
|
||||
constructor() {
|
||||
|
@ -45,14 +46,8 @@ class SignalStore extends ArrayStore{
|
|||
|
||||
case 'signals/autoconfig-error':
|
||||
if (action.error && !action.error.handled && action.error.response) {
|
||||
|
||||
const SIGNAL_AUTOCONF_ERROR_NOTIFICATION = {
|
||||
title: 'Failed to load signal config ',
|
||||
message: action.error.response.body.message,
|
||||
level: 'error'
|
||||
};
|
||||
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_ERROR_NOTIFICATION);
|
||||
|
||||
NotificationsDataManager.addNotification(
|
||||
NotificationsFactory.AUTOCONF_ERROR(action.error.response.body.message));
|
||||
}
|
||||
return super.reduce(state, action);
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import RestDataManager from '../common/data-managers/rest-data-manager';
|
|||
import RestAPI from "../common/api/rest-api";
|
||||
import AppDispatcher from "../common/app-dispatcher";
|
||||
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
|
||||
import NotificationsFactory from "../common/data-managers/notifications-factory";
|
||||
|
||||
class SignalsDataManager extends RestDataManager{
|
||||
|
||||
|
@ -62,12 +63,8 @@ class SignalsDataManager extends RestDataManager{
|
|||
saveSignals(nodes, token, configID, socketname){
|
||||
|
||||
if(nodes.length === 0){
|
||||
const SIGNAL_AUTOCONF_ERROR_NOTIFICATION = {
|
||||
title: 'Failed to load nodes ',
|
||||
message: 'VILLASnode returned empty response',
|
||||
level: 'error'
|
||||
};
|
||||
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_ERROR_NOTIFICATION);
|
||||
NotificationsDataManager.addNotification(
|
||||
NotificationsFactory.AUTOCONF_ERROR('Failed to load nodes, VILLASnode returned empty response'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -79,12 +76,9 @@ class SignalsDataManager extends RestDataManager{
|
|||
console.warn("Could not parse the following node config because it lacks a name parameter:", nodeConfig);
|
||||
} else if(nodeConfig.name === socketname){
|
||||
if(configured){
|
||||
const SIGNAL_AUTOCONF_WARNING_NOTIFICATION = {
|
||||
title: 'There might be a problem with the signal auto-config',
|
||||
message: 'VILLASnode returned multiple node configurations for the websocket ' + socketname + '. This is a problem of the VILLASnode.',
|
||||
level: 'warning'
|
||||
};
|
||||
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_WARNING_NOTIFICATION);
|
||||
NotificationsDataManager.addNotification(
|
||||
NotificationsFactory.AUTOCONF_WARN('VILLASnode returned multiple node configurations for the websocket ' +
|
||||
socketname + '. This is a problem of the VILLASnode.'));
|
||||
continue;
|
||||
}
|
||||
// signals are not yet configured:
|
||||
|
@ -92,12 +86,8 @@ class SignalsDataManager extends RestDataManager{
|
|||
let index_out = 1
|
||||
|
||||
if(!nodeConfig.in.hasOwnProperty("signals")){
|
||||
const SIGNAL_AUTOCONF_ERROR_NOTIFICATION = {
|
||||
title: 'Failed to load in signal config ',
|
||||
message: 'No field for in signals contained in response.',
|
||||
level: 'error'
|
||||
};
|
||||
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_ERROR_NOTIFICATION);
|
||||
NotificationsDataManager.addNotification(
|
||||
NotificationsFactory.AUTOCONF_ERROR('Failed to load in signal config, no field for in signals contained in response.'));
|
||||
error = true;
|
||||
} else{
|
||||
|
||||
|
@ -128,12 +118,8 @@ class SignalsDataManager extends RestDataManager{
|
|||
}
|
||||
|
||||
if(!nodeConfig.out.hasOwnProperty("signals")){
|
||||
const SIGNAL_AUTOCONF_ERROR_NOTIFICATION = {
|
||||
title: 'Failed to load out signal config ',
|
||||
message: 'No field for out signals contained in response.',
|
||||
level: 'error'
|
||||
};
|
||||
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_ERROR_NOTIFICATION);
|
||||
NotificationsDataManager.addNotification(
|
||||
NotificationsFactory.AUTOCONF_ERROR('Failed to load out signal config, no field for out signals contained in response.'));
|
||||
error=true;
|
||||
}else {
|
||||
|
||||
|
@ -173,12 +159,7 @@ class SignalsDataManager extends RestDataManager{
|
|||
}
|
||||
|
||||
if(!error) {
|
||||
const SIGNAL_AUTOCONF_INFO_NOTIFICATION = {
|
||||
title: 'Signal configuration loaded successfully.',
|
||||
message: '',
|
||||
level: 'info'
|
||||
};
|
||||
NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_INFO_NOTIFICATION);
|
||||
NotificationsDataManager.addNotification(NotificationsFactory.AUTOCONF_INFO());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -343,6 +343,7 @@ body {
|
|||
display: -webkit-flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
|
|
|
@ -394,4 +394,19 @@ div[class*="-widget"] label {
|
|||
height: 100%;
|
||||
border: 2px solid lightgray;
|
||||
}
|
||||
/* End box widget */
|
||||
/* End box widget */
|
||||
|
||||
/* Begin time offset widget */
|
||||
.time-offset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.time-offset span {
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* End time offset widget */
|
|
@ -25,6 +25,7 @@ import UsersStore from './users-store';
|
|||
import Icon from '../common/icon';
|
||||
import EditOwnUserDialog from './edit-own-user'
|
||||
import NotificationsDataManager from "../common/data-managers/notifications-data-manager"
|
||||
import NotificationsFactory from "../common/data-managers/notifications-factory";
|
||||
|
||||
class User extends Component {
|
||||
static getStores() {
|
||||
|
@ -81,12 +82,7 @@ class User extends Component {
|
|||
updatedData.password = data.password;
|
||||
updatedData.oldPassword = data.oldPassword;
|
||||
} else if (data.password !== '' && data.password !== data.confirmpassword) {
|
||||
const USER_UPDATE_ERROR_NOTIFICATION = {
|
||||
title: 'Update Error ',
|
||||
message: 'New password not correctly confirmed',
|
||||
level: 'error'
|
||||
};
|
||||
NotificationsDataManager.addNotification(USER_UPDATE_ERROR_NOTIFICATION);
|
||||
NotificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR('New password not correctly confirmed'));
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -102,12 +98,7 @@ class User extends Component {
|
|||
token: this.state.token
|
||||
});
|
||||
} else {
|
||||
const USER_UPDATE_WARNING_NOTIFICATION = {
|
||||
title: 'Update Warning ',
|
||||
message: 'No update requested, no input data',
|
||||
level: 'warning'
|
||||
};
|
||||
NotificationsDataManager.addNotification(USER_UPDATE_WARNING_NOTIFICATION);
|
||||
NotificationsDataManager.addNotification(NotificationsFactory.UPDATE_WARNING('No update requested, no input data'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
import ArrayStore from '../common/array-store';
|
||||
import UsersDataManager from './users-data-manager';
|
||||
import NotificationsDataManager from '../common/data-managers/notifications-data-manager';
|
||||
import NotificationsFactory from "../common/data-managers/notifications-factory";
|
||||
|
||||
class UsersStore extends ArrayStore {
|
||||
constructor() {
|
||||
|
@ -30,12 +31,8 @@ class UsersStore extends ArrayStore {
|
|||
case this.type + '/add-error':
|
||||
if (action.error && !action.error.handled && action.error.response) {
|
||||
// If it was an error and hasn't been handled, user could not be added
|
||||
const USER_ADD_ERROR_NOTIFICATION = {
|
||||
title: 'Failed to add new user',
|
||||
message: action.error.response.body.message,
|
||||
level: 'error'
|
||||
};
|
||||
NotificationsDataManager.addNotification(USER_ADD_ERROR_NOTIFICATION);
|
||||
NotificationsDataManager.addNotification(
|
||||
NotificationsFactory.ADD_ERROR('Failed to add new user: ' + action.error.response.body.message));
|
||||
|
||||
}
|
||||
return super.reduce(state, action);
|
||||
|
@ -43,12 +40,8 @@ class UsersStore extends ArrayStore {
|
|||
case this.type + '/edit-error':
|
||||
if (action.error && !action.error.handled && action.error.response) {
|
||||
// If it was an error and hasn't been handled, user couldn't not be updated
|
||||
const USER_EDIT_ERROR_NOTIFICATION = {
|
||||
title: 'Failed to edit user',
|
||||
message: action.error.response.body.message,
|
||||
level: 'error'
|
||||
};
|
||||
NotificationsDataManager.addNotification(USER_EDIT_ERROR_NOTIFICATION);
|
||||
NotificationsDataManager.addNotification(
|
||||
NotificationsFactory.UPDATE_ERROR('Failed to edit user: ' + action.error.response.body.message));
|
||||
|
||||
}
|
||||
return super.reduce(state, action);
|
||||
|
|
|
@ -30,6 +30,7 @@ import EditUserDialog from './edit-user';
|
|||
|
||||
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 Users extends Component {
|
||||
static getStores() {
|
||||
|
@ -97,12 +98,8 @@ class Users extends Component {
|
|||
token: this.state.token
|
||||
});
|
||||
} else{
|
||||
const USER_UPDATE_ERROR_NOTIFICATION = {
|
||||
title: 'Update Error ',
|
||||
message: 'New password not correctly confirmed',
|
||||
level: 'error'
|
||||
};
|
||||
NotificationsDataManager.addNotification(USER_UPDATE_ERROR_NOTIFICATION)
|
||||
|
||||
NotificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR("New password not correctly confirmed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,9 @@ class ColorPicker extends React.Component {
|
|||
if(this.props.controlId === 'strokeStyle'){
|
||||
temp.customProperties.zones[this.props.zoneIndex]['strokeStyle'] = color.hex;
|
||||
}
|
||||
else if(this.props.controlId === 'lineColor'){
|
||||
temp.customProperties.lineColors[this.props.lineIndex] = color.hex;
|
||||
}
|
||||
else{
|
||||
let parts = this.props.controlId.split('.');
|
||||
let isCustomProperty = true;
|
||||
|
@ -85,7 +88,7 @@ class ColorPicker extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
let disableOpacity = false;
|
||||
|
||||
let hexColor;
|
||||
let opacity = 1;
|
||||
let parts = this.props.controlId.split('.');
|
||||
|
@ -94,12 +97,14 @@ class ColorPicker extends React.Component {
|
|||
isCustomProperty = false;
|
||||
}
|
||||
|
||||
if((this.state.widget.type === "Box" && parts[1] === "border_color") || this.props.controlId === 'strokeStyle'){
|
||||
disableOpacity = true;
|
||||
}
|
||||
if(this.props.controlId === 'strokeStyle'){
|
||||
if(typeof this.state.widget.customProperties.zones[this.props.zoneIndex] !== 'undefined'){
|
||||
hexColor = this.state.widget.customProperties.zones[this.props.zoneIndex]['strokeStyle'];
|
||||
hexColor = this.state.widget.customProperties.zones[this.props.zoneIndex]['strokeStyle'];
|
||||
}
|
||||
}
|
||||
else if(this.props.controlId === 'lineColor'){
|
||||
if(typeof this.state.widget.customProperties.lineColors[this.props.lineIndex] !== 'undefined'){
|
||||
hexColor = this.state.widget.customProperties.lineColors[this.props.lineIndex];
|
||||
}
|
||||
}
|
||||
else{
|
||||
|
@ -117,7 +122,7 @@ class ColorPicker extends React.Component {
|
|||
<form>
|
||||
<SketchPicker
|
||||
color={rgbColor}
|
||||
disableAlpha={disableOpacity}
|
||||
disableAlpha={this.props.disableOpacity}
|
||||
onChangeComplete={ this.handleChangeComplete }
|
||||
width={"300"}
|
||||
/>
|
||||
|
|
|
@ -82,7 +82,7 @@ class EditWidgetColorControl extends Component {
|
|||
}
|
||||
|
||||
let tooltipText = "Change color and opacity";
|
||||
if(this.state.widget.type === "Box" && parts[1] === "border_color"){
|
||||
if(this.props.disableOpacity){
|
||||
tooltipText = "Change border color";
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ class EditWidgetColorControl extends Component {
|
|||
</OverlayTrigger>
|
||||
</div>
|
||||
|
||||
<ColorPicker show={this.state.showColorPicker} onClose={(data) => this.closeEditModal(data)} widget={this.state.widget} controlId={this.props.controlId} />
|
||||
<ColorPicker show={this.state.showColorPicker} onClose={(data) => this.closeEditModal(data)} widget={this.state.widget} controlId={this.props.controlId} disableOpacity={this.props.disableOpacity}/>
|
||||
</FormGroup>
|
||||
|
||||
)
|
||||
|
|
|
@ -191,7 +191,7 @@ class EditWidgetColorZonesControl extends React.Component {
|
|||
}
|
||||
|
||||
return <FormGroup>
|
||||
<FormLabel>Color zones</FormLabel>
|
||||
<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>
|
||||
|
||||
<div>
|
||||
|
@ -205,8 +205,6 @@ class EditWidgetColorZonesControl extends React.Component {
|
|||
height: '40px'
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (<Button
|
||||
style={style} key={idx} onClick={i => this.editColorZone(idx)} disabled={!this.props.widget.customProperties.colorZones}><Icon icon="pen" /></Button>
|
||||
)
|
||||
|
|
|
@ -31,9 +31,11 @@ import EditWidgetCheckboxControl from './edit-widget-checkbox-control';
|
|||
import EditWidgetColorZonesControl from './edit-widget-color-zones-control';
|
||||
import EditWidgetMinMaxControl from './edit-widget-min-max-control';
|
||||
import EditWidgetParametersControl from './edit-widget-parameters-control';
|
||||
import EditWidgetICControl from './edit-widget-ic-control';
|
||||
import EditWidgetPlotColorsControl from './edit-widget-plot-colors-control';
|
||||
//import EditWidgetHTMLContent from './edit-widget-html-content';
|
||||
|
||||
export default function CreateControls(widgetType = null, widget = null, sessionToken = null, files = null, signals, handleChange) {
|
||||
export default function CreateControls(widgetType = null, widget = null, sessionToken = null, files = null,ics = null, signals, handleChange) {
|
||||
// Use a list to concatenate the controls according to the widget type
|
||||
var DialogControls = [];
|
||||
|
||||
|
@ -62,16 +64,17 @@ export default function CreateControls(widgetType = null, widget = null, session
|
|||
DialogControls.push(
|
||||
<EditWidgetSignalControl key={0} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} direction={'out'}/>,
|
||||
<EditWidgetTextControl key={1} widget={widget} controlId={'customProperties.threshold'} label={'Threshold'} placeholder={'0.5'} handleChange={e => handleChange(e)} />,
|
||||
<EditWidgetColorControl key={2} widget={widget} controlId={'customProperties.on_color'} label={'Color On'} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetColorControl key={3} widget={widget} controlId={'customProperties.off_color'} label={'Color Off'} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetColorControl key={2} widget={widget} controlId={'customProperties.on_color'} label={'Color On'} handleChange={(e) => handleChange(e)} disableOpacity={false}/>,
|
||||
<EditWidgetColorControl key={3} widget={widget} controlId={'customProperties.off_color'} label={'Color Off'} handleChange={(e) => handleChange(e)} disableOpacity={false}/>,
|
||||
);
|
||||
break;
|
||||
case 'Plot':
|
||||
DialogControls.push(
|
||||
<EditWidgetTimeControl key={0} widget={widget} controlId={'customProperties.time'} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetSignalsControl key={1} widget={widget} controlId={'signalIDs'} signals={signals} handleChange={(e) => handleChange(e)} direction={'out'}/>,
|
||||
<EditWidgetTextControl key={2} widget={widget} controlId={'customProperties.ylabel'} label={'Y-Axis name'} placeholder={'Enter a name for the y-axis'} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetMinMaxControl key={3} widget={widget} controlId="customProperties.y" handleChange={e => handleChange(e)} />
|
||||
<EditWidgetPlotColorsControl key={2} widget={widget} controlId={'customProperties.lineColors'} signals={signals} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetTextControl key={3} widget={widget} controlId={'customProperties.ylabel'} label={'Y-Axis name'} placeholder={'Enter a name for the y-axis'} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetMinMaxControl key={4} widget={widget} controlId="customProperties.y" handleChange={e => handleChange(e)} />
|
||||
);
|
||||
break;
|
||||
case 'Table':
|
||||
|
@ -115,20 +118,23 @@ export default function CreateControls(widgetType = null, widget = null, session
|
|||
<EditWidgetSignalControl key={1} widget={widget} controlId={'signalIDs'} input signals={signals} handleChange={(e) => handleChange(e)} direction={'in'}/>,
|
||||
<EditWidgetCheckboxControl key={2} widget={widget} controlId={'customProperties.toggle'} input text="Toggle" handleChange={e => handleChange(e)} />,
|
||||
<EditWidgetNumberControl key={3} widget={widget} controlId={'customProperties.on_value'} label={'On Value'} defaultValue={1} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetNumberControl key={4} widget={widget} controlId={'customProperties.off_value'} label={'Off Value'} defaultValue={0} handleChange={(e) => handleChange(e)} />
|
||||
<EditWidgetNumberControl key={4} widget={widget} controlId={'customProperties.off_value'} label={'Off Value'} defaultValue={0} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetColorControl key={5} widget={widget} controlId={'customProperties.background_color'} label={'Background Color and Opacity'} handleChange={(e) => handleChange(e)} disableOpacity={false}/>,
|
||||
<EditWidgetColorControl key={6} widget={widget} controlId={'customProperties.border_color'} label={'Border Color'} handleChange={(e) => handleChange(e)} disableOpacity={true}/>,
|
||||
<EditWidgetColorControl key={7} widget={widget} controlId={'customProperties.font_color'} label={'Font Color'} handleChange={(e) => handleChange(e)} disableOpacity={true}/>,
|
||||
);
|
||||
break;
|
||||
case 'Box':
|
||||
DialogControls.push(
|
||||
<EditWidgetColorControl key={0} widget={widget} controlId={'customProperties.border_color'} label={'Border color'} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetColorControl key={1} widget={widget} controlId={'customProperties.background_color'} label={'Background color'} handleChange={e => handleChange(e)} />,
|
||||
<EditWidgetColorControl key={0} widget={widget} controlId={'customProperties.background_color'} label={'Background Color and Opacity'} handleChange={e => handleChange(e)} disableOpacity={false}/>,
|
||||
<EditWidgetColorControl key={1} widget={widget} controlId={'customProperties.border_color'} label={'Border Color'} handleChange={(e) => handleChange(e)} disableOpacity={true}/>,
|
||||
);
|
||||
break;
|
||||
case 'Label':
|
||||
DialogControls.push(
|
||||
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} handleChange={e => handleChange(e)} />,
|
||||
<EditWidgetTextSizeControl key={1} widget={widget} handleChange={e => handleChange(e)} />,
|
||||
<EditWidgetColorControl key={2} widget={widget} controlId={'customProperties.fontColor'} label={'Text color'} handleChange={e => handleChange(e)} />
|
||||
<EditWidgetColorControl key={2} widget={widget} controlId={'customProperties.fontColor'} label={'Text color'} handleChange={e => handleChange(e)} disableOpacity={false}/>
|
||||
);
|
||||
break;
|
||||
/*case 'HTML':
|
||||
|
@ -152,12 +158,22 @@ export default function CreateControls(widgetType = null, widget = null, session
|
|||
break;
|
||||
case 'Line':
|
||||
DialogControls.push(
|
||||
<EditWidgetColorControl key={0} widget={widget} controlId={'customProperties.border_color'} label={'Line color'} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetColorControl key={0} widget={widget} controlId={'customProperties.border_color'} label={'Line color'} handleChange={(e) => handleChange(e)} disableOpacity={false}/>,
|
||||
<EditWidgetNumberControl key={1} widget={widget} controlId={'customProperties.rotation'} label={'Rotation (degrees)'} defaultValue={0} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetNumberControl key={2} widget={widget} controlId={'customProperties.border_width'} label={'Line width'} defaultValue={0} handleChange={(e) => handleChange(e)} />
|
||||
);
|
||||
break;
|
||||
|
||||
case 'TimeOffset':
|
||||
DialogControls.push(
|
||||
<EditWidgetICControl key={0} widget={widget} controlId={'customProperties.icID'} input ics={ics} handleChange={(e) => handleChange(e)}/>,
|
||||
<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)} />,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Non-valid widget type: ' + widgetType);
|
||||
}
|
||||
|
|
74
src/widget/edit-widget/edit-widget-ic-control.js
Normal file
74
src/widget/edit-widget/edit-widget-ic-control.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
class EditWidgetICControl extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
ics: [],
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state){
|
||||
return {
|
||||
ics: props.ics
|
||||
};
|
||||
}
|
||||
|
||||
handleICChange(e) {
|
||||
let value = e.target.value === "Select IC" ? (-1) : (e.target.value);
|
||||
this.props.handleChange({ target: { id: this.props.controlId, value: value } });
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let parts = this.props.controlId.split('.');
|
||||
let isCustomProperty = true;
|
||||
if(parts.length === 1){
|
||||
isCustomProperty = false;
|
||||
}
|
||||
|
||||
let icOptions = [];
|
||||
if (this.state.ics !== null && this.state.ics.length > 0){
|
||||
icOptions.push(
|
||||
<option key = {0} default>Select IC</option>
|
||||
)
|
||||
icOptions.push(this.state.ics.map((ic, index) => (
|
||||
<option key={index+1} value={ic.id}>{ic.name}</option>
|
||||
)))
|
||||
} else {
|
||||
icOptions = <option style={{ display: 'none' }}>No ics found</option>
|
||||
}
|
||||
|
||||
return <div>
|
||||
<FormGroup controlId="ic">
|
||||
<FormLabel>IC</FormLabel>
|
||||
<FormControl
|
||||
as="select"
|
||||
value={isCustomProperty ? this.props.widget[parts[0]][parts[1]] : this.props.widget[this.props.controlId]}
|
||||
onChange={(e) => this.handleICChange(e)}>{icOptions} </FormControl>
|
||||
</FormGroup>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default EditWidgetICControl;
|
101
src/widget/edit-widget/edit-widget-plot-colors-control.js
Normal file
101
src/widget/edit-widget/edit-widget-plot-colors-control.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* 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, { Component } from 'react';
|
||||
import { FormGroup, OverlayTrigger, Tooltip , FormLabel, Button } from 'react-bootstrap';
|
||||
import ColorPicker from './color-picker'
|
||||
import Icon from "../../common/icon";
|
||||
|
||||
// schemeCategory20 no longer available in d3
|
||||
|
||||
class EditWidgetPlotColorsControl extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
widget: {},
|
||||
showColorPicker: false,
|
||||
originalColor: null,
|
||||
selectedIndex: null
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state){
|
||||
return {
|
||||
widget: props.widget
|
||||
};
|
||||
}
|
||||
|
||||
//same here
|
||||
|
||||
closeEditModal = (data) => {
|
||||
this.setState({showColorPicker: false})
|
||||
if(typeof data === 'undefined'){
|
||||
|
||||
let temp = this.state.widget;
|
||||
temp.customProperties.lineColors[this.state.selectedIndex] = this.state.originalColor;
|
||||
this.setState({ widget: temp });
|
||||
}
|
||||
}
|
||||
|
||||
editLineColor = (index) => {
|
||||
if(this.state.selectedIndex !== index){
|
||||
let color = this.state.widget.customProperties.lineColors[index];
|
||||
this.setState({selectedIndex: index, showColorPicker: true, originalColor: color});
|
||||
}
|
||||
else{
|
||||
this.setState({selectedIndex: null});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<FormLabel>Line Colors</FormLabel>
|
||||
|
||||
<div>
|
||||
{
|
||||
this.state.widget.signalIDs.map((signalID, idx) => {
|
||||
let color = this.state.widget.customProperties.lineColors[signalID];
|
||||
let width = 260 / this.state.widget.signalIDs.length;
|
||||
let style = {
|
||||
backgroundColor: color,
|
||||
width: width,
|
||||
height: '40px'
|
||||
}
|
||||
|
||||
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>
|
||||
</OverlayTrigger>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<ColorPicker show={this.state.showColorPicker} onClose={(data) => this.closeEditModal(data)} widget={this.state.widget} lineIndex={this.state.selectedIndex} controlId={'lineColor'} disableOpacity={true}/>
|
||||
</FormGroup>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default EditWidgetPlotColorsControl;
|
|
@ -141,6 +141,11 @@ class EditWidgetDialog extends React.Component {
|
|||
customProperty ? changeObject[parts[0]][parts[1]]= 'default' : changeObject[e.target.id] = 'default';
|
||||
}
|
||||
changeObject = this.setMaxWidth(changeObject);
|
||||
} else if (parts[1] === 'horizontal'){
|
||||
customProperty ? changeObject[parts[0]][parts[1]] = e.target.value : changeObject[e.target.id] = e.target.value ;
|
||||
let tempWidth = changeObject.width;
|
||||
changeObject.width = changeObject.height;
|
||||
changeObject.height = tempWidth;
|
||||
} else {
|
||||
customProperty ? changeObject[parts[0]][parts[1]] = e.target.value : changeObject[e.target.id] = e.target.value ;
|
||||
}
|
||||
|
@ -163,6 +168,7 @@ class EditWidgetDialog extends React.Component {
|
|||
this.state.temporal,
|
||||
this.props.sessionToken,
|
||||
this.props.files,
|
||||
this.props.ics,
|
||||
this.props.signals,
|
||||
(e) => this.handleChange(e));
|
||||
}
|
||||
|
|
59
src/widget/websocket-store.js
Normal file
59
src/widget/websocket-store.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
class WebsocketStore extends ArrayStore {
|
||||
|
||||
updateSocketStatus(state, socket) {
|
||||
let checkInclusion = false;
|
||||
state.forEach((element) => {
|
||||
if (element.url === socket.url) {
|
||||
element.connected = socket.connected;
|
||||
checkInclusion = true;
|
||||
}
|
||||
})
|
||||
if (!checkInclusion) {
|
||||
state.push(socket);
|
||||
}
|
||||
this.__emitChange();
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
reduce(state, action) {
|
||||
let tempSocket = {};
|
||||
switch (action.type) {
|
||||
|
||||
case 'websocket/connected':
|
||||
tempSocket.url = action.data;
|
||||
tempSocket.connected = true;
|
||||
return this.updateSocketStatus(state, tempSocket);
|
||||
|
||||
case 'websocket/connection-error':
|
||||
tempSocket.url = action.data;
|
||||
tempSocket.connected = false;
|
||||
return this.updateSocketStatus(state, tempSocket);
|
||||
|
||||
|
||||
default:
|
||||
return super.reduce(state, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebsocketStore();
|
|
@ -89,6 +89,7 @@ class WidgetFactory {
|
|||
widget.customProperties.yMin = 0;
|
||||
widget.customProperties.yMax = 10;
|
||||
widget.customProperties.yUseMinMax = false;
|
||||
widget.customProperties.lineColors = [];
|
||||
break;
|
||||
case 'Table':
|
||||
widget.minWidth = 200;
|
||||
|
@ -122,8 +123,10 @@ class WidgetFactory {
|
|||
widget.minHeight = 50;
|
||||
widget.width = 100;
|
||||
widget.height = 100;
|
||||
widget.customProperties.background_color = '#4287f5';
|
||||
widget.customProperties.background_color = '#527984';
|
||||
widget.customProperties.font_color = '#4287f5';
|
||||
widget.customProperties.border_color = '#4287f5';
|
||||
widget.customProperties.background_color_opacity = 1;
|
||||
widget.customProperties.on_value = 1;
|
||||
widget.customProperties.off_value = 0;
|
||||
widget.customProperties.toggle = false;
|
||||
|
@ -195,6 +198,19 @@ class WidgetFactory {
|
|||
widget.customProperties.lockAspect = true;
|
||||
break;
|
||||
|
||||
case 'TimeOffset':
|
||||
widget.minWidth = 20;
|
||||
widget.minHeight = 20;
|
||||
widget.width = 100;
|
||||
widget.height = 40;
|
||||
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;
|
||||
break;
|
||||
|
||||
default:
|
||||
widget.width = 100;
|
||||
widget.height = 100;
|
||||
|
|
|
@ -20,13 +20,17 @@ import { scaleOrdinal} from 'd3-scale';
|
|||
import {schemeCategory10} from 'd3-scale-chromatic'
|
||||
|
||||
function Legend(props){
|
||||
|
||||
const signal = props.sig;
|
||||
const hasScalingFactor = (signal.scalingFactor !== 1);
|
||||
|
||||
const newLineColor = scaleOrdinal(schemeCategory10);
|
||||
|
||||
let color = typeof props.lineColor === "undefined" ? newLineColor(signal.id) : props.lineColor;
|
||||
|
||||
if(hasScalingFactor){
|
||||
return (
|
||||
<li key={signal.id} className="signal-legend" style={{ color: props.colorScale(signal.id) }}>
|
||||
<li key={signal.id} className="signal-legend" style={{ color: color }}>
|
||||
<span className="signal-legend-name">{signal.name}</span>
|
||||
<span style={{ marginLeft: '0.3em' }} className="signal-unit">{signal.unit}</span>
|
||||
<span style={{ marginLeft: '0.3em' }} className="signal-scale">{signal.scalingFactor}</span>
|
||||
|
@ -34,7 +38,7 @@ function Legend(props){
|
|||
)
|
||||
} else {
|
||||
return (
|
||||
<li key={signal.id} className="signal-legend" style={{ color: props.colorScale(signal.id) }}>
|
||||
<li key={signal.id} className="signal-legend" style={{ color: color }}>
|
||||
<span className="signal-legend-name">{signal.name}</span>
|
||||
<span style={{ marginLeft: '0.3em' }} className="signal-unit">{signal.unit}</span>
|
||||
</li>
|
||||
|
@ -45,13 +49,12 @@ function Legend(props){
|
|||
|
||||
class PlotLegend extends React.Component {
|
||||
render() {
|
||||
const colorScale = scaleOrdinal(schemeCategory10);
|
||||
|
||||
return <div className="plot-legend">
|
||||
<ul>
|
||||
{
|
||||
this.props.signals.map( signal =>
|
||||
<Legend key={signal.id} sig={signal} colorScale={colorScale}/>
|
||||
<Legend key={signal.id} sig={signal} lineColor={this.props.lineColors[signal.id]}/>
|
||||
)}
|
||||
</ul>
|
||||
</div>;
|
||||
|
|
|
@ -203,14 +203,21 @@ class Plot extends React.Component {
|
|||
|
||||
// generate paths from data
|
||||
const sparkLine = line().x(p => xScale(p.x)).y(p => yScale(p.y));
|
||||
const lineColor = scaleOrdinal(schemeCategory10);
|
||||
const newLineColor = scaleOrdinal(schemeCategory10);
|
||||
|
||||
const lines = this.state.data.map((values, index) => <path d={sparkLine(values)} key={index} style={{ fill: 'none', stroke: lineColor(index) }} />);
|
||||
const lines = this.state.data.map((values, index) => {
|
||||
let signalID = this.props.signalIDs[index];
|
||||
if (typeof this.props.lineColors[signalID] === "undefined") {
|
||||
this.props.lineColors[signalID] = newLineColor(signalID);
|
||||
}
|
||||
return <path d={sparkLine(values)} key={index} style={{ fill: 'none', stroke: this.props.lineColors[signalID] }} />
|
||||
});
|
||||
|
||||
this.setState({ lines, xAxis, yAxis });
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const yLabelPos = {
|
||||
x: 12,
|
||||
y: this.props.height / 2
|
||||
|
|
|
@ -164,6 +164,7 @@ class WidgetToolbox extends React.Component {
|
|||
<ToolboxItem name='Lamp' type='widget' icon = 'plus' />
|
||||
<ToolboxItem name='Gauge' type='widget' icon = 'plus'/>
|
||||
<ToolboxItem name='Topology' type='widget' disabled={thereIsTopologyWidget} title={topologyItemMsg} icon = 'plus'/>
|
||||
<ToolboxItem name='TimeOffset' type='widget' icon = 'plus' />
|
||||
<OverlayTrigger key={0} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"?"}`}> Drag and drop widgets onto the dashboard </Tooltip>} >
|
||||
<Button disabled={true} variant="light" size="sm" key={0} >
|
||||
<Icon icon="question" />
|
||||
|
|
|
@ -23,6 +23,8 @@ import ICDataStore from '../ic/ic-data-store';
|
|||
import ConfigsStore from '../componentconfig/config-store';
|
||||
import FileStore from '../file/file-store';
|
||||
import SignalStore from '../signal/signal-store'
|
||||
import WebsocketStore from './websocket-store'
|
||||
import ICStore from '../ic/ic-store';
|
||||
|
||||
import WidgetCustomAction from './widgets/custom-action';
|
||||
import WidgetAction from './widgets/action';
|
||||
|
@ -39,6 +41,7 @@ import WidgetGauge from './widgets/gauge';
|
|||
import WidgetBox from './widgets/box';
|
||||
import WidgetTopology from './widgets/topology';
|
||||
import WidgetLine from './widgets/line';
|
||||
import WidgetTimeOffset from './widgets/time-offset'
|
||||
//import WidgetHTML from './widgets/html';
|
||||
|
||||
|
||||
|
@ -46,11 +49,14 @@ import '../styles/widgets.css';
|
|||
|
||||
class Widget extends React.Component {
|
||||
static getStores() {
|
||||
return [ ICDataStore, ConfigsStore, FileStore, SignalStore];
|
||||
return [ ICDataStore, ConfigsStore, FileStore, SignalStore, WebsocketStore, ICStore];
|
||||
}
|
||||
|
||||
static calculateState(prevState, props) {
|
||||
|
||||
let websockets = WebsocketStore.getState();
|
||||
let ics = ICStore.getState();
|
||||
|
||||
let icData = {};
|
||||
|
||||
if (props.paused) {
|
||||
|
@ -78,6 +84,8 @@ class Widget extends React.Component {
|
|||
}
|
||||
|
||||
return {
|
||||
ics: ics,
|
||||
websockets: websockets,
|
||||
icData: icData,
|
||||
signals: signals,
|
||||
icIDs: icIDs,
|
||||
|
@ -223,6 +231,13 @@ class Widget extends React.Component {
|
|||
widget={widget}
|
||||
editing={this.props.editing}
|
||||
/>
|
||||
} else if (widget.type === 'TimeOffset') {
|
||||
return <WidgetTimeOffset
|
||||
widget={widget}
|
||||
data={this.state.icData}
|
||||
websockets={this.state.websockets}
|
||||
ics={this.state.ics}
|
||||
/>
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -59,10 +59,19 @@ class WidgetButton extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
|
||||
const buttonStyle = {
|
||||
backgroundColor: this.props.widget.customProperties.background_color,
|
||||
borderColor: this.props.widget.customProperties.border_color,
|
||||
color: this.props.widget.customProperties.font_color,
|
||||
opacity: this.props.widget.customProperties.background_color_opacity
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="button-widget full">
|
||||
<Button
|
||||
className="full"
|
||||
style={buttonStyle}
|
||||
active={ this.state.pressed }
|
||||
disabled={ this.props.editing }
|
||||
onMouseDown={ (e) => this.onPress(e) }
|
||||
|
|
|
@ -103,9 +103,11 @@ class WidgetPlot extends React.Component {
|
|||
yUseMinMax={this.props.widget.customProperties.yUseMinMax}
|
||||
paused={this.props.paused}
|
||||
yLabel={this.props.widget.customProperties.ylabel}
|
||||
lineColors={this.props.widget.customProperties.lineColors}
|
||||
signalIDs={this.props.widget.signalIDs}
|
||||
/>
|
||||
</div>
|
||||
<PlotLegend signals={this.state.signals} />
|
||||
<PlotLegend signals={this.state.signals} lineColors={this.props.widget.customProperties.lineColors} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
98
src/widget/widgets/time-offset.js
Normal file
98
src/widget/widgets/time-offset.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* 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, { Component } from 'react';
|
||||
import TrafficLight from 'react-trafficlight';
|
||||
import {OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
|
||||
class WidgetTimeOffset extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
timeOffset: '',
|
||||
icID: '',
|
||||
icName: '',
|
||||
websocketOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state){
|
||||
|
||||
if(typeof props.widget.customProperties.icID !== "undefined" && state.icID !== props.widget.customProperties.icID){
|
||||
return {icID: props.widget.customProperties.icID};
|
||||
}
|
||||
|
||||
if (props.data == null
|
||||
|| props.data[state.icID] == null
|
||||
|| props.data[state.icID].output == null
|
||||
|| props.data[state.icID].output.timestamp == null) {
|
||||
return {timeOffset: -1};
|
||||
}
|
||||
|
||||
let ic = props.ics.find(ic => ic.id === parseInt(state.icID, 10));
|
||||
let websocket = props.websockets.find(ws => ws.url === ic.websocketurl);
|
||||
|
||||
let serverTime = props.data[state.icID].output.timestamp;
|
||||
let localTime = Date.now();
|
||||
let absoluteOffset = Math.abs(serverTime - localTime);
|
||||
|
||||
if(typeof websocket === 'undefined'){
|
||||
return {timeOffset: Number.parseFloat(absoluteOffset/1000).toPrecision(5)}
|
||||
}
|
||||
return {timeOffset: Number.parseFloat(absoluteOffset/1000).toPrecision(5), websocketOpen: websocket.connected, icName: ic.name};
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let icSelected = " ";
|
||||
if(!this.state.websocketOpen){
|
||||
icSelected = "no connection";
|
||||
} 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>)
|
||||
}
|
||||
<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}
|
||||
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}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
{this.props.widget.customProperties.icID !== -1 ?
|
||||
(
|
||||
<span>{icSelected}</span>)
|
||||
:
|
||||
(<span>selected</span>)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WidgetTimeOffset;
|
Loading…
Add table
Reference in a new issue