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

Merge branch 'develop' into 'master'

Develop --> Master

See merge request acs/public/villas/web!66
This commit is contained in:
Sonja Happ 2020-11-19 15:25:30 +01:00
commit 55ed652ed6
190 changed files with 22601 additions and 14008 deletions

View file

@ -1,7 +1,7 @@
variables:
GIT_SUBMODULE_STRATEGY: normal
DOCKER_TAG: ${CI_COMMIT_REF_NAME}
DOCKER_IMAGE_DEV: villas/web-dev:${CI_COMMIT_REF_NAME}
DOCKER_TAG: ${CI_COMMIT_SHORT_SHA}
DOCKER_IMAGE: ${CI_REGISTRY_IMAGE}
cache:
untracked: true
@ -13,54 +13,38 @@ cache:
- .yarn
stages:
- prepare
- build
- test
# - deploy
prepare:
stage: prepare
script:
- docker build -t ${DOCKER_IMAGE_DEV} -f packaging/docker/Dockerfile.dev .
tags:
- linux
- shell
- deploy
build_job:
build:
stage: build
image: node:12.2
before_script:
- mkdir -p build
script:
- npm install
- npm run build
image: ${DOCKER_IMAGE_DEV}
artifacts:
paths:
- build/
expire_in: 1 week
tags:
- docker
test_job:
stage: test
deploy:
stage: deploy
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [ "" ]
script:
- npm test
image: ${DOCKER_IMAGE_DEV}
dependencies:
- build_job
tags:
- docker
#deploy:docker:
# stage: deploy
# script:
# - docker build -t ${DOCKER_IMAGE} -f packaging/docker/Dockerfile .
# - docker tag villas/web:${DOCKER_TAG} villas/web:latest
# - docker push villas/web:${DOCKER_TAG}
# - docker push villas/web:latest
# tags:
# - shell
# - linux
# only:
# refs:
# - master
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"${CI_REGISTRY}\":{\"username\":\"${CI_REGISTRY_USER}\",\"password\":\"${CI_REGISTRY_PASSWORD}\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor
--context ${CI_PROJECT_DIR}
--dockerfile ${CI_PROJECT_DIR}/Dockerfile
--destination ${DOCKER_IMAGE}:${DOCKER_TAG}
--snapshotMode=redo
--cache=true
--cache-ttl=12h
only:
refs:
- master
- develop

View file

@ -1 +0,0 @@
packaging/docker/Dockerfile

35
Dockerfile Normal file
View file

@ -0,0 +1,35 @@
#
# 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/>.
# ******************************************************************************
FROM node:12.2 AS builder
# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# use changes to package.json to force Docker not to use the cache
# when we change our application's nodejs dependencies:
ADD package.json /usr/src/app
RUN npm install
# Install app dependencies
COPY . /usr/src/app
RUN npm run build
FROM nginx
COPY --from=builder /usr/src/app/build /usr/share/nginx/html

View file

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

View file

@ -1,39 +1,42 @@
# <img src="doc/pictures/villas_web.png" width=40 /> VILLASweb
## Description
This is VILLASweb, the website displaying and processing simulation data in the web browser. The term __frontend__ refers to this project, the actual website.
The frontend connects to __two__ backends: _VILLASweb-backend_ and _VILLASnode_.
VILLASnode provides actual simulation data via websockets. VILLASweb-backend provides any other data like user acounts, simulation configuration etc.
This is VILLASweb, the website to configure real-time co-simulations and display simulation real-time data in the web browser.
The term **frontend** refers to this project, the actual website.
The frontend connects to **two** backends: [VILLASweb-backend-go](https://git.rwth-aachen.de/acs/public/villas/web-backend-go) and [VILLASnode](https://git.rwth-aachen.de/acs/public/villas/node).
VILLASnode provides actual simulation data via websockets. VILLASweb-backend-go provides any other data such as user accounts, infrastructure components and configurations, dashboards etc.
For more information on the backends see their repositories.
## Frameworks
The frontend is build upon [ReactJS](https://facebook.github.io/react/) and [Flux](https://facebook.github.io/flux/).
React is responsible for rendering the UI and Flux for handling the data and communication with the backends. For more information also have a look at REACT.md
Additional libraries are used, for a complete list see the file `package.json`.
Additional libraries are used, for a complete list see package.json.
## Data model
![Datamodel](src/img/datamodel.png)
## Quick start
We recommend Docker to get started quickly:
```bash
$ git clone --recursive git@git.rwth-aachen.de:VILLASframework/VILLASweb.git
$ cd VILLASweb
$ git clone --recursive https://git.rwth-aachen.de/acs/public/villas/web.git
$ cd web
$ npm install
$ npm start
```
We recommend to start the VILLASweb-backend-go before the frontend.
If you want to use test data (including some test users), you can start the backend with the parameter `-mode=test`.
Please check the repository of the VILLASweb-backend-go to find information on the test user login names and passwords.
The testing mode is NOT intended for production deployments.
The default user and password are configured in the `config.js` file of the _backend_. By default they are: __admin__ / __admin__.
## Documentation
More details on the setup and usage of VILLASweb is available here:
- [Requirements](doc/Requirements.md)
- [Structure and datamodel](doc/Structure.md)
- [Development setup](doc/development.md)
- [Production setup](doc/Production.md)
## Copyright
2017, Institute for Automation of Complex Power Systems, EONERC
2020, Institute for Automation of Complex Power Systems, EONERC
## License
@ -66,6 +69,7 @@ For other licensing options please consult [Prof. Antonello Monti](mailto:amonti
[![EONERC ACS Logo](doc/pictures/eonerc_logo.png)](http://www.acs.eonerc.rwth-aachen.de)
- Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
- Sonja Happ <sonja.happ@eonerc.rwth-aachen.de>
[Institute for Automation of Complex Power Systems (ACS)](http://www.acs.eonerc.rwth-aachen.de)
[EON Energy Research Center (EONERC)](http://www.eonerc.rwth-aachen.de)

82
doc/Production.md Normal file
View file

@ -0,0 +1,82 @@
# Production Setup {#web-production}
## Setting up VILLASweb for production
For development setup instructions see @ref web-development.
The production setup is based on docker.
Clone the [frontend](https://git.rwth-aachen.de/acs/public/villas/web) and [backend](https://git.rwth-aachen.de/acs/public/villas/web-backend-go) repositories on your computer and build the Docker images for both:
### Frontend
- `cd VILLASweb`
- `docker build -t villasweb-frontend .`
### Backend
- `cd ..\VILLASweb-backend-go`
- `docker build -t villasweb-backend .`
### WIP Docker compose and/or Kubernetes
Run the production docker-compose file:
- `docker-compose -f docker-compose-production.yml up -d`
## Configure VILLASnode to get data into VILLASweb
### Install VILLASnode
See: @ref node-installation
### Create a VILLASnode demo data source
1. Create a new empty configuration file with the following contents and save it as `webdemo.conf`:
> WIP this example configuration requires revision!
```
nodes = {
sine = {
type = "signal"
signal = "mixed"
values = 5
rate = 25
frequency = 5
}
web = {
type = "websocket"
destinations = [
"TODO"
]
}
}
paths = (
{
in = "sine"
out = "web"
}
)
```
The node `sine` is a software signal generator for 5 signals.
The node `web` is the websocket interface to stream the data generated by the `sine` node to the browser.
> Note: If you do not want to use your local system as the destination for the websocket node,
>change the option `destinations` of the `web` node to the destination of your production environment, for example `https://my.production.environment/ws/webdemo`.
### Start the VILLASnode gateway
Run the following command on your system:
```bash
villas node webdemo.conf
```
> Note: Change the path to the configuration file accordingly. The `villas` command will only work if VILLASnode is installed on your system.
### Visualize real-time data in VILLASweb Dashboards
1. Use the VILLASweb frontend to create a new infrastructure component for the VILLASnode gateway from above (Admin user required).
2. Set the `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.

12
doc/Requirements.md Normal file
View file

@ -0,0 +1,12 @@
# 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

79
doc/Structure.md Normal file
View file

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

70
doc/development.md Normal file
View file

@ -0,0 +1,70 @@
# 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.

17981
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,47 +3,65 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.8",
"@fortawesome/free-solid-svg-icons": "^5.5.0",
"@fortawesome/react-fontawesome": "^0.1.3",
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "^0.1.12",
"babel-runtime": "^6.26.0",
"bootstrap": "^3.3.7",
"bootstrap": "^4.5.3",
"bufferutil": "^4.0.1",
"canvas": "^2.6.1",
"classnames": "^2.2.6",
"d3-array": "^1.2.4",
"d3-axis": "^1.0.12",
"d3-scale": "^1.0.6",
"d3-selection": "^1.3.2",
"d3-shape": "^1.2.2",
"d3-time-format": "^2.1.3",
"es6-promise": "^4.2.5",
"file-saver": "^1.3.8",
"flux": "^3.1.2",
"gaugeJS": "^1.3.2",
"handlebars": "^4.1.1",
"immutable": "^3.8.1",
"jszip": "^3.2.0",
"d3-array": "^2.8.0",
"d3-axis": "^2.0.0",
"d3-scale": "^3.2.3",
"d3-scale-chromatic": "^2.0.0",
"d3-selection": "^2.0.0",
"d3-shape": "^2.0.0",
"d3-time-format": "^3.0.0",
"es6-promise": "^4.2.8",
"fibers": "^5.0.0",
"file-saver": "^2.0.2",
"flux": "^3.1.3",
"gaugeJS": "^1.3.7",
"handlebars": "^4.7.6",
"jquery": "^3.5.1",
"jszip": "^3.5.0",
"libcimsvg": "git+https://git.rwth-aachen.de/acs/public/cim/pintura-npm-package.git",
"lodash": "^4.17.11",
"prop-types": "^15.6.2",
"rc-slider": "^8.6.3",
"react": "^16.6.3",
"react-bootstrap": "^0.31.1",
"react-contexify": "^3.0.3",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"multiselect-react-dropdown": "^1.6.1",
"node-sass": "^4.14.1",
"popper.js": "^1.16.1",
"prop-types": "^15.7.2",
"rc-slider": "^9.6.0",
"react": "^16.14.0",
"react-bootstrap": "^1.4.0",
"react-bootstrap-time-picker": "^2.0.1",
"react-collapse": "^5.0.1",
"react-color": "^2.19.3",
"react-contexify": "^4.1.1",
"react-d3": "^0.4.0",
"react-dnd": "^2.6.0",
"react-dnd-html5-backend": "^2.6.0",
"react-dom": "^16.6.3",
"react-fullscreenable": "^2.5.0",
"react-dnd": "^10.0.2",
"react-dnd-html5-backend": "^10.0.2",
"react-dom": "^16.14.0",
"react-fullscreenable": "^2.5.1-0",
"react-grid-system": "^7.1.1",
"react-json-view": "^1.19.1",
"react-notification-system": "^0.2.17",
"react-rnd": "^7.4.3",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-scripts": "^3.0.1",
"react-sortable-tree": "^0.1.19",
"react-svg-pan-zoom": "^2.18.0",
"superagent": "^3.8.3",
"validator": "^10.9.0"
"react-notification-system": "^0.4.0",
"react-rnd": "^10.2.3",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.0",
"react-svg-pan-zoom": "^3.8.1",
"sass": "^1.28.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",
"validator": "^13.1.17",
"webpack-hot-middleware": "^2.25.0",
"webpack-plugin-serve": "^1.2.0"
},
"devDependencies": {
"chai": "^4.2.0"

View file

@ -1,21 +0,0 @@
FROM node:12.2 AS builder
RUN apt-get install -y \
git
# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# use changes to package.json to force Docker not to use the cache
# when we change our application's nodejs dependencies:
ADD package.json /usr/src/app
RUN npm install
# Install app dependencies
COPY . /usr/src/app
RUN npm run build
FROM nginx
COPY --from=builder /usr/src/app/build /usr/share/nginx/html

View file

@ -1,4 +0,0 @@
FROM node:12.2
RUN apt-get install -y \
git

View file

@ -1,53 +0,0 @@
import { expect } from 'chai';
import createControls from '../../../components/dialogs/edit-widget-control-creator';
import EditWidgetTextControl from '../../../components/dialogs/edit-widget-text-control';
import EditWidgetColorControl from '../../../components/dialogs/edit-widget-color-control';
import EditWidgetTimeControl from '../../../components/dialogs/edit-widget-time-control';
import EditImageWidgetControl from '../../../components/dialogs/edit-widget-image-control';
import EditWidgetSimulationControl from '../../../components/dialogs/edit-widget-simulation-control';
import EditWidgetSignalControl from '../../../components/dialogs/edit-widget-signal-control';
import EditWidgetSignalsControl from '../../../components/dialogs/edit-widget-signals-control';
import EditWidgetOrientation from '../../../components/dialogs/edit-widget-orientation';
import EditWidgetTextSizeControl from '../../../components/dialogs/edit-widget-text-size-control';
import EditWidgetAspectControl from '../../../components/dialogs/edit-widget-aspect-control';
import EditWidgetCheckboxControl from '../../../components/dialogs/edit-widget-checkbox-control';
import EditWidgetMinMaxControl from '../../../components/dialogs/edit-widget-min-max-control';
import EditWidgetColorZonesControl from '../../../components/dialogs/edit-widget-color-zones-control';
import EditWidgetHTMLContent from '../../../components/dialogs/edit-widget-html-content';
import EditWidgetNumberControl from '../../../components/dialogs/edit-widget-number-control';
describe('edit widget control creator', () => {
it('should not return null', () => {
let controls = createControls('Value', null, null, null, null, null, null);
expect(controls).to.be.not.undefined;
});
var runs = [
{ args: { widgetType: 'Lamp' }, result: { controlNumber: 5, controlTypes: [EditWidgetSimulationControl, EditWidgetSignalControl, EditWidgetTextControl, EditWidgetColorControl, EditWidgetColorControl] } },
{ args: { widgetType: 'Value' }, result: { controlNumber: 5, controlTypes: [EditWidgetTextControl, EditWidgetSimulationControl, EditWidgetSignalControl, EditWidgetTextSizeControl, EditWidgetCheckboxControl] } },
{ args: { widgetType: 'Plot' }, result: { controlNumber: 5, controlTypes: [EditWidgetTimeControl, EditWidgetSimulationControl, EditWidgetSignalsControl, EditWidgetTextControl, EditWidgetMinMaxControl] } },
{ args: { widgetType: 'Table' }, result: { controlNumber: 2, controlTypes: [EditWidgetSimulationControl, EditWidgetCheckboxControl] } },
{ args: { widgetType: 'Image' }, result: { controlNumber: 2, controlTypes: [EditImageWidgetControl, EditWidgetAspectControl] } },
{ args: { widgetType: 'Gauge' }, result: { controlNumber: 6, controlTypes: [EditWidgetTextControl, EditWidgetSimulationControl, EditWidgetSignalControl, EditWidgetCheckboxControl, EditWidgetColorZonesControl, EditWidgetMinMaxControl] } },
{ args: { widgetType: 'PlotTable' }, result: { controlNumber: 5, controlTypes: [EditWidgetSimulationControl, EditWidgetSignalsControl, EditWidgetTextControl, EditWidgetTimeControl, EditWidgetMinMaxControl] } },
{ args: { widgetType: 'Slider' }, result: { controlNumber: 9, controlTypes: [EditWidgetTextControl, EditWidgetOrientation, EditWidgetSimulationControl, EditWidgetSignalControl, EditWidgetCheckboxControl, EditWidgetCheckboxControl, EditWidgetMinMaxControl, EditWidgetNumberControl, EditWidgetNumberControl] } },
{ args: { widgetType: 'Button' }, result: { controlNumber: 6, controlTypes: [EditWidgetTextControl, EditWidgetSimulationControl, EditWidgetSignalControl, EditWidgetCheckboxControl, EditWidgetNumberControl, EditWidgetNumberControl] } },
{ args: { widgetType: 'Box' }, result: { controlNumber: 2, controlTypes: [EditWidgetColorControl, EditWidgetColorControl] } },
{ args: { widgetType: 'Label' }, result: { controlNumber: 3, controlTypes: [EditWidgetTextControl, EditWidgetTextSizeControl, EditWidgetColorControl] } },
{ args: { widgetType: 'HTML' }, result: { controlNumber: 1, controlTypes: [EditWidgetHTMLContent] } },
{ args: { widgetType: 'Input'}, result: { controlNumber: 3, controlTypes: [EditWidgetTextControl, EditWidgetSimulationControl, EditWidgetSignalControl] } }
];
runs.forEach( (run) => {
let itMsg = run.args.widgetType + ' widget edit model should have correct controls';
it(itMsg, () => {
let controls = createControls(run.args.widgetType, null, null, null, null, null, null);
expect(controls).to.have.lengthOf(run.result.controlNumber);
controls.forEach( (control) => expect(control.type).to.be.oneOf(run.result.controlTypes))
});
});
});

View file

@ -0,0 +1,51 @@
import { expect } from 'chai';
import createControls from '../../widget/edit-widget/edit-widget-control-creator';
import EditWidgetTextControl from '../../widget/edit-widget/edit-widget-text-control';
import EditWidgetColorControl from '../../widget/edit-widget/edit-widget-color-control';
import EditWidgetTimeControl from '../../widget/edit-widget/edit-widget-time-control';
import EditFileWidgetControl from '../../widget/edit-widget/edit-widget-file-control';
import EditWidgetSignalControl from '../../widget/edit-widget/edit-widget-signal-control';
import EditWidgetSignalsControl from '../../widget/edit-widget/edit-widget-signals-control';
import EditWidgetOrientation from '../../widget/edit-widget/edit-widget-orientation';
import EditWidgetTextSizeControl from '../../widget/edit-widget/edit-widget-text-size-control';
import EditWidgetAspectControl from '../../widget/edit-widget/edit-widget-aspect-control';
import EditWidgetCheckboxControl from '../../widget/edit-widget/edit-widget-checkbox-control';
import EditWidgetMinMaxControl from '../../widget/edit-widget/edit-widget-min-max-control';
import EditWidgetColorZonesControl from '../../widget/edit-widget/edit-widget-color-zones-control';
import EditWidgetHTMLContent from '../../widget/edit-widget/edit-widget-html-content';
import EditWidgetNumberControl from '../../widget/edit-widget/edit-widget-number-control';
describe('edit widget control creator', () => {
it('should not return null', () => {
let controls = createControls('Value', null, null, null, null, null, null);
expect(controls).to.be.not.undefined;
});
var runs = [
{ args: { widgetType: 'Lamp' }, result: { controlNumber: 5, controlTypes: [EditWidgetSignalControl, EditWidgetTextControl, EditWidgetColorControl, EditWidgetColorControl] } },
{ args: { widgetType: 'Value' }, result: { controlNumber: 5, controlTypes: [EditWidgetTextControl, EditWidgetSignalControl, EditWidgetTextSizeControl, EditWidgetCheckboxControl] } },
{ args: { widgetType: 'Plot' }, result: { controlNumber: 5, controlTypes: [EditWidgetTimeControl, EditWidgetSignalsControl, EditWidgetTextControl, EditWidgetMinMaxControl] } },
{ args: { widgetType: 'Table' }, result: { controlNumber: 2, controlTypes: [EditWidgetCheckboxControl] } },
{ args: { widgetType: 'Image' }, result: { controlNumber: 2, controlTypes: [EditFileWidgetControl, EditWidgetAspectControl] } },
{ args: { widgetType: 'Gauge' }, result: { controlNumber: 6, controlTypes: [EditWidgetTextControl, EditWidgetSignalControl, EditWidgetCheckboxControl, EditWidgetColorZonesControl, EditWidgetMinMaxControl] } },
{ args: { widgetType: 'Slider' }, result: { controlNumber: 9, controlTypes: [EditWidgetTextControl, EditWidgetOrientation, EditWidgetSignalControl, EditWidgetCheckboxControl, EditWidgetCheckboxControl, EditWidgetMinMaxControl, EditWidgetNumberControl, EditWidgetNumberControl] } },
{ args: { widgetType: 'Button' }, result: { controlNumber: 6, controlTypes: [EditWidgetTextControl, EditWidgetSignalControl, EditWidgetCheckboxControl, EditWidgetNumberControl, EditWidgetNumberControl] } },
{ args: { widgetType: 'Box' }, result: { controlNumber: 2, controlTypes: [EditWidgetColorControl, EditWidgetColorControl] } },
{ args: { widgetType: 'Label' }, result: { controlNumber: 3, controlTypes: [EditWidgetTextControl, EditWidgetTextSizeControl, EditWidgetColorControl] } },
{ args: { widgetType: 'HTML' }, result: { controlNumber: 1, controlTypes: [EditWidgetHTMLContent] } },
{ args: { widgetType: 'Input'}, result: { controlNumber: 3, controlTypes: [EditWidgetTextControl, EditWidgetSignalControl] } }
];
runs.forEach( (run) => {
let itMsg = run.args.widgetType + ' widget edit should have correct controls';
it(itMsg, () => {
let controls = createControls(run.args.widgetType, null, null, null, null, null, null);
expect(controls).to.have.lengthOf(run.result.controlNumber);
controls.forEach( (control) => expect(control.type).to.be.oneOf(run.result.controlTypes))
});
});
});

132
src/app.js Normal file
View file

@ -0,0 +1,132 @@
/**
* 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 { DndProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import NotificationSystem from 'react-notification-system';
import { Redirect, Route } from 'react-router-dom';
import { Col } from 'react-bootstrap';
import { Hidden } from 'react-grid-system'
import AppDispatcher from './common/app-dispatcher';
import NotificationsDataManager from './common/data-managers/notifications-data-manager';
import Home from './common/home';
import Header from './common/header';
import Footer from './common/footer';
import SidebarMenu from './common/menu-sidebar';
import HeaderMenu from './common/header-menu';
import InfrastructureComponents from './ic/ics';
import Dashboard from './dashboard/dashboard';
import Scenarios from './scenario/scenarios';
import Scenario from './scenario/scenario';
import Users from './user/users';
import User from './user/user';
import './styles/app.css';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
showSidebarMenu: false,
}
}
componentDidMount() {
NotificationsDataManager.setSystem(this.refs.notificationSystem);
// if token stored locally, we are already logged-in
let token = localStorage.getItem("token");
if (token != null && token !== '') {
let currentUser = JSON.parse(localStorage.getItem("currentUser"));
console.log("Already logged-in")
AppDispatcher.dispatch({
type: 'users/logged-in',
token: token,
currentUser: currentUser
});
}
}
showSidebarMenu = () => {
this.setState({ showSidebarMenu: true });
};
hideSidebarMenu = () => {
this.setState({ showSidebarMenu: false });
};
render() {
let token = localStorage.getItem("token");
let currentUserRaw = localStorage.getItem("currentUser");
if (token == null || token === "" || currentUserRaw == null || currentUserRaw === "") {
console.log("APP redirecting to logout/ login")
return (<Redirect to="/logout" />);
}
let currentUser = JSON.parse(currentUserRaw);
return (
<DndProvider backend={HTML5Backend} >
<div>
{/*
<Col style={{ width: this.state.showSidebarMenu ? '280px' : '0px' }} smHidden mdHidden lgHidden className="sidenav">
*/}
<Hidden sm md lg xl xxl>
<Col style={{ width: this.state.showSidebarMenu ? '280px' : '0px' }} className="sidenav">
<HeaderMenu onClose={this.hideSidebarMenu} currentRole={currentUser.role} />
</Col>
</Hidden>
<div className="app">
<NotificationSystem ref="notificationSystem" />
<Header onMenuButton={this.showSidebarMenu} showMenuButton={false} />
<div className={`app-body app-body-spacing`} >
<Col xs={false}>
<SidebarMenu currentRole={currentUser.role} />
</Col>
<div className={`app-content app-content-margin-left`}>
<Route exact path="/" component={Home} />
<Route path="/home" component={Home} />
<Route exact path="/scenarios" component={Scenarios} />
<Route path="/scenarios/:scenario" component={Scenario} />
<Route path="/dashboards/:dashboard" component={Dashboard} />
<Route path="/infrastructure" component={InfrastructureComponents} />
<Route path="/account" component={User} />
<Route path="/users" component={Users} />
</div>
</div>
<Footer />
</div>
</div>
</DndProvider>
)
}
}
export default App

View file

@ -1,8 +1,4 @@
/**
* File: visualizations-data-manager.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 03.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -19,6 +15,20 @@
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import RestDataManager from './rest-data-manager';
/// FluxContainerConverter.js
/// ATTENTION!!! This is an ugly workaround found here https://github.com/facebook/flux/issues/351 to make Flux Containers work with ES6
function convert(containerClass) {
const tmp = containerClass;
containerClass = function(...args) {
return new tmp(...args);
};
containerClass.prototype = tmp.prototype;
containerClass.getStores = tmp.getStores;
containerClass.calculateState = tmp.calculateState;
return containerClass;
}
export {convert}
export default new RestDataManager('visualization', '/visualizations');

View file

@ -1,8 +1,4 @@
/**
* File: rest-api.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -23,7 +19,6 @@ import request from 'superagent/lib/client';
import Promise from 'es6-promise';
import NotificationsDataManager from '../data-managers/notifications-data-manager';
// TODO: Add this to a central pool of notifications
const SERVER_NOT_REACHABLE_NOTIFICATION = {
title: 'Server not reachable',
@ -37,32 +32,36 @@ const REQUEST_TIMEOUT_NOTIFICATION = {
level: 'error'
};
// Check if the error was due to network failure, timeouts, etc.
// Check if the error was due to network failure, timeouts, etc.
// Can be used for the rest of requests
function isNetworkError(err) {
let result = false;
// If not status nor response fields, it is a network error. TODO: Handle timeouts
if (err.status == null || err.response == null) {
if (err.status == null || err.status === 500 || err.response == null) {
result = true;
let notification = err.timeout? REQUEST_TIMEOUT_NOTIFICATION : SERVER_NOT_REACHABLE_NOTIFICATION;
NotificationsDataManager.addNotification(notification);
}
return result;
}
let prevURL = null;
class RestAPI {
get(url, token) {
return new Promise(function (resolve, reject) {
var req = request.get(url);
if (token != null) {
req.set('x-access-token', token);
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 {
resolve(JSON.parse(res.text));
@ -76,9 +75,9 @@ class RestAPI {
var req = request.post(url).send(body).timeout({ response: 5000 }); // Simple response start timeout (3s)
if (token != null) {
req.set('x-access-token', token);
req.set('Authorization', "Bearer " + token);
}
req.end(function (error, res) {
if (res == null || res.status !== 200) {
@ -97,11 +96,12 @@ class RestAPI {
var req = request.delete(url);
if (token != null) {
req.set('x-access-token', token);
req.set('Authorization', "Bearer " + token);
}
req.end(function (error, res) {
if (res == null || res.status !== 200) {
error.handled = isNetworkError(error);
reject(error);
} else {
resolve(JSON.parse(res.text));
@ -115,11 +115,12 @@ class RestAPI {
var req = request.put(url).send(body);
if (token != null) {
req.set('x-access-token', token);
req.set('Authorization', "Bearer " + token);
}
req.end(function (error, res) {
if (res == null || res.status !== 200) {
error.handled = isNetworkError(error);
reject(error);
} else {
resolve(JSON.parse(res.text));
@ -128,16 +129,17 @@ class RestAPI {
});
}
upload(url, data, token, progressCallback) {
upload(url, data, token, progressCallback, scenarioID) {
return new Promise(function (resolve, reject) {
const req = request.post(url).send(data).on('progress', progressCallback);
const req = request.post(url + "?scenarioID=" + scenarioID).send(data).on('progress', progressCallback);
if (token != null) {
req.set('x-access-token', token);
req.set('Authorization', "Bearer " + token);
}
req.end(function (error, res) {
if (res == null || res.status !== 200) {
error.handled = isNetworkError(error);
reject(error);
} else {
resolve(JSON.parse(res.text));
@ -145,6 +147,29 @@ 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);
}
req.end(function (error, res) {
if (error !== null || res.status !== 200) {
error.handled = isNetworkError(error);
reject(error);
} else {
// file data is contained in res.body (because of blob response type)
let parts = url.split("/");
resolve({data: res.body, type: res.type, id: parts[parts.length-1]})
}
});
});
}
}
export default new RestAPI();

View file

@ -1,8 +1,4 @@
/**
* File: websocket-api.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 03.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -20,19 +16,19 @@
******************************************************************************/
class WebsocketAPI {
constructor(endpoint, callbacks) {
this.endpoint = endpoint;
constructor(websocketurl, callbacks) {
this.websocketurl = websocketurl;
this.callbacks = callbacks;
this.wasConnected = false;
this.isClosing = false;
this.connect(endpoint, callbacks);
this.connect(websocketurl, callbacks);
}
connect(endpoint, callbacks) {
connect(websocketurl, callbacks) {
// create web socket client
this.socket = new WebSocket(WebsocketAPI.getURL(endpoint), 'live');
this.socket = new WebSocket(WebsocketAPI.getURL(websocketurl), 'live');
this.socket.binaryType = 'arraybuffer';
this.socket.onclose = this.onClose;
this.socket.onopen = this.onOpen;
@ -44,12 +40,12 @@ class WebsocketAPI {
}
reconnect() {
//console.log("Reconnecting: " + this.endpoint);
this.connect(this.endpoint, this.callbacks);
//console.log("Reconnecting: " + this.websocketurl);
this.connect(this.websocketurl, this.callbacks);
}
get url() {
return WebsocketAPI.getURL(this.endpoint);
return WebsocketAPI.getURL(this.websocketurl);
}
send(data) {
@ -62,7 +58,7 @@ class WebsocketAPI {
}
onError = e => {
console.error('Error on WebSocket connection to: ' + this.endpoint + ':', e);
console.error('Error on WebSocket connection to: ' + this.websocketurl + ':', e);
if ('onError' in this.callbacks)
this.callbacks.onError(e);
@ -82,16 +78,16 @@ class WebsocketAPI {
}
else {
if (this.wasConnected) {
console.log("Connection to " + this.endpoint + " dropped. Attempt reconnect in 1 sec");
console.log("Connection to " + this.websocketurl + " dropped. Attempt reconnect in 1 sec");
window.setTimeout(() => { this.reconnect(); }, 1000);
}
}
}
static getURL(endpoint) {
static getURL(websocketurl) {
// create an anchor element (note: no need to append this element to the document)
var link = document.createElement('a');
link.href = endpoint;
link.href = websocketurl;
if (link.protocol === 'https:')
link.protocol = 'wss:';

View file

@ -1,8 +1,4 @@
/**
* File: app-dispatcher.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify

View file

@ -1,8 +1,4 @@
/**
* File: array-store.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 03.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -21,7 +17,8 @@
import { ReduceStore } from 'flux/utils';
import AppDispatcher from '../app-dispatcher';
import AppDispatcher from './app-dispatcher';
import NotificationsDataManager from '../common/data-managers/notifications-data-manager';
class ArrayStore extends ReduceStore {
constructor(type, dataManager) {
@ -39,7 +36,7 @@ class ArrayStore extends ReduceStore {
// search for existing element to update
state.forEach((element, index, array) => {
newElements = newElements.filter((updateElement, newIndex) => {
if (element._id === updateElement._id) {
if (element.id === updateElement.id) {
// update each property
for (var key in updateElement) {
if (updateElement.hasOwnProperty(key)) {
@ -69,10 +66,10 @@ class ArrayStore extends ReduceStore {
case this.type + '/start-load':
if (Array.isArray(action.data)) {
action.data.forEach((id) => {
this.dataManager.load(id, action.token);
this.dataManager.load(id, action.token,action.param);
});
} else {
this.dataManager.load(action.data, action.token);
this.dataManager.load(action.data, action.token,action.param);
}
return state;
@ -84,43 +81,73 @@ class ArrayStore extends ReduceStore {
}
case this.type + '/load-error':
// TODO: Add error message
return state;
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);
}
return super.reduce(state, action);
case this.type + '/start-add':
this.dataManager.add(action.data, action.token);
this.dataManager.add(action.data, action.token,action.param);
return state;
case this.type + '/added':
if(typeof action.data.managedexternally !== "undefined" && action.data.managedexternally === true ) return state;
return this.updateElements(state, [action.data]);
case this.type + '/add-error':
// TODO: Add error message
return state;
return state;
case this.type + '/start-remove':
this.dataManager.remove(action.data, action.token);
this.dataManager.remove(action.data, action.token,action.param);
return state;
case this.type + '/removed':
return state.filter((item) => {
return (item !== action.original);
});
if (action.original) {
return state.filter((item) => {
return (item !== action.original);
});
} else {
return state.filter((item) => {
return (item.id !== action.data);
});
}
case this.type + '/remove-error':
// TODO: Add error message
return state;
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);
}
return super.reduce(state, action);
case this.type + '/start-edit':
this.dataManager.update(action.data, action.token);
if(action.id){
this.dataManager.update(action.data, action.token,action.param,action.id);
}
else{
this.dataManager.update(action.data, action.token,action.param);
}
return state;
case this.type + '/edited':
return this.updateElements(state, [action.data]);
case this.type + '/edit-error':
// TODO: Add error message
return state;
return state;
default:
return state;

View file

@ -1,8 +1,4 @@
/**
* File: notifications-data-manager.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 21.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify

View file

@ -1,8 +1,4 @@
/**
* File: simulation-data-manager.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 04.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -19,6 +15,17 @@
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import RestDataManager from './rest-data-manager';
class NotificationsFactory {
export default new RestDataManager('simulation', '/simulations', [ '_id', 'name', 'projects', 'models', 'startParameters' ]);
// This is an example
static get EXAMPLE_NOTIFICATION() {
return {
title: 'Example notification',
message: 'Write something here that describes what happend.',
level: 'warning'
};
}
}
export default NotificationsFactory;

View file

@ -0,0 +1,229 @@
/**
* 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 RestAPI from '../api/rest-api';
import AppDispatcher from '../app-dispatcher';
const API_URL = '/api/v2';
class RestDataManager {
constructor(type, url, keyFilter) {
this.url = url;
this.type = type;
this.keyFilter = keyFilter;
this.onLoad = null;
}
makeURL(part) {
return API_URL + part;
}
filterKeys(object) {
// don't change anything if no filter is set
if (this.keyFilter == null || Array.isArray(this.keyFilter) === false) {
return object;
}
// remove all keys not in the filter
Object.keys(object).filter(key => {
return this.keyFilter.indexOf(key) === -1;
}).forEach(key => {
delete object[key];
});
return object;
}
requestURL(form, id, param, object = null){
switch(form){
case 'load/add':
if (param === null){
if(id != null){
return this.makeURL(this.url + '/' + id);
}
else {
return this.makeURL(this.url);
}
}
else{
if(id != null){
return this.makeURL(this.url + '/' + id + param);
}
else {
return this.makeURL(this.url + param)
}
}
case 'remove/update':
if(id !== null){
return this.makeURL(this.url + '/' + id);
}
else if(param === null){
return this.makeURL(this.url + '/' + object.id);
}
else{
return this.makeURL(this.url + '/' + object.id + param);
}
default:
console.log("something went wrong");
break;
}
}
load(id, token = null,param = null) {
if (id != null) {
// load single object
RestAPI.get(this.requestURL('load/add',id,param), token).then(response => {
let data;
if (response.hasOwnProperty(this.type)) {
data = this.filterKeys(response[this.type]);
}else{
// loaded file
data = response;
}
AppDispatcher.dispatch({
type: this.type + 's/loaded',
data: data,
token: token
});
if (this.onLoad != null) {
this.onLoad(data, token);
}
}).catch(error => {
AppDispatcher.dispatch({
type: this.type + 's/load-error',
error: error
});
});
} else {
// load all objects
RestAPI.get(this.requestURL('load/add',id,param), token).then(response => {
const data = response[this.type + 's'].map(element => {
return this.filterKeys(element);
});
AppDispatcher.dispatch({
type: this.type + 's/loaded',
data: data,
token: token,
});
if (this.onLoad != null) {
this.onLoad(data, token);
}
}).catch(error => {
AppDispatcher.dispatch({
type: this.type + 's/load-error',
error: error
});
});
}
}
add(object, token = null, param = null, subObjects = null) {
var obj = {};
obj[this.type] = this.filterKeys(object);
RestAPI.post(this.requestURL('load/add',null,param), obj, token).then(response => {
AppDispatcher.dispatch({
type: this.type + 's/added',
data: response[this.type],
token: token
});
// check if POST is done for import of object and issue dispatches of sub-objects
if (subObjects !== null){
// there are sub-objects to be added for an import
for (let objectType of subObjects){
let type = Object.keys(objectType) // type can be dashboards, configs, widgets, ...
type = type[0];
for (let newObj of objectType[type]){
// set the ID of the object that the sub-object shall be associated with
if(type === "configs" || type === "dashboards"){
// the main object is a scenario
newObj.scenarioID = response[this.type].id
} else if (type === "widgets") {
// the main object is a dashboard
newObj.dashboardID = response[this.type].id
} else if (type === "signals") {
// the main object is a component configuration
newObj.configID = response[this.type].id
}
// iterate over all objects of type 'type' add issue add dispatch
AppDispatcher.dispatch({
type: type + '/start-add',
data: newObj,
token: token
})
}
}
}
}).catch(error => {
AppDispatcher.dispatch({
type: this.type + 's/add-error',
error: error
});
});
}
remove(object, token = null, param = null) {
RestAPI.delete(this.requestURL('remove/update',null,param,object), token).then(response => {
AppDispatcher.dispatch({
type: this.type + 's/removed',
data: response[this.type],
original: object,
token: token
});
}).catch(error => {
AppDispatcher.dispatch({
type: this.type + 's/remove-error',
error: error
});
});
}
update(object, token = null, param = null, id = null) {
var obj = {};
obj[this.type] = this.filterKeys(object);
RestAPI.put(this.requestURL('remove/update',id,param,object), obj, token).then(response => {
AppDispatcher.dispatch({
type: this.type + 's/edited',
data: response[this.type]
});
}).catch(error => {
AppDispatcher.dispatch({
type: this.type + 's/edit-error',
error: error
});
});
}
};
export default RestDataManager;

View file

@ -1,8 +1,4 @@
/**
* File: edit-user.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 02.05.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -20,13 +16,14 @@
******************************************************************************/
import React from 'react';
import { Button, Modal } from 'react-bootstrap';
import { Button, Modal, FormLabel } from 'react-bootstrap';
import {Collapse} from 'react-collapse';
class DeleteDialog extends React.Component {
onModalKeyPress = (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.props.onClose(false);
}
}
@ -39,11 +36,14 @@ class DeleteDialog extends React.Component {
<Modal.Body>
Are you sure you want to delete the {this.props.title} <strong>'{this.props.name}'</strong>?
<Collapse isOpened={this.props.managedexternally} >
<FormLabel size="sm">The IC will be deleted if the respective VILLAScontroller sends "gone" state and no component config is using the IC anymore</FormLabel>
</Collapse>
</Modal.Body>
<Modal.Footer>
<Button onClick={() => this.props.onClose(false)}>Cancel</Button>
<Button bsStyle="danger" onClick={() => this.props.onClose(true)}>Delete</Button>
<Button variant="danger" onClick={() => this.props.onClose(true)}>Delete</Button>
</Modal.Footer>
</Modal>;
}

View file

@ -1,8 +1,4 @@
/**
* File: dialog.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 03.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -50,7 +46,7 @@ class Dialog extends React.Component {
render() {
return (
<Modal keyboard show={this.props.show} onEnter={this.props.onReset} onHide={this.cancelModal} onKeyPress={this.onKeyPress}>
<Modal size={this.props.size || 'sm'} keyboard show={this.props.show} onEnter={this.props.onReset} onHide={this.cancelModal} onKeyPress={this.onKeyPress}>
<Modal.Header>
<Modal.Title>{this.props.title}</Modal.Title>
</Modal.Header>
@ -60,7 +56,7 @@ class Dialog extends React.Component {
</Modal.Body>
<Modal.Footer>
<Button onClick={this.cancelModal}>Cancel</Button>
{this.props.blendOutCancel? <div></div>: <Button onClick={this.cancelModal}>Cancel</Button>}
<Button onClick={this.closeModal} disabled={!this.props.valid}>{this.props.buttonTitle}</Button>
</Modal.Footer>
</Modal>

View file

@ -1,8 +1,4 @@
/**
* File: header.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 25.05.2018
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -18,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
// TODO remove this file (not used!)
import React from 'react';
import PropTypes from 'prop-types';
import { FormControl, Button } from 'react-bootstrap';
@ -36,8 +32,10 @@ class EditableHeader extends React.Component {
};
}
componentWillReceiveProps(nextProps) {
this.setState({ title: nextProps.title });
static getDerivedStateFromProps(props, state){
return {
title: props.title
};
}
edit = () => {
@ -66,7 +64,7 @@ class EditableHeader extends React.Component {
};
const iconStyle = {
float: 'left',
float: 'right',
marginLeft: '10px',
marginTop: '25px',
@ -82,11 +80,11 @@ class EditableHeader extends React.Component {
return <div>
<form style={wrapperStyle}>
<FormControl type='text' bsSize='large' value={this.state.title} onChange={this.onChange} style={editStyle} autoFocus />
<FormControl type='text' size='large' value={this.state.title} onChange={this.onChange} style={editStyle} autoFocus />
</form>
<Button bsStyle="link" onClick={this.save}><Icon icon='check' style={iconStyle} /></Button>
<Button bsStyle="link" onClick={this.cancel}><Icon icon='times' style={iconStyle} /></Button>
<Button onClick={this.save}><Icon icon='check' style={iconStyle} /></Button>
<Button onClick={this.cancel}><Icon icon='times' style={iconStyle} /></Button>
</div>;
}
@ -95,7 +93,7 @@ class EditableHeader extends React.Component {
{this.state.title}
</h1>
<Button bsStyle="link" onClick={this.edit}><Icon icon='edit' style={iconStyle} /></Button>
<Button onClick={this.edit}><Icon icon='edit' style={iconStyle} /></Button>
</div>;
}
}

View file

@ -1,8 +1,4 @@
/**
* File: footer.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify

View file

@ -1,8 +1,4 @@
/**
* File: header-menu.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 17.08.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -26,16 +22,17 @@ import { NavLink } from 'react-router-dom';
export default class HeaderMenu extends React.Component {
render() {
return <div>
<Button className="closeButton" bsStyle="link" onClick={this.props.onClose}>&times;</Button>
<Button className="closeButton" variant="link" onClick={this.props.onClose}>&times;</Button>
<ul>
<li><NavLink to="/home" activeClassName="active" title="Home" onClick={this.props.onClose}>Home</NavLink></li>
<li><NavLink to="/projects" activeClassName="active" title="Projects" onClick={this.props.onClose}>Projects</NavLink></li>
<li><NavLink to="/simulations" activeClassName="active" title="Simulations" onClick={this.props.onClose}>Simulations</NavLink></li>
<li><NavLink to="/simulators" activeClassName="active" title="Simulators" onClick={this.props.onClose}>Simulators</NavLink></li>
{ this.props.currentRole === 'admin' ?
<li><NavLink to="/scenarios" activeClassName="active" title="Scenarios" onClick={this.props.onClose}>Scenarios</NavLink></li>
<li><NavLink to="/infrastructure" activeClassName="active" title="Infrastructure Components" onClick={this.props.onClose}>Infrastructure Components</NavLink></li>
{ this.props.currentRole === 'Admin' ?
<li><NavLink to="/users" activeClassName="active" title="User Management" onClick={this.props.onClose}>User Management</NavLink></li> : ''
}
<li><NavLink to="/account" title="Account">Account</NavLink></li>
<li><NavLink to="/logout" title="Logout" onClick={this.props.onClose}>Logout</NavLink></li>
</ul>
</div>;

View file

@ -1,8 +1,4 @@
/**
* File: header.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -21,20 +17,26 @@
import React from 'react';
import { Col, Button } from 'react-bootstrap';
import { Hidden } from 'react-grid-system'
import Icon from './icon';
import config from '../config';
class Header extends React.Component {
render() {
return (
<header className="app-header">
<Col xs={10} smOffset={2} sm={8}>
<h1>VILLASweb</h1>
</Col>
<Col xs={2} smHidden mdHidden lgHidden style={{ paddingLeft: 'auto', paddingRight: 0 }}>
{this.props.showMenuButton &&
<Button bsStyle="link" onClick={this.props.onMenuButton} style={{ float: 'right', marginRight: '10px' }}><Icon size="3x" icon="bars" className="menu-icon" /></Button>
}
<Col xs={{span: 10}} sm={{span: 8, offset: 2}}>
<h1>{config.instance} - {config.subtitle}</h1>
</Col>
<Hidden sm md lg xl>
<Col xs={2} style={{ paddingLeft: 'auto', paddingRight: 0 }}>
{this.props.showMenuButton &&
<Button variant="link" onClick={this.props.onMenuButton} style={{ float: 'right', marginRight: '10px' }}>
<Icon size="3x" icon="bars" className="menu-icon" />
</Button>
}
</Col>
</Hidden>
</header>
);
}

140
src/common/home.js Normal file
View file

@ -0,0 +1,140 @@
/**
* 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 config from '../config';
import {Redirect} from "react-router-dom";
class Home extends React.Component {
constructor(props) {
super(props);
// create url for API documentation, distinguish between localhost and production deployment
let docs_url = "";
let docs_location = "/swagger/index.html";
let base_url = window.location.origin;
if (base_url.search("localhost") === -1){
docs_url = base_url + docs_location;
} else {
// useful for local testing, replace port 3000 with port 4000 (port of backend)
docs_url = base_url.replace("3000", "4000") + docs_location;
}
this.state = {
docs_url: docs_url
};
}
getCounts(type) {
if (this.state.hasOwnProperty('counts'))
return this.state.counts[type];
else
return '?';
}
render() {
let currentUser = JSON.parse(localStorage.getItem("currentUser"));
if (currentUser == null){
console.log("HOME redirecting to logout/ login")
return (<Redirect to="/logout" />);
}
return (
<div className="home-container">
<img style={{height: 120, float: 'right'}} src={require('../img/villas_web.svg').default} alt="Logo VILLASweb" />
<h1>Home</h1>
<p>
Welcome to <b>{config.instance}</b> hosted by <a href={"mailto:" + config.admin.mail}>{config.admin.name}</a>!<br />
</p>
<p>
You are logged in as user <b>{currentUser.username}</b> with <b>ID {currentUser.id}</b> and role <b>{currentUser.role}</b>.
</p>
<p>
An interactive documentation of the VILLASweb API is available <a href={this.state.docs_url} target="_blank" rel="noopener noreferrer">here</a>.
</p>
<h3>Data Model</h3>
<img height={400} src={require('../img/datamodel.png').default} alt="Datamodel VILLASweb" />
<h3>Terminology </h3>
<h5>Infrastructure Component</h5>
<ul>
<li>A component of research infrastructure</li>
<li>Category: for example simulator, gateway, amplifier, database, etc.</li>
<li>Type: for example RTDS, OpalRT, VILLASnode, Cassandra</li>
</ul>
<h5>Component Configuration</h5>
<ul>
<li>Input signals: Signals that can be modified in VILLASweb</li>
<li>Output signals: Signals that can be visualized on dashboards of VILLASweb</li>
<li>Parameters: Further configuration parameters of the infrastructure component</li>
</ul>
<h5>Dashboard</h5>
<ul>
<li>Visualize ongoing experiments in real-time</li>
<li>Interact with ongoing experiments in real-time</li>
</ul>
<h5>Scenario</h5>
<ul>
<li>A collection of component configurations and dashboards for a specific experiment</li>
<li>Users can have access to multiple scenarios</li>
</ul>
<h3>Credits</h3>
<p>VILLASweb is developed by the <a href="http://acs.eonerc.rwth-aachen.de">Institute for Automation of Complex Power Systems</a> at the <a href="https;//www.rwth-aachen.de">RWTH Aachen University</a>.</p>
<ul>
<li><a href="mailto:stvogel@eonerc.rwth-aachen.de">Steffen Vogel</a></li>
<li><a href="mailto:sonja.happ@eonerc.rwth-aachen.de">Sonja Happ</a></li>
</ul>
<h3>Links</h3>
<ul>
<li><a href="http://fein-aachen.org/projects/villas-framework/">Project Page</a></li>
<li><a href="https://villas.fein-aachen.org/doc/web.html">Documentation</a></li>
<li><a href="https://git.rwth-aachen.de/acs/public/villas/web">Source Code</a></li>
</ul>
<h3>Funding</h3>
<p>The development of <a href="http://fein-aachen.org/projects/villas-framework/">VILLASframework</a> projects have received funding from</p>
<ul>
<li><a href="http://www.acs.eonerc.rwth-aachen.de/cms/E-ON-ERC-ACS/Forschung/Forschungsprojekte/Gruppe-Real-Time-Simulation-and-Hardware/~qxvw/Urban-Energy-Lab-4/">Urban Energy Lab 4.0</a> a project funded by OP EFRE NRW (European Regional Development Fund) for the setup of a novel energy research infrastructure.</li>
<li><a href="http://www.re-serve.eu">RESERVE</a> a European Unions Horizon 2020 research and innovation programme under grant agreement No 727481</li>
<li><a href="http://www.jara.org/en/research/energy">JARA-ENERGY</a>. Jülich-Aachen Research Alliance (JARA) is an initiative of RWTH Aachen University and Forschungszentrum Jülich.</li>
</ul>
<img height={100} src={require('../img/european_commission.svg').default} alt="Logo EU" />
<img height={70} src={require('../img/reserve.svg').default} alt="Logo EU" />
<img height={70} src={require('../img/uel_efre.jpeg').default} alt="Logo UEL OP EFRE NRW" />
<img height={70} src={require('../img/uel.png').default} alt="Logo UEL" />
<img height={60} src={require('../img/eonerc_rwth.svg').default} alt="Logo ACS" />
{
//<img height={70} src={require('../img/jara.svg').default} alt="Logo JARA" />
}
</div>
);
}
}
export default Home;

View file

@ -1,8 +1,4 @@
/**
* File: icon.js
* Author: Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* Date: 09.06.2018
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify

View file

@ -1,8 +1,4 @@
/**
* File: menu-sidebar.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -30,12 +26,12 @@ class SidebarMenu extends React.Component {
<ul>
<li><NavLink to="/home" activeClassName="active" title="Home">Home</NavLink></li>
<li><NavLink to="/projects" activeClassName="active" title="Projects">Projects</NavLink></li>
<li><NavLink to="/simulations" activeClassName="active" title="Simulations">Simulations</NavLink></li>
<li><NavLink to="/simulators" activeClassName="active" title="Simulators">Simulators</NavLink></li>
{ this.props.currentRole === 'admin' ?
<li><NavLink to="/users" activeClassName="active" title="User Management">Users</NavLink></li> : ''
<li><NavLink to="/scenarios" activeClassName="active" title="Scenarios">Scenarios</NavLink></li>
<li><NavLink to="/infrastructure" activeClassName="active" title="Infrastructure Components">Infrastructure Components</NavLink></li>
{ this.props.currentRole === 'Admin' ?
<li><NavLink to="/users" activeClassName="active" title="User Management">User Management</NavLink></li> : ''
}
<li><NavLink to="/account" title="Account">Account</NavLink></li>
<li><NavLink to="/logout" title="Logout">Logout</NavLink></li>
</ul>
</div>

View file

@ -1,8 +1,4 @@
/**
* File: header.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 06.06.2018
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify

View file

@ -1,8 +1,4 @@
/**
* File: table-column.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 06.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -29,14 +25,19 @@ class TableColumn extends Component {
editButton: false,
deleteButton: false,
exportButton: false,
duplicateButton: false,
link: '/',
linkKey: '',
dataIndex: false,
inlineEditable: false,
inputType: 'text',
clickable: false,
labelKey: null,
checkbox: false,
checkboxKey: ''
checkboxKey: '',
labelStyle: null,
labelModifier: null
};
render() {

View file

@ -1,8 +1,4 @@
/**
* File: table.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -21,7 +17,7 @@
import React, { Component } from 'react';
import _ from 'lodash';
import { Table, Button, FormControl, Label, Checkbox } from 'react-bootstrap';
import { Table, Button, FormControl, FormLabel, FormCheck, Tooltip, OverlayTrigger } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import Icon from './icon';
@ -32,7 +28,7 @@ class CustomTable extends Component {
this.activeInput = null;
this.state = {
rows: this.getRows(props),
rows: CustomTable.getRows(props),
editCell: [ -1, -1 ]
};
}
@ -45,7 +41,7 @@ class CustomTable extends Component {
this.setState({ editCell: [ column, row ]}); // x, y
}
addCell(data, index, child) {
static addCell(data, index, child) {
// add data to cell
let content = null;
@ -67,14 +63,14 @@ class CustomTable extends Component {
let cell = [];
if (content != null) {
content = content.toString();
//content = content.toString();
// check if cell should be a link
const linkKey = child.props.linkKey;
if (linkKey && data[linkKey] != null) {
cell.push(<Link to={child.props.link + data[linkKey]}>{content}</Link>);
} else if (child.props.clickable) {
cell.push(<Button bsStyle="link" onClick={() => child.props.onClick(index)}>{content}</Button>);
cell.push(<Button variant="link" onClick={() => child.props.onClick(index)}>{content}</Button>);
} else {
cell.push(content);
}
@ -83,45 +79,63 @@ class CustomTable extends Component {
// add label to content
const labelKey = child.props.labelKey;
if (labelKey && data[labelKey] != null) {
var labelContent = data[labelKey];
let labelContent = data[labelKey];
if (child.props.labelModifier) {
labelContent = child.props.labelModifier(labelContent, data);
}
cell.push(<span>&nbsp;<Label bsClass={child.props.labelStyle(data[labelKey], data)}>{labelContent.toString()}</Label></span>);
let labelStyle = child.props.labelStyle(data[labelKey], data)
cell.push(<span>
&nbsp;
<FormLabel column={false} className={labelStyle}>
{labelContent}
</FormLabel>
</span>
);
}
if (child.props.dataIndex) {
cell.push(index);
}
// add buttons
if (child.props.editButton) {
cell.push(<Button bsClass='table-control-button' onClick={() => child.props.onEdit(index)} disabled={child.props.onEdit == null}><Icon icon='edit' /></Button>);
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(<Button bsClass='table-control-button' onClick={() => child.props.onDelete(index)} disabled={child.props.onDelete == null}><Icon icon='trash' /></Button>);
cell.push(<OverlayTrigger key={1} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"delete"}`}> Delete </Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onDelete(index)} disabled={child.props.onDelete == null}><Icon icon='trash' /></Button></OverlayTrigger>);
}
if (child.props.checkbox) {
const checkboxKey = this.props.checkboxKey;
const checkboxKey = child.props.checkboxKey;
cell.push(<Checkbox className="table-control-checkbox" inline checked={checkboxKey ? data[checkboxKey] : null} onChange={e => child.props.onChecked(index, e)} />);
cell.push(<FormCheck className="table-control-checkbox" inline checked={checkboxKey ? data[checkboxKey] : null} onChange={e => child.props.onChecked(index, e)} />);
}
if (child.props.exportButton) {
cell.push(<Button bsClass='table-control-button' onClick={() => child.props.onExport(index)} disabled={child.props.onExport == null}><Icon icon='download' /></Button>);
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>);
}
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>);
}
return cell;
}
} // addCell
componentWillReceiveProps(nextProps) {
const rows = this.getRows(nextProps);
static getDerivedStateFromProps(props, state){
const rows = CustomTable.getRows(props);
this.setState({ rows });
return { rows };
}
componentDidUpdate() {
@ -141,7 +155,7 @@ class CustomTable extends Component {
this.setState({ editCell: [ -1, -1 ] });
}
getRows(props) {
static getRows(props) {
if (props.data == null) {
return [];
}
@ -150,13 +164,13 @@ class CustomTable extends Component {
// check if multiple columns
if (Array.isArray(props.children) === false) {
// table only has a single column
return [ this.addCell(data, index, props.children) ];
return [ CustomTable.addCell(data, index, props.children) ];
}
const row = [];
for (let child of props.children) {
row.push(this.addCell(data, index, child));
row.push(CustomTable.addCell(data, index, child));
}
return row;
@ -200,7 +214,7 @@ class CustomTable extends Component {
return (<td key={cellIndex} tabIndex={tabIndex} onClick={ evtHdls.onCellClick } onFocus={ evtHdls.onCellFocus } onBlur={ evtHdls.onCellBlur }>
{(this.state.editCell[0] === cellIndex && this.state.editCell[1] === rowIndex ) ? (
<FormControl type="text" value={cell} onChange={(event) => children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} inputRef={ref => { this.activeInput = ref; }} />
<FormControl as='input' type={children[cellIndex].props.inputType} value={cell} onChange={(event) => children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} ref={ref => { this.activeInput = ref; }} />
) : (
<span>
{cell.map((element, elementIndex) => (

View file

@ -0,0 +1,69 @@
/**
* 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 ConfigsDataManager from './configs-data-manager';
class ConfigStore extends ArrayStore {
constructor() {
super('configs', ConfigsDataManager);
}
reduce(state, action) {
switch (action.type) {
case 'configs/loaded':
return super.reduce(state, action);
case 'configs/start-add':
// Check if this is a recursive component config import or not
if (action.data.hasOwnProperty("outputMapping") || action.data.hasOwnProperty("inputMapping")) {
// import
let subObjects = []
let outputMapping = {}
let inputMapping = {}
if (action.data.hasOwnProperty("outputMapping")){
outputMapping["signals"] = action.data.outputMapping
subObjects.push(outputMapping)
delete action.data.outputMapping; // remove outputMapping signals from config object
}
if (action.data.hasOwnProperty("inputMapping")){
inputMapping["signals"] = action.data.inputMapping
subObjects.push(inputMapping)
delete action.data.inputMapping; // remove inputMapping signals from config object
}
// action.data should now contain the config and no sub-objects
// sub-objects are treated in add method of RestDataManager
this.dataManager.add(action.data, action.token,action.param, subObjects);
return state
} else {
// no import
return super.reduce(state, action);
}
default:
return super.reduce(state, action);
}
}
}
export default new ConfigStore();

View file

@ -0,0 +1,60 @@
/**
* 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 AppDispatcher from '../common/app-dispatcher';
class ConfigDataManager extends RestDataManager {
constructor() {
super('config', '/configs');
this.onLoad = this.onConfigsLoad;
}
onConfigsLoad(data, token) {
if (!Array.isArray(data))
data = [ data ];
for (let config of data) {
// prepare IC data
AppDispatcher.dispatch({
type: 'icData/prepare',
inputLength: parseInt(config.inputLength, 10),
outputLength: parseInt(config.outputLength, 10),
id: config.icID
});
// request in signals
AppDispatcher.dispatch({
type: 'signals/start-load',
token: token,
param: '?direction=in&configID=' + config.id,
});
// request out signals
AppDispatcher.dispatch({
type: 'signals/start-load',
token: token,
param: '?direction=out&configID=' + config.id,
});
}
}
}
export default new ConfigDataManager();

View file

@ -0,0 +1,191 @@
/**
* 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 { Multiselect } from 'multiselect-react-dropdown'
import Dialog from '../common/dialogs/dialog';
import ParametersEditor from '../common/parameters-editor';
class EditConfigDialog extends React.Component {
valid = false;
constructor(props) {
super(props);
this.state = {
name: '',
icID: '',
startParameters: {},
selectedFiles: [] // list of selected files {name, id}, this is not the fileIDs list of the config!
};
}
onClose(canceled) {
if (canceled === false) {
if (this.valid) {
let data = this.props.config;
if (this.state.name !== '' && this.props.config.name !== this.state.name) {
data.name = this.state.name;
}
if (this.state.icID !== '' && this.props.config.icID !== parseInt(this.state.icID)) {
data.icID = parseInt(this.state.icID, 10);
}
if(this.state.startParameters !== {} &&
JSON.stringify(this.props.config.startParameters) !== JSON.stringify(this.state.startParameters)){
data.startParameters = this.state.startParameters;
}
let IDs = []
for(let e of this.state.selectedFiles){
IDs.push(e.id)
}
if(this.props.config.fileIDs !== null && this.props.config.fileIDs !== undefined) {
if (JSON.stringify(IDs) !== JSON.stringify(this.props.config.fileIDs)) {
data.fileIDs = IDs;
}
}
else{
data.fileIDs = IDs
}
//forward modified config to callback function
this.props.onClose(data)
}
} else {
this.props.onClose();
}
}
handleChange(e) {
this.setState({ [e.target.id]: e.target.value });
this.valid = this.isValid()
}
handleParameterChange(data) {
if (data) {
this.setState({startParameters: data});
}
this.valid = this.isValid()
}
onFileChange(selectedList, changedItem) {
this.setState({
selectedFiles: selectedList
})
this.valid = this.isValid()
}
isValid() {
// input is valid if at least one element has changed from its initial value
return this.state.name !== ''
|| this.state.icID !== ''
|| this.state.startParameters !== {}
}
resetState() {
// determine list of selected files incl id and filename
let selectedFiles = []
if(this.props.config.fileIDs !== null && this.props.config.fileIDs !== undefined) {
for (let selectedFileID of this.props.config.fileIDs) {
for (let file of this.props.files) {
if (file.id === selectedFileID) {
selectedFiles.push({name: file.name, id: file.id})
}
}
}
}
this.setState({
name: this.props.config.name,
icID: this.props.config.icID,
startParameters: this.props.config.startParameters,
selectedFiles: selectedFiles,
});
}
render() {
const ICOptions = this.props.ics.map(s =>
<option key={s.id} value={s.id}>{s.name}</option>
);
let configFileOptions = [];
for(let file of this.props.files) {
configFileOptions.push(
{name: file.name, id: file.id}
);
}
return (
<Dialog
show={this.props.show}
title="Edit Component Configuration"
buttonTitle="Save"
onClose={(c) => this.onClose(c)}
onReset={() => this.resetState()}
valid={this.valid}
>
<form>
<FormGroup controlId="name">
<FormLabel column={false}>Name</FormLabel>
<FormControl
type="text"
placeholder={this.props.config.name}
value={this.state.name}
onChange={(e) => this.handleChange(e)}
/>
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="icID">
<FormLabel column={false}> Infrastructure Component </FormLabel>
<FormControl
as="select"
placeholder='Select infrastructure component'
value={this.state.icID}
onChange={(e) => this.handleChange(e)}
>
{ICOptions}
</FormControl>
</FormGroup>
<Multiselect
options={configFileOptions}
showCheckbox={true}
selectedValues={this.state.selectedFiles}
onSelect={(selectedList, selectedItem) => this.onFileChange(selectedList, selectedItem)}
onRemove={(selectedList, removedItem) => this.onFileChange(selectedList, removedItem)}
displayValue={'name'}
placeholder={'Select file(s)...'}
/>
<FormGroup controlId='startParameters'>
<FormLabel> Start Parameters </FormLabel>
<ParametersEditor
content={this.state.startParameters}
onChange={(data) => this.handleParameterChange(data)}
/>
</FormGroup>
</form>
</Dialog>
);
}
}
export default EditConfigDialog;

View file

@ -0,0 +1,129 @@
/**
* 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 ImportConfigDialog extends React.Component {
imported = false;
valid = false;
constructor(props) {
super(props);
this.state = {
config: {},
name: '',
};
}
onClose(canceled){
if (canceled) {
this.props.onClose();
return;
}
this.props.onClose(this.state);
}
resetState = () => {
this.setState({
config: {},
name: ''
});
this.imported = false;
}
loadFile = event => {
// get file
const file = event.target.files[0];
if (file.type.match('application/json') === false) {
return;
}
// create file reader
let reader = new FileReader();
let self = this;
reader.onload = event => {
const config = JSON.parse(event.target.result);
self.imported = true;
self.valid = true;
this.setState({name: config.name, config: config });
};
reader.readAsText(file);
}
handleChange(e, index) {
this.setState({ [e.target.id]: e.target.value });
}
validateForm(target) {
// check all controls
let name = true;
if (this.state.name === '') {
name = false;
}
this.valid = name;
// return state to control
if (target === 'name'){
return name;
}
}
render() {
return (
<Dialog
show={this.props.show}
title="Import Component Configuration"
buttonTitle="Import"
onClose={(c) => this.onClose(c)}
onReset={() => this.resetState()}
valid={this.valid} >
<form>
<FormGroup controlId='file'>
<FormLabel>Component Configuration File</FormLabel>
<FormControl type='file' onChange={this.loadFile} />
</FormGroup>
<FormGroup controlId="name" >
<FormLabel>Name</FormLabel>
<FormControl
readOnly={!this.imported}
isValid={this.validateForm('name')}
type="text"
placeholder="Enter name"
value={this.state.name}
onChange={(e) => this.handleChange(e)}
/>
<FormControl.Feedback />
</FormGroup>
</form>
</Dialog>
);
}
}
export default ImportConfigDialog;

View file

@ -1,101 +0,0 @@
/**
* File: edit-project.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 07.03.2017
*
* 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, ControlLabel } from 'react-bootstrap';
import Dialog from './dialog';
class EditProjectDialog extends React.Component {
valid: true;
constructor(props) {
super(props);
this.state = {
name: '',
simulation: '',
_id: ''
}
}
onClose(canceled) {
if (canceled === false) {
if (this.valid) {
this.props.onClose(this.state);
}
} else {
this.props.onClose();
}
}
handleChange(e) {
this.setState({ [e.target.id]: e.target.value });
}
resetState() {
this.setState({
name: this.props.project.name,
simulation: this.props.project.simulation,
_id: this.props.project._id
});
}
validateForm(target) {
// check all controls
var name = true;
if (this.state.name === '') {
name = false;
}
this.valid = name;
// return state to control
if (target === 'name') return name ? "success" : "error";
return "success";
}
render() {
return (
<Dialog show={this.props.show} title="Edit Simulation" buttonTitle="Save" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<form>
<FormGroup controlId="name" validationState={this.validateForm('name')}>
<ControlLabel>Name</ControlLabel>
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="simulation">
<ControlLabel>Simulation</ControlLabel>
<FormControl componentClass="select" placeholder="Select simulation" value={this.state.simulation} onChange={(e) => this.handleChange(e)}>
{this.props.simulations.map(simulation => (
<option key={simulation._id} value={simulation._id}>{simulation.name}</option>
))}
</FormControl>
</FormGroup>
</form>
</Dialog>
);
}
}
export default EditProjectDialog;

View file

@ -1,103 +0,0 @@
/**
* File: new-simulation.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 04.03.2017
*
* 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, ControlLabel } from 'react-bootstrap';
import Dialog from './dialog';
import ParametersEditor from '../parameters-editor';
class EditSimulationDialog extends React.Component {
valid = true;
constructor(props) {
super(props);
this.state = {
name: '',
_id: '',
startParameters: {}
};
}
onClose = canceled => {
if (canceled) {
if (this.props.onClose != null) {
this.props.onClose();
}
return;
}
if (this.valid && this.props.onClose != null) {
this.props.onClose(this.state);
}
}
handleChange = event => {
this.setState({ [event.target.id]: event.target.value });
}
resetState = () => {
this.setState({
name: this.props.simulation.name,
_id: this.props.simulation._id,
startParameters: this.props.simulation.startParameters || {}
});
}
handleStartParametersChange = startParameters => {
this.setState({ startParameters });
}
validateForm(target) {
let name = true;
if (this.state.name === '') {
name = false;
}
this.valid = name;
// return state to control
if (target === 'name') return name ? 'success' : 'error';
}
render() {
return <Dialog show={this.props.show} title='Edit Simulation' buttonTitle='Save' onClose={this.onClose} onReset={this.resetState} valid={true}>
<form>
<FormGroup controlId='name' validationState={this.validateForm('name')}>
<ControlLabel>Name</ControlLabel>
<FormControl type='text' placeholder='Enter name' value={this.state.name} onChange={this.handleChange} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId='startParameters'>
<ControlLabel>Start Parameters</ControlLabel>
<ParametersEditor content={this.state.startParameters} onChange={this.handleStartParametersChange} />
</FormGroup>
</form>
</Dialog>;
}
}
export default EditSimulationDialog;

View file

@ -1,96 +0,0 @@
/**
* File: new-simulator.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* 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, ControlLabel } from 'react-bootstrap';
import _ from 'lodash';
import Dialog from './dialog';
import ParametersEditor from '../parameters-editor';
class EditSimulatorDialog extends React.Component {
valid = true;
constructor(props) {
super(props);
this.state = {
name: '',
endpoint: ''
};
}
onClose(canceled) {
if (canceled === false) {
if (this.valid) {
let data = {};
if (this.state.name != null && this.state.name !== "" && this.state.name !== _.get(this.props.simulator, 'rawProperties.name')) {
data.name = this.state.name;
}
if (this.state.endpoint != null && this.state.endpoint !== "" && this.state.endpoint !== "http://" && this.state.endpoint !== _.get(this.props.simulator, 'rawProperties.endpoint')) {
data.endpoint = this.state.endpoint;
}
this.props.onClose(data);
}
} else {
this.props.onClose();
}
}
handleChange(e) {
this.setState({ [e.target.id]: e.target.value });
}
resetState() {
this.setState({
name: _.get(this.props.simulator, 'properties.name') || _.get(this.props.simulator, 'rawProperties.name'),
endpoint: _.get(this.props.simulator, 'properties.endpoint') || _.get(this.props.simulator, 'rawProperties.endpoint')
});
}
render() {
return (
<Dialog show={this.props.show} title="Edit Simulator" buttonTitle="Save" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<form>
<FormGroup controlId="name">
<ControlLabel>Name</ControlLabel>
<FormControl type="text" placeholder={_.get(this.props.simulator, 'rawProperties.name')} value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="endpoint">
<ControlLabel>Endpoint</ControlLabel>
<FormControl type="text" placeholder={_.get(this.props.simulator, 'rawProperties.endpoint')} value={this.state.endpoint || 'http://' } onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId='properties'>
<ControlLabel>Properties</ControlLabel>
<ParametersEditor content={_.merge({}, this.props.simulator.rawProperties, this.props.simulator.properties)} disabled={true} />
</FormGroup>
</form>
</Dialog>
);
}
}
export default EditSimulatorDialog;

View file

@ -1,107 +0,0 @@
/**
* File: edit-user.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 02.05.2017
*
* 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, ControlLabel } from 'react-bootstrap';
import Dialog from './dialog';
class EditUserDialog extends React.Component {
valid: true;
constructor(props) {
super(props);
this.state = {
username: '',
mail: '',
role: '',
_id: ''
}
}
onClose(canceled) {
if (canceled === false) {
if (this.valid) {
this.props.onClose(this.state);
}
} else {
this.props.onClose();
}
}
handleChange(e) {
this.setState({ [e.target.id]: e.target.value });
}
resetState() {
this.setState({
username: this.props.user.username,
mail: this.props.user.mail,
role: this.props.user.role,
_id: this.props.user._id
});
}
validateForm(target) {
// check all controls
var username = true;
if (this.state.username === '') {
username = false;
}
this.valid = username;
// return state to control
if (target === 'username') return username ? "success" : "error";
return "success";
}
render() {
return (
<Dialog show={this.props.show} title="Edit user" buttonTitle="Save" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<form>
<FormGroup controlId="username" validationState={this.validateForm('username')}>
<ControlLabel>Username</ControlLabel>
<FormControl type="text" placeholder="Enter username" value={this.state.username} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="mail">
<ControlLabel>E-mail</ControlLabel>
<FormControl type="text" placeholder="Enter e-mail" value={this.state.mail} onChange={(e) => this.handleChange(e)} />
</FormGroup>
<FormGroup controlId="role" validationState={this.validateForm('role')}>
<ControlLabel>Role</ControlLabel>
<FormControl componentClass="select" placeholder="Select role" value={this.state.role} onChange={(e) => this.handleChange(e)}>
<option key='1' value='admin'>Administrator</option>
<option key='2' value='user'>User</option>
<option key='3' value='guest'>Guest</option>
</FormControl>
</FormGroup>
</form>
</Dialog>
);
}
}
export default EditUserDialog;

View file

@ -1,83 +0,0 @@
/**
* File: edit-widget-color-control.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 24.04.2017
* Copyright: 2018, Institute for Automation of Complex Power Systems, EONERC
*
* 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, Col, Row, Radio, ControlLabel } from 'react-bootstrap';
import classNames from 'classnames';
import { scaleOrdinal, schemeCategory20 } from 'd3-scale';
class EditWidgetColorControl extends Component {
static get ColorPalette() {
let colorCount = 0;
const colors = [];
const colorScale = scaleOrdinal(schemeCategory20);
while (colorCount < 20) { colors.push(colorScale(colorCount)); colorCount++; }
colors.unshift('#000', '#FFF'); // include black and white
return colors;
}
constructor(props) {
super(props);
this.state = {
widget: {}
};
}
componentWillReceiveProps(nextProps) {
// Update state's widget with props
this.setState({ widget: nextProps.widget });
}
render() {
return (
<FormGroup bsClass="color-control">
<Row>
<Col componentClass={ControlLabel} style={{whiteSpace: 'nowrap' }} sm={2}>
{ this.props.label }
</Col>
<Col sm={10} bsClass='colors-column'>
{
EditWidgetColorControl.ColorPalette.map( (color, idx ) => {
let colorStyle = {
background: color,
borderColor: color
};
let checkedClass = classNames({
'checked': idx === this.state.widget[this.props.controlId]
});
return (<Radio key={idx} name={this.props.controlId} style={colorStyle} className={checkedClass} value={idx} inline onChange={(e) => this.props.handleChange({target: { id: this.props.controlId, value: idx}})} />)
}
)
}
</Col>
</Row>
</FormGroup> )
}
}
export default EditWidgetColorControl;

View file

@ -1,133 +0,0 @@
/**
* File: edit-widget-color-zones-control.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 20.08.2017
*
* 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, ControlLabel, Button } from 'react-bootstrap';
import Icon from '../icon';
import Table from '../table';
import TableColumn from '../table-column';
class EditWidgetColorZonesControl extends React.Component {
constructor(props) {
super(props);
this.state = {
widget: {
zones: []
},
selectedZones: []
};
}
componentWillReceiveProps(nextProps) {
this.setState({ widget: nextProps.widget });
}
addZone = () => {
// add row
const widget = this.state.widget;
widget.zones.push({ strokeStyle: 'ffffff', min: 0, max: 100 });
this.setState({ widget });
this.sendEvent(widget);
}
removeZones = () => {
// remove zones
const widget = this.state.widget;
this.state.selectedZones.forEach(row => {
widget.zones.splice(row, 1);
});
this.setState({ selectedZones: [], widget });
this.sendEvent(widget);
}
changeCell = (event, row, column) => {
// change row
const widget = this.state.widget;
if (column === 1) {
widget.zones[row].strokeStyle = event.target.value;
} else if (column === 2) {
widget.zones[row].min = event.target.value;
} else if (column === 3) {
widget.zones[row].max = event.target.value;
}
this.setState({ widget });
this.sendEvent(widget);
}
sendEvent(widget) {
// create event
const event = {
target: {
id: 'zones',
value: widget.zones
}
};
this.props.handleChange(event);
}
checkedCell = (row, event) => {
// update selected rows
const selectedZones = this.state.selectedZones;
if (event.target.checked) {
if (selectedZones.indexOf(row) === -1) {
selectedZones.push(row);
}
} else {
let index = selectedZones.indexOf(row);
if (row > -1) {
selectedZones.splice(index, 1);
}
}
this.setState({ selectedZones });
}
render() {
return <FormGroup>
<ControlLabel>Color zones</ControlLabel>
<Table data={this.state.widget.zones}>
<TableColumn width="20" checkbox onChecked={this.checkedCell} />
<TableColumn title="Color" dataKey="strokeStyle" inlineEditable onInlineChange={this.changeCell} />
<TableColumn title="Minimum" dataKey="min" inlineEditable onInlineChange={this.changeCell} />
<TableColumn title="Maximum" dataKey="max" inlineEditable onInlineChange={this.changeCell} />
</Table>
<Button onClick={this.addZone} disabled={!this.props.widget.colorZones}><Icon icon="plus" /> Add</Button>
<Button onClick={this.removeZones} disabled={!this.props.widget.colorZones}><Icon icon="minus" /> Remove</Button>
</FormGroup>;
}
}
export default EditWidgetColorZonesControl;

View file

@ -1,202 +0,0 @@
/**
* File: edit-widget-control-creator.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 23.05.2017
*
* 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 EditWidgetTextControl from './edit-widget-text-control';
import EditWidgetNumberControl from './edit-widget-number-control';
import EditWidgetColorControl from './edit-widget-color-control';
import EditWidgetTimeControl from './edit-widget-time-control';
import EditImageWidgetControl from './edit-widget-image-control';
import EditWidgetSimulationControl from './edit-widget-simulation-control';
import EditWidgetSignalControl from './edit-widget-signal-control';
import EditWidgetSignalsControl from './edit-widget-signals-control';
import EditWidgetOrientation from './edit-widget-orientation';
import EditWidgetAspectControl from './edit-widget-aspect-control';
import EditWidgetTextSizeControl from './edit-widget-text-size-control';
import EditWidgetCheckboxControl from './edit-widget-checkbox-control';
import EditWidgetColorZonesControl from './edit-widget-color-zones-control';
import EditWidgetMinMaxControl from './edit-widget-min-max-control';
import EditWidgetHTMLContent from './edit-widget-html-content';
import EditWidgetParametersControl from './edit-widget-parameters-control';
export default function createControls(widgetType = null, widget = null, sessionToken = null, files = null, validateForm, simulationModels, handleChange) {
// Use a list to concatenate the controls according to the widget type
var dialogControls = [];
switch(widgetType) {
case 'CustomAction':
dialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} validate={id => validateForm(id)} handleChange={e => handleChange(e)} />,
<EditWidgetTextControl key={1} widget={widget} controlId={'icon'} label={'Icon'} placeholder={'Enter an awesome font icon name'} validate={id => validateForm(id)} handleChange={e => handleChange(e)} />,
<EditWidgetSimulationControl key={2} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetParametersControl key={3} widget={widget} controlId={'actions'} label={'Actions'} handleChange={(e) => handleChange(e)} />
)
break;
case 'Action':
dialogControls.push(
<EditWidgetSimulationControl key={0} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />
)
break;
case 'Value':
let valueBoundOnChange = (e) => {
handleChange([e, {target: {id: 'signal', value: 0}}]);
}
dialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} validate={id => validateForm(id)} handleChange={e => handleChange(e)} />,
<EditWidgetSimulationControl key={1} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => valueBoundOnChange(e)} />,
<EditWidgetSignalControl key={2} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetTextSizeControl key={3} widget={widget} handleChange={e => handleChange(e)} />,
<EditWidgetCheckboxControl key={4} widget={widget} controlId={'showUnit'} text="Show unit" handleChange={e => handleChange(e)} />
);
break;
case 'Lamp':
let lampBoundOnChange = (e) => {
handleChange([e, {target: {id: 'signal', value: 0}}]);
}
dialogControls.push(
<EditWidgetSimulationControl key={0} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => lampBoundOnChange(e)} />,
<EditWidgetSignalControl key={1} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetTextControl key={2} widget={widget} controlId={'threshold'} label={'Threshold'} placeholder={'0.5'} validate={id => validateForm(id)} handleChange={e => handleChange(e)} />,
<EditWidgetColorControl key={3} widget={widget} controlId={'on_color'} label={'Color On'} validate={(id) => validateForm(id)} handleChange={(e) => handleChange(e)} />,
<EditWidgetColorControl key={4} widget={widget} controlId={'off_color'} label={'Color Off'} validate={(id) => validateForm(id)} handleChange={(e) => handleChange(e)} />,
);
break;
case 'Plot':
let plotBoundOnChange = (e) => {
handleChange([e, {target: {id: 'signals', value: []}}]);
}
dialogControls.push(
<EditWidgetTimeControl key={0} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetSimulationControl key={1} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => plotBoundOnChange(e)} />,
<EditWidgetSignalsControl key={2} controlId={'signals'} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetTextControl key={3} controlId={'ylabel'} label={'Y-Axis name'} placeholder={'Enter a name for the y-axis'} widget={widget} handleChange={(e) => handleChange(e)} />,
<EditWidgetMinMaxControl key={4} widget={widget} controlId="y" handleChange={e => handleChange(e)} />
);
break;
case 'Table':
dialogControls.push(
<EditWidgetSimulationControl key={0} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetCheckboxControl key={1} widget={widget} controlId={'showUnit'} text="Show unit" handleChange={e => handleChange(e)} />
);
break;
case 'Image':
// Restrict to only image file types (MIME)
let imageControlFiles = files == null? [] : files.filter(file => file.type.includes('image'));
dialogControls.push(
<EditImageWidgetControl key={0} sessionToken={sessionToken} widget={widget} files={imageControlFiles} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetAspectControl key={1} widget={widget} handleChange={e => handleChange(e)} />
);
break;
case 'Gauge':
let gaugeBoundOnChange = (e) => {
handleChange([e, {target: {id: 'signal', value: ''}}]);
}
dialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} validate={id => validateForm(id)} handleChange={e => handleChange(e)} />,
<EditWidgetSimulationControl key={1} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => gaugeBoundOnChange(e) } />,
<EditWidgetSignalControl key={2} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetCheckboxControl key={3} widget={widget} controlId="colorZones" text="Show color zones" handleChange={e => handleChange(e)} />,
<EditWidgetColorZonesControl key={4} widget={widget} handleChange={e => handleChange(e)} />,
<EditWidgetMinMaxControl key={5} widget={widget} controlId="value" handleChange={e => handleChange(e)} />
);
break;
case 'PlotTable':
let plotTableBoundOnChange = (e) => {
handleChange([e, {target: {id: 'preselectedSignals', value: []}}]);
}
dialogControls.push(
<EditWidgetSimulationControl key={0} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => plotTableBoundOnChange(e)} />,
<EditWidgetSignalsControl key={1} controlId={'preselectedSignals'} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetTextControl key={2} controlId={'ylabel'} label={'Y-Axis'} placeholder={'Enter a name for the Y-axis'} widget={widget} handleChange={(e) => handleChange(e)} />,
<EditWidgetTimeControl key={3} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetMinMaxControl key={4} widget={widget} controlId="y" handleChange={e => handleChange(e)} />
);
break;
case 'Slider':
dialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} handleChange={e => handleChange(e)} validate={id => validateForm(id)} />,
<EditWidgetOrientation key={1} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetSimulationControl key={2} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetSignalControl key={3} widget={widget} input validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetCheckboxControl key={4} text={'Continous Update'} controlId={'continous_update'} widget={widget} input simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetCheckboxControl key={5} widget={widget} controlId={'showUnit'} text="Show unit" handleChange={e => handleChange(e)} />,
<EditWidgetMinMaxControl key={6} widget={widget} controlId={'range'} handleChange={e => handleChange(e)} />,
<EditWidgetNumberControl key={7} widget={widget} controlId={'step'} label={'Step Size'} defaultValue={0.1} handleChange={(e) => handleChange(e)} />,
<EditWidgetNumberControl key={8} widget={widget} controlId={'default_value'} label={'Default Value'} defaultValue={50} handleChange={(e) => handleChange(e)} />
);
break;
case 'Button':
let buttonBoundOnChange = (e) => {
handleChange([e, {target: {id: 'signal', value: 0}}]);
}
dialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} handleChange={e => handleChange(e)} validate={id => validateForm(id)} />,
<EditWidgetSimulationControl key={1} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => buttonBoundOnChange(e)} />,
<EditWidgetSignalControl key={2} widget={widget} controlId={'signal'} input validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />,
<EditWidgetCheckboxControl key={3} widget={widget} controlId={'toggle'} text="Toggle" handleChange={e => handleChange(e)} />,
<EditWidgetNumberControl key={4} widget={widget} controlId={'on_value'} label={'On Value'} defaultValue={1} handleChange={(e) => handleChange(e)} />,
<EditWidgetNumberControl key={5} widget={widget} controlId={'off_value'} label={'Off Value'} defaultValue={0} handleChange={(e) => handleChange(e)} />
);
break;
case 'Box':
dialogControls.push(
<EditWidgetColorControl key={0} widget={widget} controlId={'border_color'} label={'Border color'} validate={(id) => validateForm(id)} handleChange={(e) => handleChange(e)} />,
<EditWidgetColorControl key={1} widget={widget} controlId={'background_color'} label={'Background color'} handleChange={e => handleChange(e)} />
);
break;
case 'Label':
dialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} handleChange={e => handleChange(e)} validate={id => validateForm(id)} />,
<EditWidgetTextSizeControl key={1} widget={widget} handleChange={e => handleChange(e)} />,
<EditWidgetColorControl key={2} widget={widget} controlId={'fontColor'} label={'Text color'} handleChange={e => handleChange(e)} />
);
break;
case 'HTML':
dialogControls.push(
<EditWidgetHTMLContent key={0} widget={widget} placeholder='HTML Code' controlId='content' handleChange={e => handleChange(e)} />
);
break;
case 'Topology':
// Restrict to only xml files (MIME)
let topologyControlFiles = files == null? [] : files.filter( file => file.type.includes('xml'));
dialogControls.push(
<EditImageWidgetControl key={0} sessionToken={sessionToken} widget={widget} files={topologyControlFiles} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />
);
break;
case 'Input':
let inputBoundOnChange = (e) => {
handleChange([e, {target: {id: 'signal', value: 0}}]);
}
dialogControls.push(
<EditWidgetTextControl key={0} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} validate={id => validateForm(id)} handleChange={e => handleChange(e)} />,
<EditWidgetSimulationControl key={1} widget={widget} validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => inputBoundOnChange(e)} />,
<EditWidgetSignalControl key={2} widget={widget} controlId={'signal'} input validate={(id) => validateForm(id)} simulationModels={simulationModels} handleChange={(e) => handleChange(e)} />
);
break;
default:
console.log('Non-valid widget type: ' + widgetType);
}
return dialogControls;
}

View file

@ -1,52 +0,0 @@
/**
* File: edit-widget-html-content.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 03.09.2017
*
* 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, ControlLabel } from 'react-bootstrap';
class EditWidgetHTMLContent extends React.Component {
constructor(props) {
super(props);
this.state = {
widget: {}
};
}
handleKeyIgnore(event){
// This function prevents a keystroke from beeing handled by dialog.js
event.stopPropagation();
}
componentWillReceiveProps(nextProps) {
// Update state's widget with props
this.setState({ widget: nextProps.widget });
}
render() {
return <FormGroup controlId={this.props.controlId}>
<ControlLabel>HTML Content</ControlLabel>
<FormControl onKeyPress={this.handleKeyIgnore} componentClass="textarea" style={{ height: 200 }} placeholder={this.props.placeholder} value={this.state.widget[this.props.controlId] || ''} onChange={e => this.props.handleChange(e)} />
</FormGroup>;
}
}
export default EditWidgetHTMLContent;

View file

@ -1,101 +0,0 @@
/**
* File: edit-widget-image-control.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 04.03.2017
*
* 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, ControlLabel, Button, ProgressBar } from 'react-bootstrap';
import AppDispatcher from '../../app-dispatcher';
class EditImageWidgetControl extends React.Component {
constructor(props) {
super(props);
this.state = {
widget: {
file: ''
},
fileList: null,
progress: 0
};
}
componentWillReceiveProps(nextProps) {
this.setState({ widget: nextProps.widget });
}
startFileUpload = () => {
// get selected file
let formData = new FormData();
for (let key in this.state.fileList) {
if (this.state.fileList.hasOwnProperty(key) && this.state.fileList[key] instanceof File) {
formData.append(key, this.state.fileList[key]);
}
}
// upload files
AppDispatcher.dispatch({
type: 'files/start-upload',
data: formData,
token: this.props.sessionToken,
progressCallback: this.uploadProgress,
finishedCallback: this.clearProgress
});
}
uploadProgress = (e) => {
this.setState({ progress: Math.round(e.percent) });
}
clearProgress = () => {
this.setState({ progress: 0 });
}
render() {
return <div>
<FormGroup controlId="file">
<ControlLabel>Image</ControlLabel>
<FormControl componentClass="select" value={this.state.widget.file} onChange={(e) => this.props.handleChange(e)}>
{this.props.files.length === 0 ? (
<option disabled value style={{ display: 'none' }}>No images found, please upload one first.</option>
) : (
this.props.files.reduce((entries, file, index) => {
entries.push(<option key={++index} value={file._id}>{file.name}</option>);
return entries;
}, [
<option key={0} value=''>Please select one image</option>
])
)}
</FormControl>
</FormGroup>
<FormGroup controlId="upload">
<ControlLabel>Upload</ControlLabel>
<FormControl type="file" onChange={(e) => this.setState({ fileList: e.target.files }) } />
</FormGroup>
<ProgressBar striped active now={this.state.progress} label={`${this.state.progress}%`} />
<Button bsSize="small" onClick={this.startFileUpload}>Upload</Button>
</div>;
}
}
export default EditImageWidgetControl;

View file

@ -1,64 +0,0 @@
/**
* File: edit-widget-min-max-control.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 30.08.2017
*
* 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, ControlLabel, Checkbox, Table } from 'react-bootstrap';
class EditWidgetMinMaxControl extends React.Component {
constructor(props) {
super(props);
const widget = {};
widget[props.controlID + "UseMinMax"] = false;
widget[props.controlId + "Min"] = 0;
widget[props.controlId + "Max"] = 100;
this.state = {
widget
};
}
componentWillReceiveProps(nextProps) {
this.setState({ widget: nextProps.widget });
}
render() {
return <FormGroup>
<ControlLabel>{this.props.label}</ControlLabel>
<Checkbox id={this.props.controlId + "UseMinMax"} checked={this.state.widget[this.props.controlId + "UseMinMax"] || ''} onChange={e => this.props.handleChange(e)}>Enable min-max</Checkbox>
<Table>
<tbody>
<tr>
<td>
Min: <FormControl type="number" step="any" id={this.props.controlId + "Min"} disabled={!this.state.widget[this.props.controlId + "UseMinMax"]} placeholder="Minimum value" value={this.state.widget[this.props.controlId + 'Min']} onChange={e => this.props.handleChange(e)} />
</td>
<td>
Max: <FormControl type="number" step="any" id={this.props.controlId + "Max"} disabled={!this.state.widget[this.props.controlId + "UseMinMax"]} placeholder="Maximum value" value={this.state.widget[this.props.controlId + 'Max']} onChange={e => this.props.handleChange(e)} />
</td>
</tr>
</tbody>
</Table>
</FormGroup>;
}
}
export default EditWidgetMinMaxControl;

View file

@ -1,83 +0,0 @@
/**
* File: edit-widget-signals-control.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 03.04.2017
*
* 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, Checkbox, ControlLabel, FormControl } from 'react-bootstrap';
class EditWidgetSignalsControl extends Component {
constructor(props) {
super(props);
this.state = {
widget: {
simulator: {}
}
};
}
componentWillReceiveProps(nextProps) {
// Update state's widget with props
this.setState({ widget: nextProps.widget });
}
handleSignalChange(checked, index) {
var signals = this.state.widget[this.props.controlId];
var new_signals;
if (checked) {
// add signal
new_signals = signals.concat(index);
} else {
// remove signal
new_signals = signals.filter( (idx) => idx !== index );
}
this.props.handleChange({ target: { id: this.props.controlId, value: new_signals } });
}
render() {
const simulationModel = this.props.simulationModels.find(m => m._id === this.state.widget.simulationModel);
let signalsToRender = [];
if (simulationModel != null) {
// If simulation model update the signals to render
signalsToRender = simulationModel ? simulationModel.outputMapping : [];
}
return (
<FormGroup>
<ControlLabel>Signals</ControlLabel>
{
signalsToRender.length === 0 || !this.state.widget.hasOwnProperty(this.props.controlId)? (
<FormControl.Static>No signals available.</FormControl.Static>
) : (
signalsToRender.map((signal, index) => (
<Checkbox key={index} checked={this.state.widget[this.props.controlId].indexOf(index) !== -1} onChange={(e) => this.handleSignalChange(e.target.checked, index)}>{signal.name}</Checkbox>
))
)
}
</FormGroup>
);
}
}
export default EditWidgetSignalsControl;

View file

@ -1,60 +0,0 @@
/**
* File: edit-widget-simulation-control.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 03.04.2017
*
* 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, FormControl, ControlLabel } from 'react-bootstrap';
class EditWidgetSimulationControl extends Component {
constructor(props) {
super(props);
this.state = {
widget: {
simulationModel: ''
}
};
}
componentWillReceiveProps(nextProps) {
// Update state's widget with props
this.setState({ widget: nextProps.widget });
}
render() {
return (
<FormGroup controlId="simulationModel">
<ControlLabel>Simulation Model</ControlLabel>
<FormControl componentClass="select" placeholder="Select simulation model" value={this.state.widget.simulationModel || '' } onChange={(e) => this.props.handleChange(e)}>
{
this.props.simulationModels.length === 0 ? (
<option disabled value style={{ display: 'none' }}> No simulation models available. </option>
) : (
this.props.simulationModels.map((model, index) => (
<option key={index} value={model._id}>{model.name}</option>
)))
}
</FormControl>
</FormGroup>
);
}
}
export default EditWidgetSimulationControl;

View file

@ -1,146 +0,0 @@
/**
* File: edit-widget.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 08.03.2017
*
* 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, ControlLabel } from 'react-bootstrap';
import Dialog from './dialog';
import createControls from './edit-widget-control-creator';
class EditWidgetDialog extends React.Component {
valid = true;
constructor(props) {
super(props);
this.state = {
temporal: {
name: '',
simulationModel: '',
signal: 0
}
};
}
onClose(canceled) {
if (canceled === false) {
if (this.valid) {
this.props.onClose(this.state.temporal);
}
} else {
this.props.onClose();
}
}
assignAspectRatio(changeObject, fileId) {
// get aspect ratio of file
const file = this.props.files.find(element => element._id === fileId);
// scale width to match aspect
const aspectRatio = file.dimensions.width / file.dimensions.height;
changeObject.width = this.state.temporal.height * aspectRatio;
return changeObject;
}
handleChange(e) {
if (e.constructor === Array) {
// Every property in the array will be updated
let changes = e.reduce( (changesObject, event) => {
changesObject[event.target.id] = event.target.value;
return changesObject;
}, {});
this.setState({ temporal: Object.assign({}, this.state.temporal, changes ) });
} else {
let changeObject = {};
if (e.target.id === 'lockAspect') {
changeObject[e.target.id] = e.target.checked;
// correct image aspect if turned on
if (e.target.checked) {
changeObject = this.assignAspectRatio(changeObject, this.state.temporal.file);
}
} else if (e.target.id === 'file') {
changeObject[e.target.id] = e.target.value;
// get file and update size (if it's an image)
if ('lockAspect' in this.state.temporal && this.state.temporal.lockAspect) {
changeObject = this.assignAspectRatio(changeObject, e.target.value);
}
} else if (e.target.type === 'checkbox') {
changeObject[e.target.id] = e.target.checked;
} else if (e.target.type === 'number') {
changeObject[e.target.id] = Number(e.target.value);
} else {
changeObject[e.target.id] = e.target.value;
}
this.setState({ temporal: Object.assign({}, this.state.temporal, changeObject ) });
}
}
resetState() {
var widget_data = Object.assign({}, this.props.widget);
this.setState({ temporal: widget_data });
}
validateForm(target) {
// check all controls
var name = true;
if (this.state.name === '') {
name = false;
}
//this.valid = name;
this.valid = true;
// return state to control
if (target === 'name') return name ? "success" : "error";
}
render() {
let controls = null;
if (this.props.widget) {
controls = createControls(
this.props.widget.type,
this.state.temporal,
this.props.sessionToken,
this.props.files,
(id) => this.validateForm(id),
this.props.simulationModels,
(e) => this.handleChange(e));
}
return (
<Dialog show={this.props.show} title="Edit Widget" buttonTitle="Save" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<form encType='multipart/form-data'>
{ controls || '' }
</form>
</Dialog>
);
}
}
export default EditWidgetDialog;

View file

@ -1,112 +0,0 @@
/**
* File: import-simulation-model.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 03.09.2017
*
* 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, ControlLabel } from 'react-bootstrap';
import _ from 'lodash';
import Dialog from './dialog';
class ImportSimulationModelDialog extends React.Component {
imported = false;
constructor(props) {
super(props);
this.state = {
model: {}
};
}
onClose = canceled => {
if (canceled) {
this.props.onClose();
return;
}
this.props.onClose(this.state.model);
}
resetState = () => {
this.setState({
model: {}
});
this.imported = false;
}
loadFile = event => {
// get file
const file = event.target.files[0];
if (file.type.match('application/json') === false) {
return;
}
// create file reader
const reader = new FileReader();
const self = this;
reader.onload = event => {
const model = JSON.parse(event.target.result);
model.simulator = this.props.simulators.length > 0 ? this.props.simulators[0]._id : null;
self.imported = true;
this.setState({ model });
};
reader.readAsText(file);
}
handleSimulatorChange = event => {
const model = this.state.model;
model.simulator = event.target.value;
this.setState({ model });
}
render() {
return (
<Dialog show={this.props.show} title="Import Simulation Model" buttonTitle="Import" onClose={this.onClose} onReset={this.resetState} valid={this.imported}>
<form>
<FormGroup controlId='file'>
<ControlLabel>Simulation Model File</ControlLabel>
<FormControl type='file' onChange={this.loadFile} />
</FormGroup>
<FormGroup controlId='simulator'>
<ControlLabel>Simulator</ControlLabel>
<FormControl disabled={this.imported === false} componentClass='select' placeholder='Select simulator' value={this.state.model.simulator} onChange={this.handleSimulatorChange}>
{this.props.simulators.map(simulator => (
<option key={simulator._id} value={simulator._id}>{_.get(simulator, 'properties.name') || _.get(simulator, 'rawProperties.name')}</option>
))}
</FormControl>
</FormGroup>
</form>
</Dialog>
);
}
}
export default ImportSimulationModelDialog;

View file

@ -1,155 +0,0 @@
/**
* File: import-simulation.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 03.09.2017
*
* 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, ControlLabel } from 'react-bootstrap';
import Dialog from './dialog';
import ParametersEditor from '../parameters-editor';
class ImportSimulationDialog extends React.Component {
valid = false;
imported = false;
constructor(props) {
super(props);
this.state = {
name: '',
models: [],
startParameters: {}
};
}
onClose = canceled => {
if (canceled) {
if (this.props.onClose != null) {
this.props.onClose();
}
return;
}
if (this.valid && this.props.onClose != null) {
this.props.onClose(this.state);
}
}
handleChange(e, index) {
if (e.target.id === 'simulator') {
const models = this.state.models;
models[index].simulator = JSON.parse(e.target.value);
this.setState({ models });
return;
}
this.setState({ [e.target.id]: e.target.value });
}
resetState = () => {
this.setState({ name: '', models: [], startParameters: {} });
this.imported = false;
}
loadFile = event => {
const file = event.target.files[0];
if (!file.type.match('application/json')) {
return;
}
// create file reader
const reader = new FileReader();
const self = this;
reader.onload = onloadEvent => {
const simulation = JSON.parse(onloadEvent.target.result);
// simulation.models.forEach(model => {
// model.simulator = {
// node: self.props.nodes[0]._id,
// simulator: 0
// };
// });
self.imported = true;
self.valid = true;
self.setState({ name: simulation.name, models: simulation.models, startParameters: simulation.startParameters });
};
reader.readAsText(file);
}
validateForm(target) {
// check all controls
let name = true;
if (this.state.name === '') {
name = false;
}
this.valid = name;
// return state to control
if (target === 'name') return name ? "success" : "error";
}
render() {
return <Dialog show={this.props.show} title="Import Simulation" buttonTitle="Import" onClose={this.onClose} onReset={this.resetState} valid={this.valid}>
<form>
<FormGroup controlId="file">
<ControlLabel>Simulation File</ControlLabel>
<FormControl type="file" onChange={this.loadFile} />
</FormGroup>
<FormGroup controlId="name" validationState={this.validateForm('name')}>
<ControlLabel>Name</ControlLabel>
<FormControl readOnly={this.imported === false} type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup>
<ControlLabel>Start Parameters</ControlLabel>
<ParametersEditor content={this.state.startParameters} onChange={this.handleStartParametersChange} disabled={this.imported === false} />
</FormGroup>
{/* {this.state.models.map((model, index) => (
<FormGroup controlId="simulator" key={index}>
<ControlLabel>{model.name} - Simulator</ControlLabel>
<FormControl componentClass="select" placeholder="Select simulator" value={JSON.stringify({ node: model.simulator.node, simulator: model.simulator.simulator})} onChange={(e) => this.handleChange(e, index)}>
{this.props.nodes.map(node => (
node.simulators.map((simulator, index) => (
<option key={node._id + index} value={JSON.stringify({ node: node._id, simulator: index })}>{node.name}/{simulator.name}</option>
))
))}
</FormControl>
</FormGroup>
))} */}
</form>
</Dialog>;
}
}
export default ImportSimulationDialog;

View file

@ -1,103 +0,0 @@
/**
* File: new-project.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 07.03.2017
*
* 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, ControlLabel } from 'react-bootstrap';
import Dialog from './dialog';
class NewProjectDialog extends React.Component {
valid: false;
constructor(props) {
super(props);
this.state = {
name: '',
simulation: ''
};
}
onClose(canceled) {
if (canceled === false) {
if (this.valid) {
this.props.onClose(this.state);
}
} else {
this.props.onClose();
}
}
handleChange(e) {
this.setState({ [e.target.id]: e.target.value });
}
resetState() {
this.setState({
name: '',
simulation: this.props.simulations[0] != null ? this.props.simulations[0]._id : ''
});
}
validateForm(target) {
// check all controls
var name = true;
var simulation = true;
if (this.state.name === '') {
name = false;
}
if (this.state.simulation === '') {
simulation = false;
}
this.valid = name && simulation;
// return state to control
if (target === 'name') return name ? "success" : "error";
else if (target === 'simulation') return simulation ? "success" : "error";
}
render() {
return (
<Dialog show={this.props.show} title="New Project" buttonTitle="Add" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<form>
<FormGroup controlId="name" validationState={this.validateForm('name')}>
<ControlLabel>Name</ControlLabel>
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="simulation" validationState={this.validateForm('simulation')}>
<ControlLabel>Simulation</ControlLabel>
<FormControl componentClass="select" placeholder="Select simulation" value={this.state.simulation} onChange={(e) => this.handleChange(e)}>
{this.props.simulations.map(simulation => (
<option key={simulation._id} value={simulation._id}>{simulation.name}</option>
))}
</FormControl>
</FormGroup>
</form>
</Dialog>
);
}
}
export default NewProjectDialog;

View file

@ -1,99 +0,0 @@
/**
* File: new-simulation.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 04.03.2017
*
* 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, ControlLabel } from 'react-bootstrap';
import Dialog from './dialog';
import ParametersEditor from '../parameters-editor';
class NewSimulationDialog extends React.Component {
valid = false;
constructor(props) {
super(props);
this.state = {
name: '',
startParameters: {}
};
}
onClose = canceled => {
if (canceled) {
if (this.props.onClose != null) {
this.props.onClose();
}
return;
}
if (this.valid && this.props.onClose != null) {
this.props.onClose(this.state);
}
}
handleChange = event => {
this.setState({ [event.target.id]: event.target.value });
}
resetState = () => {
this.setState({ name: '', startParameters: {} });
}
handleStartParametersChange = startParameters => {
this.setState({ startParameters });
}
validateForm(target) {
// check all controls
let name = true;
if (this.state.name === '') {
name = false;
}
this.valid = name;
// return state to control
if (target === 'name') return name ? "success" : "error";
}
render() {
return <Dialog show={this.props.show} title="New Simulation" buttonTitle="Add" onClose={this.onClose} onReset={this.resetState} valid={this.valid}>
<form>
<FormGroup controlId="name" validationState={this.validateForm('name')}>
<ControlLabel>Name</ControlLabel>
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={this.handleChange} />
<FormControl.Feedback />
</FormGroup>
<FormGroup>
<ControlLabel>Start Parameters</ControlLabel>
<ParametersEditor content={this.state.startParameters} onChange={this.handleStartParametersChange} />
</FormGroup>
</form>
</Dialog>;
}
}
export default NewSimulationDialog;

View file

@ -1,122 +0,0 @@
/**
* File: new-simulator.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* 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, ControlLabel } from 'react-bootstrap';
import Dialog from './dialog';
class NewSimulatorDialog extends React.Component {
valid = false;
constructor(props) {
super(props);
this.state = {
name: '',
endpoint: '',
uuid: ''
};
}
onClose(canceled) {
if (canceled === false) {
if (this.valid) {
const data = {
properties: {
name: this.state.name
},
uuid: this.state.uuid
};
if (this.state.endpoint != null && this.state.endpoint !== "" && this.state.endpoint !== 'http://') {
data.properties.endpoint = this.state.endpoint;
}
this.props.onClose(data);
}
} else {
this.props.onClose();
}
}
handleChange(e) {
this.setState({ [e.target.id]: e.target.value });
}
resetState() {
this.setState({ name: '', endpoint: 'http://', uuid: this.uuidv4()});
}
validateForm(target) {
// check all controls
let name = true;
let uuid = true;
if (this.state.name === '') {
name = false;
}
if (this.state.uuid === '') {
uuid = false;
}
this.valid = name && uuid;
// return state to control
if (target === 'name') return name ? "success" : "error";
if (target === 'uuid') return uuid ? "success" : "error";
}
uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
// eslint-disable-next-line
var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
render() {
return (
<Dialog show={this.props.show} title="New Simulator" buttonTitle="Add" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<form>
<FormGroup controlId="name" validationState={this.validateForm('name')}>
<ControlLabel>Name</ControlLabel>
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="endpoint">
<ControlLabel>Endpoint</ControlLabel>
<FormControl type="text" placeholder="Enter endpoint" value={this.state.endpoint} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="uuid" validationState={this.validateForm('uuid')}>
<ControlLabel>UUID</ControlLabel>
<FormControl type="text" placeholder="Enter uuid" value={this.state.uuid} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
</form>
</Dialog>
);
}
}
export default NewSimulatorDialog;

View file

@ -1,92 +0,0 @@
/**
* File: home.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* 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 { Link } from 'react-router-dom';
import RestAPI from '../api/rest-api';
import config from '../config';
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
getCounts(type) {
if (this.state.hasOwnProperty('counts'))
return this.state.counts[type];
else
return '?';
}
componentWillMount() {
RestAPI.get('/api/v1/counts').then(response => {
this.setState({ counts: response });
});
}
render() {
return (
<div className="home-container">
<img style={{height: 120, float: 'right'}} src={require('../img/villas_web.svg')} alt="Logo VILLASweb" />
<h1>Home</h1>
<p>
Welcome to <b>{config.instance}</b>!<br />
VILLASweb is a frontend for distributed real-time simulation hosted by <a href={"mailto:" + config.admin.mail}>{config.admin.name}</a>.
</p>
<p>
This instance is hosting <Link to="/projects" title="Projects">{this.getCounts('projects')} projects</Link> consisting of <Link to="/simulators" title="Simulators">{this.getCounts('simulators')} simulators</Link>, {this.getCounts('visualizations')} visualizations and <Link to="/simulations" title="Simulations">{this.getCounts('simulations')} simulations</Link>.
A total of <Link to="/users" title="Users">{this.getCounts('users')} users</Link> are registered.<br />
</p>
<h3>Credits</h3>
<p>VILLASweb is developed by the <a href="http://acs.eonerc.rwth-aachen.de">Institute for Automation of Complex Power Systems</a> at the <a href="https;//www.rwth-aachen.de">RWTH Aachen University</a>.</p>
<ul>
<li><a href="mailto:mgrigull@eonerc.rwth-aachen.de">Markus Grigull</a></li>
<li><a href="mailto:stvogel@eonerc.rwth-aachen.de">Steffen Vogel</a></li>
<li><a href="mailto:mstevic@eonerc.rwth-aachen.de">Marija Stevic</a></li>
</ul>
<h3>Links</h3>
<ul>
<li><a href="http://fein-aachen.org/projects/villas-framework/">Project Page</a></li>
<li><a href="https://villas.fein-aachen.org/doc/web.html">Documentation</a></li>
<li><a href="https://git.rwth-aachen.de/VILLASframework/VILLASweb">Source Code</a></li>
</ul>
<h3>Funding</h3>
<p>The development of <a href="http://fein-aachen.org/projects/villas-framework/">VILLASframework</a> projects have received funding from</p>
<ul>
<li><a href="http://www.re-serve.eu">RESERVE</a> a European Unions Horizon 2020 research and innovation programme under grant agreement No 727481</li>
<li><a href="http://www.jara.org/en/research/energy">JARA-ENERGY</a>. Jülich-Aachen Research Alliance (JARA) is an initiative of RWTH Aachen University and Forschungszentrum Jülich.</li>
</ul>
<img height={100} src={require('../img/european_commission.svg')} alt="Logo EU" />
<img height={70} src={require('../img/reserve.svg')} alt="Logo EU" />
<img height={60} src={require('../img/eonerc_rwth.svg')} alt="Logo ACS" />
{
//<img height={70} src={require('../img/jara.svg')} alt="Logo JARA" />
}
</div>
);
}
}
export default Home;

View file

@ -1,131 +0,0 @@
/**
* File: signalMapping.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 10.08.2018
*
* 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 PropTypes from 'prop-types';
import { FormGroup, FormControl, ControlLabel, HelpBlock } from 'react-bootstrap';
import validator from 'validator';
import Table from './table';
import TableColumn from './table-column';
class SignalMapping extends React.Component {
constructor(props) {
super(props);
var length = props.length;
if (length === undefined)
length = 1;
this.state = {
length: length,
signals: props.signals
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.length === this.state.length && nextProps.signals === this.state.signals) {
return;
}
this.setState({ length: nextProps.length, signals: nextProps.signals });
}
validateLength(){
const valid = validator.isInt(this.state.length + '', { min: 1, max: 100 });
return valid ? 'success' : 'error';
}
handleLengthChange = event => {
const length = event.target.value;
// update signals to represent length
const signals = this.state.signals;
if (this.state.length < length) {
while (signals.length < length) {
signals.push({ name: 'Signal', type: 'Type' });
}
} else {
signals.splice(length, signals.length - length);
}
// save updated state
this.setState({ length, signals });
if (this.props.onChange != null) {
this.props.onChange(length, signals);
}
}
handleMappingChange = (event, row, column) => {
const signals = this.state.signals;
const length = this.state.length;
if (column === 1) {
signals[row].name = event.target.value;
} else if (column === 2) {
signals[row].type = event.target.value;
}
this.setState({ length, signals });
if (this.props.onChange != null) {
this.props.onChange(this.state.length, signals);
}
}
render() {
return <div>
<FormGroup validationState={this.validateLength()}>
<ControlLabel>{this.props.name} Length</ControlLabel>
<FormControl name='length' type='number' placeholder='Enter length' defaultValue={this.state.length} min='1' onBlur={this.handleLengthChange} />
<FormControl.Feedback />
</FormGroup>
<FormGroup>
<ControlLabel>{this.props.name} Mapping</ControlLabel>
<HelpBlock>Click <i>name</i> or <i>type</i> cell to edit</HelpBlock>
<Table data={this.props.signals}>
<TableColumn title='ID' width='60' dataIndex />
<TableColumn title='Name' dataKey='name' inlineEditable onInlineChange={this.handleMappingChange} />
<TableColumn title='Type' dataKey='type' inlineEditable onInlineChange={this.handleMappingChange} />
</Table>
</FormGroup>
</div>;
}
}
SignalMapping.propTypes = {
name: PropTypes.string,
length: PropTypes.number,
signals: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired
})
),
onChange: PropTypes.func
};
export default SignalMapping;

View file

@ -1,68 +0,0 @@
/**
* File: simulator-actionm.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 12.04.2018
*
* 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, DropdownButton, MenuItem } from 'react-bootstrap';
class SimulatorAction extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedAction: null
};
}
componentWillReceiveProps(nextProps) {
if (this.state.selectedAction == null) {
if (nextProps.actions != null && nextProps.actions.length > 0) {
this.setState({ selectedAction: nextProps.actions[0] });
}
}
}
setAction = id => {
// search action
for (let action of this.props.actions) {
if (action.id === id) {
this.setState({ selectedAction: action });
}
}
}
render() {
const actionList = this.props.actions.map(action => (
<MenuItem key={action.id} eventKey={action.id} active={this.state.selectedAction === action.id}>
{action.title}
</MenuItem>
));
return <div>
<DropdownButton title={this.state.selectedAction != null ? this.state.selectedAction.title : ''} id="action-dropdown" onSelect={this.setAction}>
{actionList}
</DropdownButton>
<Button style={{ marginLeft: '5px' }} disabled={this.props.runDisabled} onClick={() => this.props.runAction(this.state.selectedAction)}>Run</Button>
</div>;
}
}
export default SimulatorAction;

View file

@ -1,43 +0,0 @@
/**
* File: plot-legend.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 10.04.2017
* Copyright: 2018, Institute for Automation of Complex Power Systems, EONERC
*
* 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 { scaleOrdinal, schemeCategory10 } from 'd3-scale';
class PlotLegend extends React.Component {
render() {
const colorScale = scaleOrdinal(schemeCategory10);
return <div className="plot-legend">
<ul>
{this.props.signals.map(signal =>
<li key={signal.index} className="signal-legend" style={{ color: colorScale(signal.index) }}>
<span className="signal-legend-name">{signal.name}</span>
<span style={{ marginLeft: '0.3em' }} className="signal-unit">{signal.type}</span>
</li>
)}
</ul>
</div>;
}
}
export default PlotLegend;

View file

@ -1,214 +0,0 @@
/**
* File: gauge.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 31.03.2017
* Copyright: 2018, Institute for Automation of Complex Power Systems, EONERC
*
* 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 { Gauge } from 'gaugeJS';
class WidgetGauge extends Component {
constructor(props) {
super(props);
this.gaugeCanvas = null;
this.gauge = null;
this.state = {
value: 0,
minValue: null,
maxValue: null
};
}
componentDidMount() {
this.gauge = new Gauge(this.gaugeCanvas).setOptions(this.computeGaugeOptions(this.props.widget));
//this.gauge.maxValue = this.state.maxValue;
//this.gauge.setMinValue(this.state.minValue);
this.gauge.animationSpeed = 30;
//this.gauge.set(this.state.value);
//this.updateLabels(this.state.minValue, this.state.maxValue);
}
componentWillReceiveProps(nextProps) {
if (nextProps.simulationModel == null) {
this.setState({ value: 0 });
return;
}
const simulator = nextProps.simulationModel.simulator;
// update value
if (nextProps.data == null || nextProps.data[simulator] == null
|| nextProps.data[simulator].output == null
|| nextProps.data[simulator].output.values == null
|| nextProps.data[simulator].output.values.length === 0
|| nextProps.data[simulator].output.values[0].length === 0) {
this.setState({ value: 0 });
return;
}
// check if value has changed
const signal = nextProps.data[simulator].output.values[nextProps.widget.signal];
// Take just 3 decimal positions
// Note: Favor this method over Number.toFixed(n) in order to avoid a type conversion, since it returns a String
if (signal != null) {
const value = Math.round(signal[signal.length - 1].y * 1e3) / 1e3;
if (this.state.value !== value && value != null) {
this.setState({ value });
// update min-max if needed
let updateLabels = false;
let minValue = this.state.minValue;
let maxValue = this.state.maxValue;
if (minValue == null) {
minValue = value - 0.5;
updateLabels = true;
this.setState({ minValue });
this.gauge.setMinValue(minValue);
}
if (maxValue == null) {
maxValue = value + 0.5;
updateLabels = true;
this.setState({ maxValue });
this.gauge.maxValue = maxValue;
}
if (nextProps.widget.valueUseMinMax) {
if (this.state.minValue > nextProps.widget.valueMin) {
minValue = nextProps.widget.valueMin;
this.setState({ minValue });
this.gauge.setMinValue(minValue);
updateLabels = true;
}
if (this.state.maxValue < nextProps.widget.valueMax) {
maxValue = nextProps.widget.valueMax;
this.setState({ maxValue });
this.gauge.maxValue = maxValue;
updateLabels = true;
}
}
if (updateLabels === false) {
// check if min/max changed
if (minValue > this.gauge.minValue) {
minValue = this.gauge.minValue;
updateLabels = true;
this.setState({ minValue });
}
if (maxValue < this.gauge.maxValue) {
maxValue = this.gauge.maxValue;
updateLabels = true;
this.setState({ maxValue });
}
}
if (updateLabels) {
this.updateLabels(minValue, maxValue);
}
// update gauge's value
this.gauge.set(value);
}
}
}
updateLabels(minValue, maxValue, force) {
// calculate labels
const labels = [];
const labelCount = 5;
const labelStep = (maxValue - minValue) / (labelCount - 1);
for (let i = 0; i < labelCount; i++) {
labels.push(minValue + labelStep * i);
}
// calculate zones
let zones = this.props.widget.colorZones ? this.props.widget.zones : null;
if (zones != null) {
// adapt range 0-100 to actual min-max
const step = (maxValue - minValue) / 100;
zones = zones.map(zone => {
return Object.assign({}, zone, { min: (zone.min * step) + +minValue, max: zone.max * step + +minValue, strokeStyle: '#' + zone.strokeStyle });
});
}
this.gauge.setOptions({
staticLabels: {
font: '10px "Helvetica Neue"',
labels,
color: "#000000",
fractionDigits: 1
},
staticZones: zones
});
}
computeGaugeOptions(widget) {
return {
angle: -0.25,
lineWidth: 0.2,
pointer: {
length: 0.6,
strokeWidth: 0.035
},
radiusScale: 0.8,
colorStart: '#6EA2B0',
colorStop: '#6EA2B0',
strokeColor: '#E0E0E0',
highDpiSupport: true,
limitMax: false,
limitMin: false
};
}
render() {
const componentClass = this.props.editing ? "gauge-widget editing" : "gauge-widget";
let signalType = null;
if (this.props.simulationModel != null) {
signalType = (this.props.simulationModel != null && this.props.simulationModel.outputLength > 0 && this.props.widget.signal < this.props.simulationModel.outputLength) ? this.props.simulationModel.outputMapping[this.props.widget.signal].type : '';
}
return (
<div className={componentClass}>
<div className="gauge-name">{this.props.widget.name}</div>
<canvas ref={node => this.gaugeCanvas = node} />
<div className="gauge-unit">{signalType}</div>
<div className="gauge-value">{this.state.value}</div>
</div>
);
}
}
export default WidgetGauge;

View file

@ -1,55 +0,0 @@
/**
* File: image.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 14.03.2017
*
* 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 AppDispatcher from '../../app-dispatcher';
import config from '../../config';
class WidgetImage extends React.Component {
componentWillReceiveProps(nextProps) {
// Query the image referenced by the widget
let widgetFile = nextProps.widget.file;
if (widgetFile && !nextProps.files.find(file => file._id === widgetFile)) {
AppDispatcher.dispatch({
type: 'files/start-load',
data: widgetFile,
token: nextProps.token
});
}
}
render() {
const file = this.props.files.find(file => file._id === this.props.widget.file);
return (
<div className="full">
{file ? (
<img className="full" alt={file.name} src={'/' + config.publicPathBase + file.path} onDragStart={e => e.preventDefault()} />
) : (
<img className="full" alt="questionmark" src={'/' + config.publicPathBase + 'missing-image.png'} onDragStart={e => e.preventDefault()} />
)}
</div>
);
}
}
export default WidgetImage;

View file

@ -1,94 +0,0 @@
/**
* File: input.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 29.03.2017
* Copyright: 2018, Institute for Automation of Complex Power Systems, EONERC
*
* 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 { Form, FormGroup, Col, ControlLabel, FormControl, InputGroup } from 'react-bootstrap';
class WidgetInput extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
unit: ''
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.simulationModel == null) {
return;
}
// Update value
if (nextProps.widget.default_value && this.state.value === undefined) {
this.setState({
value: nextProps.widget.default_value
});
}
// Update unit
if (nextProps.widget.simulationModel && nextProps.simulationModel.inputMapping && this.state.unit !== nextProps.simulationModel.inputMapping[nextProps.widget.signal].type) {
this.setState({
unit: nextProps.simulationModel.inputMapping[nextProps.widget.signal].type
});
}
}
valueIsChanging(newValue) {
this.setState({ value: newValue });
}
valueChanged(newValue) {
if (this.props.onInputChanged) {
this.props.onInputChanged(newValue);
}
}
handleKeyPress(e) {
if(e.charCode === 13) {
this.valueChanged(this.state.value)
}
}
render() {
return (
<div className="number-input-widget full">
<Form componentClass="fieldset" horizontal>
<FormGroup>
<Col componentClass={ControlLabel} xs={3}>
{this.props.widget.name}
</Col>
<Col xs={9}>
<InputGroup>
<FormControl type="number" step="any" disabled={ this.props.editing } onKeyPress={ (e) => this.handleKeyPress(e) } onBlur={ (e) => this.valueChanged(this.state.value) } onChange={ (e) => this.valueIsChanging(e.target.value) } placeholder="Enter value" value={ this.state.value } />
<InputGroup.Addon>{this.state.unit}</InputGroup.Addon>
</InputGroup>
</Col>
</FormGroup>
</Form>
</div>
);
}
}
export default WidgetInput;

View file

@ -1,78 +0,0 @@
/**
* File: lamp.js
* Author: Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* Date: 20.09.2017
*
* 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 EditWidgetColorControl from '../dialogs/edit-widget-color-control';
class WidgetLamp extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
threshold: 0
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.simulationModel == null) {
this.setState({ value: '' });
return;
}
const simulator = nextProps.simulationModel.simulator;
// update value
if (nextProps.data == null || nextProps.data[simulator] == null || nextProps.data[simulator].output == null || nextProps.data[simulator].output.values == null) {
this.setState({ value: '' });
return;
}
// check if value has changed
const signal = nextProps.data[simulator].output.values[nextProps.widget.signal];
if (signal != null && this.state.value !== signal[signal.length - 1].y) {
this.setState({ value: signal[signal.length - 1].y });
}
}
render() {
let colors = EditWidgetColorControl.ColorPalette;
let color;
if (Number(this.state.value) > Number(this.props.widget.threshold))
color = colors[this.props.widget.on_color];
else
color = colors[this.props.widget.off_color];
let style = {
backgroundColor: color,
width: this.props.widget.width,
height: this.props.widget.height
}
return (
<div className="lamp-widget" style={style} />
);
}
}
export default WidgetLamp;

View file

@ -1,171 +0,0 @@
/**
* File: plot-table.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 15.03.2017
*
* 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 classNames from 'classnames';
import { FormGroup, Checkbox } from 'react-bootstrap';
import Plot from '../widget-plot/plot';
import PlotLegend from '../widget-plot/plot-legend';
class WidgetPlotTable extends Component {
constructor(props) {
super(props);
this.state = {
preselectedSignals: [],
signals: []
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.simulationModel == null) {
return;
}
// Update internal selected signals state with props (different array objects)
if (this.props.widget.signals !== nextProps.widget.signals) {
this.setState( {signals: nextProps.widget.signals});
}
// Identify if there was a change in the preselected signals
if (JSON.stringify(nextProps.widget.preselectedSignals) !== JSON.stringify(this.props.widget.preselectedSignals) || this.state.preselectedSignals.length === 0) {
// Update the currently selected signals by intersecting with the preselected signals
// Do the same with the plot values
var intersection = this.computeIntersection(nextProps.widget.preselectedSignals, nextProps.widget.signals);
this.setState({ signals: intersection });
this.updatePreselectedSignalsState(nextProps);
return;
}
}
// Perform the intersection of the lists, alternatively could be done with Sets ensuring unique values
computeIntersection(preselectedSignals, selectedSignals) {
return preselectedSignals.filter( s => selectedSignals.includes(s));
}
updatePreselectedSignalsState(nextProps) {
// Create checkboxes using the signal indices from simulation model
const preselectedSignals = nextProps.simulationModel.outputMapping.reduce(
// Loop through simulation model signals
(accum, model_signal, signal_index) => {
// Append them if they belong to the current selected type
if (nextProps.widget.preselectedSignals.indexOf(signal_index) > -1) {
accum.push(
{
index: signal_index,
name: model_signal.name,
type: model_signal.type
}
)
}
return accum;
}, []);
this.setState({ preselectedSignals });
}
updateSignalSelection(signal_index, checked) {
// Update the selected signals and propagate to parent component
var new_widget = Object.assign({}, this.props.widget, {
signals: checked? this.state.signals.concat(signal_index) : this.state.signals.filter( (idx) => idx !== signal_index )
});
this.props.onWidgetChange(new_widget);
}
render() {
let checkBoxes = [];
// Data passed to plot
if (this.props.simulationModel == null) {
return <div />;
}
const simulator = this.props.simulationModel.simulator;
let simulatorData = [];
if (this.props.data[simulator] != null && this.props.data[simulator].output != null && this.props.data[simulator].output.values != null) {
simulatorData = this.props.data[simulator].output.values.filter((values, index) => (
this.props.widget.signals.findIndex(value => value === index) !== -1
));
}
if (this.state.preselectedSignals && this.state.preselectedSignals.length > 0) {
// Create checkboxes using the signal indices from simulation model
checkBoxes = this.state.preselectedSignals.map( (signal) => {
var checked = this.state.signals.indexOf(signal.index) > -1;
var chkBxClasses = classNames({
'btn': true,
'btn-default': true,
'active': checked
});
return <Checkbox key={signal.index} className={chkBxClasses} checked={checked} disabled={ this.props.editing } onChange={(e) => this.updateSignalSelection(signal.index, e.target.checked) } > { signal.name } </Checkbox>
});
}
// Prepare an array with the signals to show in the legend
var legendSignals = this.state.preselectedSignals.reduce( (accum, signal, i) => {
if (this.state.signals.includes(signal.index)) {
accum.push({
index: signal.index,
name: signal.name,
type: signal.type
});
}
return accum;
}, []);
return (
<div className="plot-table-widget" ref="wrapper">
<div className="content">
<div className="table-plot-row">
<div className="widget-table">
{ checkBoxes.length > 0 ? (
<FormGroup className="btn-group-vertical">
{ checkBoxes }
</FormGroup>
) : ( <small>No signal has been pre-selected.</small> )
}
</div>
<div className="widget-plot">
<Plot
data={simulatorData}
time={this.props.widget.time}
width={this.props.widget.width - 100}
height={this.props.widget.height - 55}
yMin={this.props.widget.yMin}
yMax={this.props.widget.yMax}
yUseMinMax={this.props.widget.yUseMinMax}
paused={this.props.paused}
yLabel={this.props.widget.ylabel}
/>
</div>
</div>
<PlotLegend signals={legendSignals} />
</div>
</div>
);
}
}
export default WidgetPlotTable;

View file

@ -1,88 +0,0 @@
/**
* File: plot.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 08.03.2017
*
* 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 Plot from '../widget-plot/plot';
import PlotLegend from '../widget-plot/plot-legend';
class WidgetPlot extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
legend: []
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.simulationModel == null) {
this.setState({ data: [], legend: [] });
return;
}
const simulator = nextProps.simulationModel.simulator;
// Proceed if a simulation with models and a simulator are available
if (simulator && nextProps.data[simulator] != null && nextProps.data[simulator] != null && nextProps.data[simulator].output != null && nextProps.data[simulator].output.values != null) {
const chosenSignals = nextProps.widget.signals;
const data = nextProps.data[simulator].output.values.filter((values, index) => (
nextProps.widget.signals.findIndex(value => value === index) !== -1
));
// Query the signals that will be displayed in the legend
const legend = nextProps.simulationModel.outputMapping.reduce( (accum, model_signal, signal_index) => {
if (chosenSignals.includes(signal_index)) {
accum.push({ index: signal_index, name: model_signal.name, type: model_signal.type });
}
return accum;
}, []);
this.setState({ data, legend });
} else {
this.setState({ data: [], legend: [] });
}
}
render() {
return <div className="plot-widget" ref="wrapper">
<div className="widget-plot">
<Plot
data={this.state.data}
height={this.props.widget.height - 55}
width={this.props.widget.width - 20}
time={this.props.widget.time}
yMin={this.props.widget.yMin}
yMax={this.props.widget.yMax}
yUseMinMax={this.props.widget.yUseMinMax}
paused={this.props.paused}
yLabel={this.props.widget.ylabel}
/>
</div>
<PlotLegend signals={this.state.legend} />
</div>;
}
}
export default WidgetPlot;

View file

@ -1,132 +0,0 @@
/**
* File: slider.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 30.03.2017
* Copyright: 2018, Institute for Automation of Complex Power Systems, EONERC
*
* 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 { format } from 'd3';
import classNames from 'classnames';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
class WidgetSlider extends Component {
static get OrientationTypes() {
return ({
HORIZONTAL: {value: 0, name: 'Horizontal'},
VERTICAL: {value: 1, name: 'Vertical'}
})
}
constructor(props) {
super(props);
this.state = {
unit: ''
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.simulationModel == null) {
return;
}
// Update value
if (nextProps.widget.default_value && this.state.value === undefined) {
this.setState({
value: nextProps.widget.default_value,
});
}
// Update unit
if (nextProps.widget.simulationModel && nextProps.simulationModel.inputMapping && this.state.unit !== nextProps.simulationModel.inputMapping[nextProps.widget.signal].type) {
this.setState({
unit: nextProps.simulationModel.inputMapping[nextProps.widget.signal].type
});
}
// Check if the orientation changed, update the size if it did
if (this.props.widget.orientation !== nextProps.widget.orientation) {
let baseWidget = nextProps.widget;
// Exchange dimensions and constraints
let newWidget = Object.assign({}, baseWidget, {
width: baseWidget.height,
height: baseWidget.width,
minWidth: baseWidget.minHeight,
minHeight: baseWidget.minWidth,
maxWidth: baseWidget.maxHeight,
maxHeight: baseWidget.maxWidth
});
nextProps.onWidgetChange(newWidget);
}
}
valueIsChanging(newValue) {
if (this.props.widget.continous_update)
this.valueChanged(newValue);
this.setState({ value: newValue });
}
valueChanged(newValue) {
if (this.props.onInputChanged) {
this.props.onInputChanged(newValue);
}
}
render() {
let isVertical = this.props.widget.orientation === WidgetSlider.OrientationTypes.VERTICAL.value;
let fields = {
name: this.props.widget.name,
control: <Slider min={ this.props.widget.rangeMin } max={ this.props.widget.rangeMax } step={ this.props.widget.step } value={ this.state.value } disabled={ this.props.editing } vertical={ isVertical } onChange={ (v) => this.valueIsChanging(v) } onAfterChange={ (v) => this.valueChanged(v) }/>,
value: <span>{ format('.3s')(Number.parseFloat(this.state.value)) }</span>,
unit: <span className="signal-unit">{ this.state.unit }</span>
}
var widgetClasses = classNames({
'slider-widget': true,
'full': true,
'vertical': isVertical,
'horizontal': !isVertical
});
return (
this.props.widget.orientation === WidgetSlider.OrientationTypes.HORIZONTAL.value? (
<div className={widgetClasses}>
<label>{ fields.name }</label>
<div className='slider'>{ fields.control }</div>
<span>{ fields.value }</span>
</div>
) : (
<div className={widgetClasses}>
<label>{ fields.name }</label>
{ fields.control }
{ fields.value }
{ this.props.widget.showUnit && fields.unit }
</div>
)
);
}
}
export default WidgetSlider;

View file

@ -1,99 +0,0 @@
/**
* File: table.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 14.03.2017
*
* 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 { format } from 'd3';
import Table from '../table';
import TableColumn from '../table-column';
class WidgetTable extends Component {
constructor(props) {
super(props);
this.state = {
rows: [],
sequence: null,
showUnit: false
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.simulationModel == null) {
this.setState({ rows: [], sequence: null });
return;
}
const simulator = nextProps.simulationModel.simulator;
// check data
if (nextProps.data == null
|| nextProps.data[simulator] == null
|| nextProps.data[simulator].output == null
|| nextProps.data[simulator].output.values.length === 0
|| nextProps.data[simulator].output.values[0].length === 0) {
// clear values
this.setState({ rows: [], sequence: null, showUnit: false });
return;
}
// check if new data, otherwise skip
/*if (this.state.sequence >= nextProps.data[simulator.node][simulator.simulator].sequence) {
return;
}*/
// get rows
const rows = [];
nextProps.data[simulator].output.values.forEach((signal, index) => {
if (index < nextProps.simulationModel.outputMapping.length) {
rows.push({
name: nextProps.simulationModel.outputMapping[index].name,
unit: nextProps.simulationModel.outputMapping[index].type,
value: signal[signal.length - 1].y
});
}
});
this.setState({ showUnit: nextProps.showUnit, rows: rows, sequence: nextProps.data[simulator].output.sequence });
}
render() {
var columns = [
<TableColumn key={1} title="Signal" dataKey="name" width={120} />,
<TableColumn key={2} title="Value" dataKey="value" modifier={format('.4s')} />
];
if (this.props.widget.showUnit)
columns.push(<TableColumn key={3} title="Unit" dataKey="unit" />)
return (
<div className="table-widget">
<Table data={this.state.rows}>
{ columns }
</Table>
</div>
);
}
}
export default WidgetTable;

View file

@ -1,185 +0,0 @@
/**
* File: topology.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 08.01.2018
*
* 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 {ReactSVGPanZoom} from 'react-svg-pan-zoom';
import config from '../../config';
import '../../styles/simple-spinner.css';
import { cimsvg } from 'libcimsvg';
// Do not show Pintura's grid
const pinturaGridStyle = {
display: 'none'
}
// Avoid another color in the frontend
const pinturaBackingStyle = {
fill: 'transparent'
}
// Center spinner
const spinnerContainerStyle = {
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}
// Topology failed message
const msgContainerStyle = Object.assign({
backgroundColor: '#ececec'
},spinnerContainerStyle)
const msgStyle = {
fontWeight: 'bold'
}
// Initialize functions
function attachComponentEvents() {
window.onMouseOver = (e) => show(textSibling(e));
window.onMouseLeave = (e) => hide(textSibling(e));
}
function textSibling(e) {
// Find sibling text element and toggle its visibility
let gParent = e.target.parentElement;
return gParent.getElementsByTagName('text')[0];
}
function show(element) {
element.style.visibility = 'inherit';
}
function hide(element) {
element.style.visibility = 'hidden';
}
// De-initialize functions
function detachComponentEvents() {
window.onMouseOver = null;
window.onMouseLeave = null;
}
class WidgetTopology extends React.Component {
constructor(props) {
super(props);
this.svgElem = React.createRef();
this.Viewer = null;
this.state = {
visualizationState: 'initial'
};
}
componentDidMount() {
if (this.svgElem) {
window.onMouseLeave = function() {};
window.onMouseOver = function() {};
window.onMouseLeave = function() {};
window.onMouseUp = function() {};
window.onMouseDown = function() {};
window.onMouseMove = function() {};
}
}
componentWillUnmount() {
detachComponentEvents();
}
componentWillReceiveProps(nextProps) {
const file = nextProps.files.find(file => file._id === nextProps.widget.file);
// Ensure model is requested only once or a different was selected
if (this.props.widget.file !== nextProps.widget.file || (this.state.visualizationState === 'initial' && file)) {
this.setState({'visualizationState': 'loading' });
if (file) {
fetch(new Request('/' + config.publicPathBase + file.path))
.then( response => {
if (response.status === 200) {
this.setState({'visualizationState': 'ready' });
return response.text().then( contents => {
if (this.svgElem) {
let cimsvgInstance = new cimsvg(this.svgElem.current);
cimsvg.setCimsvg(cimsvgInstance);
cimsvgInstance.setFileCount(1);
cimsvgInstance.loadFile(contents);
//cimsvgInstance.fit();
attachComponentEvents();
}
else {
console.error("The svgElem variable is not initialized before the attempt to create the cimsvg instance.");
}
});
} else {
throw new Error('Request failed');
}
})
.catch(error => {
console.error(error);
this.setState({
'visualizationState': 'show_message',
'message': 'Topology could not be loaded.'});
});
}
} else {
// No file has been selected
if (!nextProps.widget.file) {
this.setState({
'visualizationState': 'show_message',
'message': 'Select a topology model first.'});
}
}
}
render() {
var markup = null;
switch(this.state.visualizationState) {
case 'loading':
markup = <div style={spinnerContainerStyle}><div className="loader" /></div>; break;
case 'show_message':
markup = <div style={msgContainerStyle}><div style={msgStyle}>{ this.state.message }</div></div>; break;
default:
markup = (<div>
<ReactSVGPanZoom
ref={Viewer => this.Viewer = Viewer}
style={{outline: "1px solid grey"}}
detectAutoPan={false}
miniaturePosition="none"
toolbarPosition="none"
background="white"
tool="pan"
width={this.props.widget.width-2} height={this.props.widget.height-2} >
<svg width={this.props.widget.width} height={this.props.widget.height}>
<svg ref={ this.svgElem } width={this.props.widget.width} height={this.props.widget.height}>
<rect className="backing" style={pinturaBackingStyle} />
<g className="grid" style={pinturaGridStyle} />
<g className="diagrams"/>
</svg>
</svg>
</ReactSVGPanZoom>
</div>);
}
return markup;
}
}
export default WidgetTopology;

View file

@ -1,72 +0,0 @@
/**
* File: value.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 04.03.2017
*
* 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 { format } from 'd3';
class WidgetValue extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
unit: ''
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.simulationModel == null) {
this.setState({ value: '' });
return;
}
const simulator = nextProps.simulationModel.simulator;
// update value
if (nextProps.data == null || nextProps.data[simulator] == null || nextProps.data[simulator].output == null || nextProps.data[simulator].output.values == null) {
this.setState({ value: '' });
return;
}
const unit = nextProps.simulationModel.outputMapping[nextProps.widget.signal].type;
// check if value has changed
const signal = nextProps.data[simulator].output.values[nextProps.widget.signal];
if (signal != null && this.state.value !== signal[signal.length - 1].y) {
this.setState({ value: signal[signal.length - 1].y, unit });
}
}
render() {
var value_to_render = Number(this.state.value);
return (
<div className="single-value-widget">
<strong style={{ fontSize: this.props.widget.textSize + 'px' }}>{this.props.widget.name}</strong>
<span style={{ fontSize: this.props.widget.textSize + 'px' }}>{Number.isNaN(value_to_render) ? NaN : format('.3s')(value_to_render)}</span>
{this.props.widget.showUnit &&
<span style={{ fontSize: this.props.widget.textSize + 'px' }}>[{this.state.unit}]</span>
}
</div>
);
}
}
export default WidgetValue;

View file

@ -1,11 +1,28 @@
/**
* 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/>.
******************************************************************************/
const config = {
publicPathBase: 'public/',
instance: 'frontend of the Global RT-SuperLab Demonstration',
instance: 'VILLASweb',
subtitle: 'ACS',
admin: {
name: 'Steffen Vogel',
name: 'Institute for Automation of Complex Power Systems (ACS), RWTH Aachen University, Germany',
mail: 'stvogel@eonerc.rwth-aachen.de'
}
}
};
export default config

View file

@ -1,151 +0,0 @@
/**
* File: app.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* 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 { Container } from 'flux/utils';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import NotificationSystem from 'react-notification-system';
import { Redirect, Route } from 'react-router-dom';
import { Col } from 'react-bootstrap';
import AppDispatcher from '../app-dispatcher';
import SimulationStore from '../stores/simulation-store';
import SimulatorStore from '../stores/simulator-store';
import UserStore from '../stores/user-store';
import NotificationsDataManager from '../data-managers/notifications-data-manager';
import Home from '../components/home';
import Header from '../components/header';
import Footer from '../components/footer';
import SidebarMenu from '../components/menu-sidebar';
import HeaderMenu from '../components/header-menu';
import Projects from './projects';
import Project from './project';
import Simulators from './simulators';
import Visualization from './visualization';
import Simulations from './simulations';
import Simulation from './simulation';
import SimulationModel from './simulation-model';
import Users from './users';
import '../styles/app.css';
class App extends React.Component {
static getStores() {
return [ SimulatorStore, UserStore, SimulationStore ];
}
static calculateState(prevState) {
let currentUser = UserStore.getState().currentUser;
return {
simulators: SimulatorStore.getState(),
simulations: SimulationStore.getState(),
currentRole: currentUser ? currentUser.role : '',
token: UserStore.getState().token,
showSidebarMenu: false,
};
}
componentWillMount() {
// if token stored locally, request user
const token = localStorage.getItem('token');
if (token != null && token !== '') {
// save token so we dont logout
this.setState({ token });
AppDispatcher.dispatch({
type: 'users/logged-in',
token: token
});
}
}
componentDidMount() {
// load all simulators and simulations to fetch simulation data
AppDispatcher.dispatch({
type: 'simulators/start-load',
token: this.state.token
});
AppDispatcher.dispatch({
type: 'simulations/start-load',
token: this.state.token
});
NotificationsDataManager.setSystem(this.refs.notificationSystem);
}
showSidebarMenu = () => {
this.setState({ showSidebarMenu: true });
}
hideSidebarMenu = () => {
this.setState({ showSidebarMenu: false });
}
render() {
if (this.state.token == null) {
return (<Redirect to="/login" />);
}
return (
<div>
<Col style={{ width: this.state.showSidebarMenu ? '280px' : '0px' }} smHidden mdHidden lgHidden className="sidenav">
<HeaderMenu onClose={this.hideSidebarMenu} currentRole={this.state.currentRole} />
</Col>
<div className="app">
<NotificationSystem ref="notificationSystem" />
<Header onMenuButton={this.showSidebarMenu} showMenuButton={this.state.token != null} />
<div className={`app-body app-body-spacing`} >
<Col xsHidden>
<SidebarMenu currentRole={this.state.currentRole} />
</Col>
<div className={`app-content app-content-margin-left`}>
<Route exact path="/" component={Home} />
<Route path="/home" component={Home} />
<Route exact path="/projects" component={Projects} />
<Route path="/projects/:project" component={Project} />
<Route path="/visualizations/:visualization" component={Visualization} />
<Route exact path="/simulations" component={Simulations} />
<Route path="/simulations/:simulation" component={Simulation} />
<Route path="/simulationModel/:simulationModel" component={SimulationModel} />
<Route path="/simulators" component={Simulators} />
<Route path="/users" component={Users} />
</div>
</div>
<Footer />
</div>
</div>
);
}
}
export default DragDropContext(HTML5Backend)(Container.create(App));

View file

@ -1,96 +0,0 @@
/**
* File: login.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 15.03.2017
*
* 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 { Container } from 'flux/utils';
import { PageHeader } from 'react-bootstrap';
import NotificationSystem from 'react-notification-system';
import { Redirect } from 'react-router-dom';
import LoginForm from '../components/login-form';
import Header from '../components/header';
import Footer from '../components/footer';
import NotificationsDataManager from '../data-managers/notifications-data-manager';
import AppDispatcher from '../app-dispatcher';
import UserStore from '../stores/user-store';
class Login extends Component {
static getStores() {
return [ UserStore ];
}
static calculateState() {
return {
currentUser: UserStore.getState().currentUser,
token: UserStore.getState().token,
loginMessage: UserStore.getState().loginMessage
};
}
componentDidMount() {
NotificationsDataManager.setSystem(this.refs.notificationSystem);
}
componentWillUpdate(nextProps, nextState) {
// if token stored locally, request user
if (nextState.token == null) {
const token = localStorage.getItem('token');
if (token != null && token !== '' && nextState.currentUser == null) {
AppDispatcher.dispatch({
type: 'users/logged-in',
token: token
});
}
} else {
// check if logged in
if (nextState.currentUser != null) {
// save login in local storage
localStorage.setItem('token', nextState.token);
}
}
}
render() {
if (this.state.currentUser != null) {
return (<Redirect to="/" />);
}
return (
<div>
<NotificationSystem ref="notificationSystem" />
<Header />
<div className="login-container">
<PageHeader>Login</PageHeader>
<LoginForm loginMessage={this.state.loginMessage} />
</div>
<Footer />
</div>
);
}
}
export default Container.create(Login);

View file

@ -1,234 +0,0 @@
/**
* File: project.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 03.03.2017
*
* 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 { Container } from 'flux/utils';
import { Button } from 'react-bootstrap';
import FileSaver from 'file-saver';
import AppDispatcher from '../app-dispatcher';
import ProjectStore from '../stores/project-store';
import UserStore from '../stores/user-store';
import VisualizationStore from '../stores/visualization-store';
import SimulationStore from '../stores/simulation-store';
import Icon from '../components/icon';
import CustomTable from '../components/table';
import TableColumn from '../components/table-column';
import NewVisualzationDialog from '../components/dialogs/new-visualization';
import EditVisualizationDialog from '../components/dialogs/edit-visualization';
import ImportVisualizationDialog from '../components/dialogs/import-visualization';
import DeleteDialog from '../components/dialogs/delete-dialog';
class Visualizations extends Component {
static getStores() {
return [ ProjectStore, VisualizationStore, UserStore, SimulationStore ];
}
static calculateState(prevState, props) {
prevState = prevState || {};
// load project
const sessionToken = UserStore.getState().token;
let project = ProjectStore.getState().find(project => project._id === props.match.params.project);
if (project == null) {
AppDispatcher.dispatch({
type: 'projects/start-load',
data: props.match.params.project,
token: sessionToken
});
project = {};
}
// load simulation
let simulation = {};
if (project.simulation != null) {
simulation = SimulationStore.getState().find(simulation => simulation._id === project.simulation);
}
// load visualizations
let visualizations = [];
if (project.visualizations != null) {
visualizations = VisualizationStore.getState().filter(visualization => project.visualizations.includes(visualization._id));
}
return {
visualizations,
project,
simulation,
sessionToken,
newModal: prevState.newModal || false,
deleteModal: prevState.deleteModal || false,
editModal: prevState.editModal || false,
importModal: prevState.importModal || false,
modalData: prevState.modalData || {}
};
}
componentDidMount() {
AppDispatcher.dispatch({
type: 'visualizations/start-load',
token: this.state.sessionToken
});
AppDispatcher.dispatch({
type: 'simulations/start-load',
token: this.state.sessionToken
});
}
closeNewModal(data) {
this.setState({ newModal: false });
if (data) {
// add project to visualization
data.project = this.state.project._id;
AppDispatcher.dispatch({
type: 'visualizations/start-add',
data: data,
token: this.state.sessionToken
});
this.setState({ project: {} }, () => {
AppDispatcher.dispatch({
type: 'projects/start-load',
data: this.props.match.params.project,
token: this.state.sessionToken
});
});
}
}
closeDeleteModal = confirmDelete => {
this.setState({ deleteModal: false });
if (confirmDelete === false) {
return;
}
AppDispatcher.dispatch({
type: 'visualizations/start-remove',
data: this.state.modalData,
token: this.state.sessionToken
});
}
closeEditModal(data) {
this.setState({ editModal : false });
if (data) {
AppDispatcher.dispatch({
type: 'visualizations/start-edit',
data: data,
token: this.state.sessionToken
});
}
}
closeImportModal(data) {
this.setState({ importModal: false });
if (data) {
data.project = this.state.project._id;
AppDispatcher.dispatch({
type: 'visualizations/start-add',
data,
token: this.state.sessionToken
});
this.setState({ project: {} }, () => {
AppDispatcher.dispatch({
type: 'projects/start-load',
data: this.props.match.params.project,
token: this.state.sessionToken
});
});
}
}
exportVisualization(index) {
// filter properties
let visualization = Object.assign({}, this.state.visualizations[index]);
delete visualization._id;
delete visualization.project;
delete visualization.user;
visualization.widgets.forEach(widget => {
delete widget.simulator;
});
// show save dialog
const blob = new Blob([JSON.stringify(visualization, null, 2)], { type: 'application/json' });
FileSaver.saveAs(blob, 'visualization - ' + visualization.name + '.json');
}
onModalKeyPress = (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.confirmDeleteModal();
}
}
render() {
const buttonStyle = {
marginRight: '10px'
};
return (
<div className='section'>
<h1>{this.state.project.name}</h1>
<CustomTable data={this.state.visualizations}>
<TableColumn title='Name' dataKey='name' link='/visualizations/' linkKey='_id' />
<TableColumn
width='100'
editButton
deleteButton
exportButton
onEdit={(index) => this.setState({ editModal: true, modalData: this.state.visualizations[index] })}
onDelete={(index) => this.setState({ deleteModal: true, modalData: this.state.visualizations[index] })}
onExport={index => this.exportVisualization(index)}
/>
</CustomTable>
<Button onClick={() => this.setState({ newModal: true })} style={buttonStyle}><Icon icon="plus" /> Visualization</Button>
<Button onClick={() => this.setState({ importModal: true })} style={buttonStyle}><Icon icon="upload" /> Import</Button>
<NewVisualzationDialog show={this.state.newModal} onClose={(data) => this.closeNewModal(data)} />
<EditVisualizationDialog show={this.state.editModal} onClose={(data) => this.closeEditModal(data)} visualization={this.state.modalData} />
<ImportVisualizationDialog show={this.state.importModal} onClose={data => this.closeImportModal(data)} simulation={this.state.simulation} />
<DeleteDialog title="visualization" name={this.state.modalData.name} show={this.state.deleteModal} onClose={this.closeDeleteModal} />
</div>
);
}
}
export default Container.create(Visualizations, {withProps: true});

View file

@ -1,159 +0,0 @@
/**
* File: projects.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* 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 { Container } from 'flux/utils';
import { Button } from 'react-bootstrap';
import AppDispatcher from '../app-dispatcher';
import ProjectStore from '../stores/project-store';
import UserStore from '../stores/user-store';
import SimulationStore from '../stores/simulation-store';
import Icon from '../components/icon';
import Table from '../components/table';
import TableColumn from '../components/table-column';
import NewProjectDialog from '../components/dialogs/new-project';
import EditProjectDialog from '../components/dialogs/edit-project';
import DeleteDialog from '../components/dialogs/delete-dialog';
class Projects extends React.Component {
static getStores() {
return [ ProjectStore, SimulationStore, UserStore ];
}
static calculateState() {
return {
projects: ProjectStore.getState(),
simulations: SimulationStore.getState(),
sessionToken: UserStore.getState().token,
newModal: false,
editModal: false,
deleteModal: false,
modalData: {}
};
}
componentWillMount() {
AppDispatcher.dispatch({
type: 'projects/start-load',
token: this.state.sessionToken
});
AppDispatcher.dispatch({
type: 'simulations/start-load',
token: this.state.sessionToken
});
}
closeNewModal(data) {
this.setState({ newModal: false });
if (data) {
AppDispatcher.dispatch({
type: 'projects/start-add',
data,
token: this.state.sessionToken
});
}
}
closeDeleteModal = confirmDelete => {
this.setState({ deleteModal: false });
if (confirmDelete === false) {
return;
}
AppDispatcher.dispatch({
type: 'projects/start-remove',
data: this.state.modalData,
token: this.state.sessionToken
});
}
closeEditModal(data) {
this.setState({ editModal: false });
if (data) {
AppDispatcher.dispatch({
type: 'projects/start-edit',
data: data,
token: this.state.sessionToken
});
}
}
getSimulationName(id) {
for (var i = 0; i < this.state.simulations.length; i++) {
if (this.state.simulations[i]._id === id) {
return this.state.simulations[i].name;
}
}
return id;
}
hasValidSimulation() {
const simulations = this.state.simulations.filter(simulation => {
return simulation.models.length > 0;
});
return simulations.length > 0;
}
onModalKeyPress = (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.confirmDeleteModal();
}
}
render() {
return (
<div className='section'>
<h1>Projects</h1>
<Table data={this.state.projects}>
<TableColumn title='Name' dataKey='name' link='/projects/' linkKey='_id' />
<TableColumn title='Simulation' dataKey='simulation' modifier={(id) => this.getSimulationName(id)} />
<TableColumn width='70' editButton deleteButton onEdit={index => this.setState({ editModal: true, modalData: this.state.projects[index] })} onDelete={index => this.setState({ deleteModal: true, modalData: this.state.projects[index] })} />
</Table>
<Button onClick={() => this.setState({ newModal: true })} disabled={!this.hasValidSimulation()}><Icon icon='plus' /> Project</Button>
{!this.hasValidSimulation() &&
<span><i> Simulation with at least one simulation-model required!</i></span>
}
<NewProjectDialog show={this.state.newModal} onClose={(data) => this.closeNewModal(data)} simulations={this.state.simulations} />
<EditProjectDialog show={this.state.editModal} onClose={(data) => this.closeEditModal(data)} project={this.state.modalData} simulations={this.state.simulations} />
<DeleteDialog title="project" name={this.state.modalData.name} show={this.state.deleteModal} onClose={this.closeDeleteModal} />
</div>
);
}
}
export default Container.create(Projects);

View file

@ -1,151 +0,0 @@
/**
* File: selectFile.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 10.05.2018
*
* 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 { Container } from 'flux/utils';
import { FormGroup, FormControl, ControlLabel, Button, ProgressBar, Col } from 'react-bootstrap';
import FileStore from '../stores/file-store';
import UserStore from '../stores/user-store';
import AppDispatcher from '../app-dispatcher';
class SelectFile extends React.Component {
static getStores() {
return [ FileStore, UserStore ];
}
static calculateState() {
return {
files: FileStore.getState(),
sessionToken: UserStore.getState().token,
selectedFile: '',
uploadFile: null,
uploadProgress: 0
};
}
componentDidMount() {
AppDispatcher.dispatch({
type: 'files/start-load',
token: this.state.sessionToken
});
}
componentWillReceiveProps(nextProps) {
if (nextProps.value === this.state.selectedSimulator) {
return;
}
let selectedSimulator = nextProps.value;
if (selectedSimulator == null) {
if (this.state.simulators.length > 0) {
selectedSimulator = this.state.simulators[0]._id;
} else {
selectedSimulator = '';
}
}
this.setState({ selectedSimulator });
}
handleChange = event => {
this.setState({ selectedFile: event.target.value });
// send file to callback
if (this.props.onChange != null) {
const file = this.state.files.find(f => f._id === event.target.value);
this.props.onChange(file);
}
}
selectUploadFile = event => {
this.setState({ uploadFile: event.target.files[0] });
}
startFileUpload = () => {
// upload file
const formData = new FormData();
formData.append(0, this.state.uploadFile);
AppDispatcher.dispatch({
type: 'files/start-upload',
data: formData,
token: this.state.sessionToken,
progressCallback: this.updateUploadProgress,
finishedCallback: this.clearProgress
});
}
updateUploadProgress = event => {
this.setState({ uploadProgress: parseInt(event.percent.toFixed(), 10) });
}
clearProgress = () => {
// select uploaded file
const selectedFile = this.state.files[this.state.files.length - 1]._id;
this.setState({ selectedFile, uploadProgress: 0 });
}
render() {
const fileOptions = this.state.files.map(f =>
<option key={f._id} value={f._id}>{f.name}</option>
);
const progressBarStyle = {
marginLeft: '100px',
marginTop: '-25px'
};
return <div>
<FormGroup>
<Col componentClass={ControlLabel} sm={3} md={2}>
{this.props.name}
</Col>
<Col sm={9} md={10}>
<FormControl disabled={this.props.disabled} componentClass='select' placeholder='Select file' onChange={this.handleChange}>
{fileOptions}
</FormControl>
</Col>
</FormGroup>
<FormGroup>
<Col sm={9} md={10} smOffset={3} mdOffset={2}>
<FormControl disabled={this.props.disabled} type='file' onChange={this.selectUploadFile} />
</Col>
</FormGroup>
<FormGroup>
<Col sm={9} md={10} smOffset={3} mdOffset={2}>
<Button disabled={this.props.disabled} bsSize='small' onClick={this.startFileUpload}>
Upload file
</Button>
<ProgressBar striped active now={this.state.uploadProgress} label={this.state.uploadProgress + '%'} style={progressBarStyle} />
</Col>
</FormGroup>
</div>;
}
}
export default Container.create(SelectFile);

View file

@ -1,88 +0,0 @@
/**
* File: selectSimulator.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 10.05.2018
*
* 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 { Container } from 'flux/utils';
import { FormGroup, FormControl, ControlLabel, Col } from 'react-bootstrap';
import _ from 'lodash';
import SimulatorStore from '../stores/simulator-store';
class SelectSimulator extends React.Component {
static getStores() {
return [ SimulatorStore ];
}
static calculateState() {
return {
simulators: SimulatorStore.getState(),
selectedSimulator: ''
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.value === this.state.selectedSimulator) {
return;
}
let selectedSimulator = nextProps.value;
if (selectedSimulator == null) {
if (this.state.simulators.length > 0) {
selectedSimulator = this.state.simulators[0]._id;
} else {
selectedSimulator = '';
}
}
this.setState({ selectedSimulator });
}
handleChange = event => {
this.setState({ selectedSimulator: event.target.value });
// send complete simulator to callback
if (this.props.onChange != null) {
const simulator = this.state.simulators.find(s => s._id === event.target.value);
this.props.onChange(simulator);
}
}
render() {
const simulatorOptions = this.state.simulators.map(s =>
<option key={s._id} value={s._id}>{_.get(s, 'properties.name') || _.get(s, 'rawProperties.name') || s.uuid}</option>
);
return <FormGroup>
<Col componentClass={ControlLabel} sm={3} md={2}>
Simulator
</Col>
<Col sm={9} md={10}>
<FormControl componentClass='select' placeholder='Select simulator' value={this.state.selectedSimulator} onChange={this.handleChange}>
{simulatorOptions}
</FormControl>
</Col>
</FormGroup>;
}
}
export default Container.create(SelectSimulator);

View file

@ -1,171 +0,0 @@
/**
* File: simulationModel.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 10.05.2018
*
* 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 { Container } from 'flux/utils';
import { Button, Col, Form, ControlLabel } from 'react-bootstrap';
import SimulationModelStore from '../stores/simulation-model-store';
import UserStore from '../stores/user-store';
import AppDispatcher from '../app-dispatcher';
import SelectSimulator from './select-simulator';
import SelectFile from './select-file';
import SignalMapping from '../components/signal-mapping';
import EditableHeader from '../components/editable-header';
import ParametersEditor from '../components/parameters-editor';
class SimulationModel extends React.Component {
static getStores() {
return [ SimulationModelStore, UserStore ];
}
static calculateState(prevState, props) {
const simulationModel = SimulationModelStore.getState().find(m => m._id === props.match.params.simulationModel);
return {
simulationModel: simulationModel || {},
sessionToken: UserStore.getState().token
};
}
componentWillMount() {
AppDispatcher.dispatch({
type: 'simulationModels/start-load',
data: this.props.match.params.simulationModel,
token: this.state.sessionToken
});
}
submitForm = event => {
event.preventDefault();
}
saveChanges = () => {
AppDispatcher.dispatch({
type: 'simulationModels/start-edit',
data: this.state.simulationModel,
token: this.state.sessionToken
});
this.props.history.push('/simulations/' + this.state.simulationModel.simulation);
}
discardChanges = () => {
this.props.history.push('/simulations/' + this.state.simulationModel.simulation);
}
handleSimulatorChange = simulator => {
const simulationModel = this.state.simulationModel;
simulationModel.simulator = simulator;
this.setState({ simulationModel });
}
handleModelChange = file => {
console.log(file);
}
handleConfigurationChange = file => {
console.log(file);
}
handleOutputMappingChange = (length, signals) => {
const simulationModel = this.state.simulationModel;
simulationModel.outputMapping = signals;
simulationModel.outputLength = length;
this.setState({ simulationModel });
}
handleInputMappingChange = (length, signals) => {
const simulationModel = this.state.simulationModel;
simulationModel.inputMapping = signals;
simulationModel.inputLength = length;
this.setState({ simulationModel });
}
handleTitleChange = title => {
const simulationModel = this.state.simulationModel;
simulationModel.name = title;
this.setState({ simulationModel });
}
handleStartParametersChange = startParameters => {
const simulationModel = this.state.simulationModel;
simulationModel.startParameters = startParameters;
this.setState({ simulationModel });
}
render() {
const buttonStyle = {
marginRight: '10px'
};
return <div className='section'>
<EditableHeader title={this.state.simulationModel.name} onChange={this.handleTitleChange} />
<Form horizontal onSubmit={this.submitForm}>
<Col xs={12} sm={12}>
<SelectSimulator onChange={this.handleSimulatorChange} value={this.state.simulationModel.simulator} />
<SelectFile disabled type='model' name='Model' onChange={this.handleModelChange} value={this.state.simulationModel.model} />
<SelectFile disabled type='configuration' name='Configuration' onChange={this.handleConfigurationChange} value={this.state.simulationModel.configuration} />
<div>
<Col componentClass={ControlLabel} sm={3} md={2}>
Start Parameters
</Col>
<Col sm={9} md={10}>
<ParametersEditor content={this.state.simulationModel.startParameters} onChange={this.handleStartParametersChange} />
</Col>
</div>
</Col>
<Col xs={12} sm={6}>
<SignalMapping name='Output' length={this.state.simulationModel.outputLength} signals={this.state.simulationModel.outputMapping} onChange={this.handleOutputMappingChange} />
</Col>
<Col xs={12} sm={6}>
<SignalMapping name='Input' length={this.state.simulationModel.inputLength} signals={this.state.simulationModel.inputMapping} onChange={this.handleInputMappingChange} />
</Col>
<div style={{ clear: 'both' }}></div>
<Button onClick={this.discardChanges} style={buttonStyle}>Cancel</Button>
<Button bsStyle='primary' onClick={this.saveChanges} style={buttonStyle}>Save</Button>
</Form>
</div>;
}
}
export default Container.create(SimulationModel, { withProps: true });

View file

@ -1,288 +0,0 @@
/**
* File: simulation.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 04.03.2017
*
* 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 { Container } from 'flux/utils';
import { Button } from 'react-bootstrap';
import FileSaver from 'file-saver';
import _ from 'lodash';
import SimulationStore from '../stores/simulation-store';
import SimulatorStore from '../stores/simulator-store';
import SimulationModelStore from '../stores/simulation-model-store';
import UserStore from '../stores/user-store';
import AppDispatcher from '../app-dispatcher';
import Icon from '../components/icon';
import Table from '../components/table';
import TableColumn from '../components/table-column';
import ImportSimulationModelDialog from '../components/dialogs/import-simulation-model';
import SimulatorAction from '../components/simulator-action';
import DeleteDialog from '../components/dialogs/delete-dialog';
class Simulation extends React.Component {
static getStores() {
return [ SimulationStore, SimulatorStore, SimulationModelStore, UserStore ];
}
static calculateState(prevState, props) {
// get selected simulation
const sessionToken = UserStore.getState().token;
let simulation = SimulationStore.getState().find(s => s._id === props.match.params.simulation);
if (simulation == null) {
AppDispatcher.dispatch({
type: 'simulations/start-load',
data: props.match.params.simulation,
token: sessionToken
});
simulation = {};
}
// load models
let simulationModels = [];
if (simulation.models != null) {
simulationModels = SimulationModelStore.getState().filter(m => m != null && simulation.models.includes(m._id));
}
return {
simulationModels,
simulation,
simulators: SimulatorStore.getState(),
sessionToken,
deleteModal: false,
importModal: false,
modalData: {},
selectedSimulationModels: []
}
}
componentWillMount() {
AppDispatcher.dispatch({
type: 'simulations/start-load',
token: this.state.sessionToken
});
AppDispatcher.dispatch({
type: 'simulationModels/start-load',
token: this.state.sessionToken
});
AppDispatcher.dispatch({
type: 'simulators/start-load',
token: this.state.sessionToken
});
}
addSimulationModel = () => {
const simulationModel = {
simulation: this.state.simulation._id,
name: 'New Simulation Model',
simulator: this.state.simulators.length > 0 ? this.state.simulators[0]._id : null,
outputLength: 1,
outputMapping: [{ name: 'Signal', type: 'Type' }],
inputLength: 1,
inputMapping: [{ name: 'Signal', type: 'Type' }]
};
AppDispatcher.dispatch({
type: 'simulationModels/start-add',
data: simulationModel,
token: this.state.sessionToken
});
this.setState({ simulation: {} }, () => {
AppDispatcher.dispatch({
type: 'simulations/start-load',
data: this.props.match.params.simulation,
token: this.state.sessionToken
});
});
}
closeDeleteModal = confirmDelete => {
this.setState({ deleteModal: false });
if (confirmDelete === false) {
return;
}
AppDispatcher.dispatch({
type: 'simulationModels/start-remove',
data: this.state.modalData,
token: this.state.sessionToken
});
}
importSimulationModel = simulationModel => {
this.setState({ importModal: false });
if (simulationModel == null) {
return;
}
simulationModel.simulation = this.state.simulation._id;
console.log(simulationModel);
AppDispatcher.dispatch({
type: 'simulationModels/start-add',
data: simulationModel,
token: this.state.sessionToken
});
this.setState({ simulation: {} }, () => {
AppDispatcher.dispatch({
type: 'simulations/start-load',
data: this.props.match.params.simulation,
token: this.state.sessionToken
});
});
}
getSimulatorName(simulatorId) {
for (let simulator of this.state.simulators) {
if (simulator._id === simulatorId) {
return _.get(simulator, 'properties.name') || _.get(simulator, 'rawProperties.name') || simulator.uuid;
}
}
}
exportModel(index) {
// filter properties
const model = Object.assign({}, this.state.simulationModels[index]);
delete model.simulator;
delete model.simulation;
// show save dialog
const blob = new Blob([JSON.stringify(model, null, 2)], { type: 'application/json' });
FileSaver.saveAs(blob, 'simulation model - ' + model.name + '.json');
}
onSimulationModelChecked(index, event) {
const selectedSimulationModels = Object.assign([], this.state.selectedSimulationModels);
for (let key in selectedSimulationModels) {
if (selectedSimulationModels[key] === index) {
// update existing entry
if (event.target.checked) {
return;
}
selectedSimulationModels.splice(key, 1);
this.setState({ selectedSimulationModels });
return;
}
}
// add new entry
if (event.target.checked === false) {
return;
}
selectedSimulationModels.push(index);
this.setState({ selectedSimulationModels });
}
runAction = action => {
for (let index of this.state.selectedSimulationModels) {
// get simulator for model
let simulator = null;
for (let sim of this.state.simulators) {
if (sim._id === this.state.simulationModels[index].simulator) {
simulator = sim;
}
}
if (simulator == null) {
continue;
}
if (action.data.action === 'start') {
action.data.parameters = this.state.simulationModels[index].startParameters;
}
AppDispatcher.dispatch({
type: 'simulators/start-action',
simulator,
data: action.data,
token: this.state.sessionToken
});
}
}
render() {
const buttonStyle = {
marginLeft: '10px'
};
return <div className='section'>
<h1>{this.state.simulation.name}</h1>
<Table data={this.state.simulationModels}>
<TableColumn checkbox onChecked={(index, event) => this.onSimulationModelChecked(index, event)} width='30' />
<TableColumn title='Name' dataKey='name' link='/simulationModel/' linkKey='_id' />
<TableColumn title='Simulator' dataKey='simulator' modifier={(simulator) => this.getSimulatorName(simulator)} />
<TableColumn title='Output' dataKey='outputLength' width='100' />
<TableColumn title='Input' dataKey='inputLength' width='100' />
<TableColumn
title=''
width='70'
deleteButton
exportButton
onDelete={(index) => this.setState({ deleteModal: true, modalData: this.state.simulationModels[index], modalIndex: index })}
onExport={index => this.exportModel(index)}
/>
</Table>
<div style={{ float: 'left' }}>
<SimulatorAction
runDisabled={this.state.selectedSimulationModels.length === 0}
runAction={this.runAction}
actions={[
{ id: '0', title: 'Start', data: { action: 'start' } },
{ id: '1', title: 'Stop', data: { action: 'stop' } },
{ id: '2', title: 'Pause', data: { action: 'pause' } },
{ id: '3', title: 'Resume', data: { action: 'resume' } }
]}/>
</div>
<div style={{ float: 'right' }}>
<Button onClick={this.addSimulationModel} style={buttonStyle}><Icon icon="plus" /> Simulation Model</Button>
<Button onClick={() => this.setState({ importModal: true })} style={buttonStyle}><Icon icon="upload" /> Import</Button>
</div>
<div style={{ clear: 'both' }} />
<ImportSimulationModelDialog show={this.state.importModal} onClose={this.importSimulationModel} simulators={this.state.simulators} />
<DeleteDialog title="simulation model" name={this.state.modalData.name} show={this.state.deleteModal} onClose={this.closeDeleteModal} />
</div>;
}
}
export default Container.create(Simulation, { withProps: true });

View file

@ -1,328 +0,0 @@
/**
* File: simulations.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 04.03.2017
*
* 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 { Container } from 'flux/utils';
import { Button } from 'react-bootstrap';
import FileSaver from 'file-saver';
import AppDispatcher from '../app-dispatcher';
import SimulationStore from '../stores/simulation-store';
import UserStore from '../stores/user-store';
import SimulatorStore from '../stores/simulator-store';
import SimulationModelStore from '../stores/simulation-model-store';
import Icon from '../components/icon';
import Table from '../components/table';
import TableColumn from '../components/table-column';
import NewSimulationDialog from '../components/dialogs/new-simulation';
import EditSimulationDialog from '../components/dialogs/edit-simulation';
import ImportSimulationDialog from '../components/dialogs/import-simulation';
import SimulatorAction from '../components/simulator-action';
import DeleteDialog from '../components/dialogs/delete-dialog';
class Simulations extends Component {
static getStores() {
return [ SimulationStore, UserStore, SimulatorStore, SimulationModelStore ];
}
static calculateState() {
const simulations = SimulationStore.getState();
const simulationModels = SimulationModelStore.getState();
const simulators = SimulatorStore.getState();
const sessionToken = UserStore.getState().token;
return {
simulations,
simulationModels,
simulators,
sessionToken,
newModal: false,
deleteModal: false,
editModal: false,
importModal: false,
modalSimulation: {},
selectedSimulations: []
};
}
componentDidMount() {
AppDispatcher.dispatch({
type: 'simulations/start-load',
token: this.state.sessionToken
});
}
componentDidUpdate() {
const simulationModelIds = [];
const simulatorIds = [];
for (let simulation of this.state.simulations) {
for (let modelId of simulation.models) {
const model = this.state.simulationModels.find(m => m != null && m._id === modelId);
if (model == null) {
simulationModelIds.push(modelId);
continue;
}
if (this.state.simulators.includes(s => s._id === model.simulator) === false) {
simulatorIds.push(model.simulator);
}
}
}
if (simulationModelIds.length > 0) {
AppDispatcher.dispatch({
type: 'simulationModels/start-load',
data: simulationModelIds,
token: this.state.sessionToken
});
}
if (simulatorIds.length > 0) {
AppDispatcher.dispatch({
type: 'simulators/start-load',
data: simulatorIds,
token: this.state.sessionToken
});
}
}
closeNewModal(data) {
this.setState({ newModal : false });
if (data) {
AppDispatcher.dispatch({
type: 'simulations/start-add',
data,
token: this.state.sessionToken
});
}
}
showDeleteModal(id) {
// get simulation by id
var deleteSimulation;
this.state.simulations.forEach((simulation) => {
if (simulation._id === id) {
deleteSimulation = simulation;
}
});
this.setState({ deleteModal: true, modalSimulation: deleteSimulation });
}
closeDeleteModal = confirmDelete => {
this.setState({ deleteModal: false });
if (confirmDelete === false) {
return;
}
AppDispatcher.dispatch({
type: 'simulations/start-remove',
data: this.state.modalSimulation,
token: this.state.sessionToken
});
}
showEditModal(id) {
// get simulation by id
var editSimulation;
this.state.simulations.forEach((simulation) => {
if (simulation._id === id) {
editSimulation = simulation;
}
});
this.setState({ editModal: true, modalSimulation: editSimulation });
}
closeEditModal(data) {
this.setState({ editModal : false });
if (data != null) {
AppDispatcher.dispatch({
type: 'simulations/start-edit',
data,
token: this.state.sessionToken
});
}
}
closeImportModal(data) {
this.setState({ importModal: false });
if (data) {
AppDispatcher.dispatch({
type: 'simulations/start-add',
data,
token: this.state.sessionToken
});
}
}
onModalKeyPress = (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.confirmDeleteModal();
}
}
exportSimulation(index) {
// filter properties
let simulation = Object.assign({}, this.state.simulations[index]);
delete simulation._id;
delete simulation.projects;
delete simulation.running;
delete simulation.user;
simulation.models.forEach(model => {
delete model.simulator;
});
// show save dialog
const blob = new Blob([JSON.stringify(simulation, null, 2)], { type: 'application/json' });
FileSaver.saveAs(blob, 'simulation - ' + simulation.name + '.json');
}
onSimulationChecked(index, event) {
const selectedSimulations = Object.assign([], this.state.selectedSimulations);
for (let key in selectedSimulations) {
if (selectedSimulations[key] === index) {
// update existing entry
if (event.target.checked) {
return;
}
selectedSimulations.splice(key, 1);
this.setState({ selectedSimulations });
return;
}
}
// add new entry
if (event.target.checked === false) {
return;
}
selectedSimulations.push(index);
this.setState({ selectedSimulations });
}
runAction = action => {
for (let index of this.state.selectedSimulations) {
for (let model of this.state.simulations[index].models) {
// get simulation model
const simulationModel = this.state.simulationModels.find(m => m != null && m._id === model);
if (simulationModel == null) {
continue;
}
// get simulator for model
let simulator = null;
for (let sim of this.state.simulators) {
if (sim._id === simulationModel.simulator) {
simulator = sim;
}
}
if (simulator == null) {
continue;
}
if (action.data.action === 'start') {
action.data.parameters = Object.assign({}, this.state.simulations[index].startParameters, simulationModel.startParameters);
}
AppDispatcher.dispatch({
type: 'simulators/start-action',
simulator,
data: action.data,
token: this.state.sessionToken
});
}
}
}
render() {
const buttonStyle = {
marginLeft: '10px'
};
return (
<div className='section'>
<h1>Simulations</h1>
<Table data={this.state.simulations}>
<TableColumn checkbox onChecked={(index, event) => this.onSimulationChecked(index, event)} width='30' />
<TableColumn title='Name' dataKey='name' link='/simulations/' linkKey='_id' />
<TableColumn
width='100'
editButton
deleteButton
exportButton
onEdit={index => this.setState({ editModal: true, modalSimulation: this.state.simulations[index] })}
onDelete={index => this.setState({ deleteModal: true, modalSimulation: this.state.simulations[index] })}
onExport={index => this.exportSimulation(index)}
/>
</Table>
<div style={{ float: 'left' }}>
<SimulatorAction
runDisabled={this.state.selectedSimulations.length === 0}
runAction={this.runAction}
actions={[
{ id: '0', title: 'Start', data: { action: 'start' } },
{ id: '1', title: 'Stop', data: { action: 'stop' } },
{ id: '2', title: 'Pause', data: { action: 'pause' } },
{ id: '3', title: 'Resume', data: { action: 'resume' } }
]}/>
</div>
<div style={{ float: 'right' }}>
<Button onClick={() => this.setState({ newModal: true })} style={buttonStyle}><Icon icon="plus" /> Simulation</Button>
<Button onClick={() => this.setState({ importModal: true })} style={buttonStyle}><Icon icon="upload" /> Import</Button>
</div>
<div style={{ clear: 'both' }} />
<NewSimulationDialog show={this.state.newModal} onClose={(data) => this.closeNewModal(data)} />
<EditSimulationDialog show={this.state.editModal} onClose={(data) => this.closeEditModal(data)} simulation={this.state.modalSimulation} />
<ImportSimulationDialog show={this.state.importModal} onClose={data => this.closeImportModal(data)} nodes={this.state.nodes} />
<DeleteDialog title="simulation" name={this.state.modalSimulation.name} show={this.state.deleteModal} onClose={this.closeDeleteModal} />
</div>
);
}
}
export default Container.create(Simulations);

View file

@ -1,313 +0,0 @@
/**
* File: simulators.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* 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 { Container } from 'flux/utils';
import { Button } from 'react-bootstrap';
import FileSaver from 'file-saver';
import _ from 'lodash';
import AppDispatcher from '../app-dispatcher';
import SimulatorStore from '../stores/simulator-store';
import UserStore from '../stores/user-store';
import Icon from '../components/icon';
import Table from '../components/table';
import TableColumn from '../components/table-column';
import NewSimulatorDialog from '../components/dialogs/new-simulator';
import EditSimulatorDialog from '../components/dialogs/edit-simulator';
import ImportSimulatorDialog from '../components/dialogs/import-simulator';
import SimulatorAction from '../components/simulator-action';
import DeleteDialog from '../components/dialogs/delete-dialog';
class Simulators extends Component {
static getStores() {
return [ UserStore, SimulatorStore ];
}
static statePrio(state) {
switch (state) {
case 'running':
case 'starting':
return 1;
case 'paused':
case 'pausing':
case 'resuming':
return 2;
case 'idle':
return 3;
case 'shutdown':
return 4;
case 'error':
return 10;
default:
return 99;
}
}
static calculateState() {
const simulators = SimulatorStore.getState().sort((a, b) => {
if (a.state !== b.state)
return this.statePrio(a.state) > this.statePrio(b.state);
else if (a.name !== b.name)
return a.name < b.name;
else
return a.stateUpdatedAt < b.stateUpdatedAt;
});
return {
sessionToken: UserStore.getState().token,
simulators,
modalSimulator: {},
deleteModal: false,
selectedSimulators: []
};
}
componentWillMount() {
AppDispatcher.dispatch({
type: 'simulators/start-load',
token: this.state.sessionToken
});
// Start timer for periodic refresh
this.timer = window.setInterval(() => this.refresh(), 1000);
}
componentWillUnmount() {
window.clearInterval(this.timer);
}
refresh() {
AppDispatcher.dispatch({
type: 'simulators/start-load',
token: this.state.sessionToken
});
}
closeNewModal(data) {
this.setState({ newModal : false });
if (data) {
AppDispatcher.dispatch({
type: 'simulators/start-add',
data,
token: this.state.sessionToken
});
}
}
closeEditModal(data) {
this.setState({ editModal : false });
if (data) {
let simulator = this.state.simulators[this.state.modalIndex];
simulator.properties = data;
this.setState({ simulator });
AppDispatcher.dispatch({
type: 'simulators/start-edit',
data: simulator,
token: this.state.sessionToken
});
}
}
closeDeleteModal = confirmDelete => {
this.setState({ deleteModal: false });
if (confirmDelete === false) {
return;
}
AppDispatcher.dispatch({
type: 'simulators/start-remove',
data: this.state.modalSimulator,
token: this.state.sessionToken
});
}
exportSimulator(index) {
// filter properties
let simulator = Object.assign({}, this.state.simulators[index]);
delete simulator._id;
// show save dialog
const blob = new Blob([JSON.stringify(simulator, null, 2)], { type: 'application/json' });
FileSaver.saveAs(blob, 'simulator - ' + (_.get(simulator, 'properties.name') || _.get(simulator, 'rawProperties.name') || 'undefined') + '.json');
}
closeImportModal(data) {
this.setState({ importModal: false });
if (data) {
AppDispatcher.dispatch({
type: 'simulators/start-add',
data,
token: this.state.sessionToken
});
}
}
onSimulatorChecked(index, event) {
const selectedSimulators = Object.assign([], this.state.selectedSimulators);
for (let key in selectedSimulators) {
if (selectedSimulators[key] === index) {
// update existing entry
if (event.target.checked) {
return;
}
selectedSimulators.splice(key, 1);
this.setState({ selectedSimulators });
return;
}
}
// add new entry
if (event.target.checked === false) {
return;
}
selectedSimulators.push(index);
this.setState({ selectedSimulators });
}
runAction = action => {
for (let index of this.state.selectedSimulators) {
AppDispatcher.dispatch({
type: 'simulators/start-action',
simulator: this.state.simulators[index],
data: action.data,
token: this.state.sessionToken
});
}
}
isSimulatorOutdated(simulator) {
if (!simulator.stateUpdatedAt)
return true;
const fiveMinutes = 5 * 60 * 1000;
return Date.now() - new Date(simulator.stateUpdatedAt) > fiveMinutes;
}
stateLabelStyle = (state, simulator) => {
var style = [ 'label' ];
if (this.isSimulatorOutdated(simulator) && state !== 'shutdown') {
style.push('label-outdated');
}
switch (state) {
case 'running':
style.push('label-success');
break;
case 'paused':
style.push('label-info');
break;
case 'idle':
style.push('label-primary');
break;
case 'error':
style.push('label-danger');
break;
case 'shutdown':
style.push('label-warning');
break;
default:
style.push('label-default');
}
return style.join(' ');
}
stateUpdateModifier = updatedAt => {
const date = new Date(updatedAt);
return date.toLocaleString('de-DE');
}
render() {
const buttonStyle = {
marginLeft: '10px'
};
return (
<div className='section'>
<h1>Simulators</h1>
<Table data={this.state.simulators}>
<TableColumn checkbox onChecked={(index, event) => this.onSimulatorChecked(index, event)} width='30' />
<TableColumn title='Name' dataKeys={['properties.name', 'rawProperties.name']} />
<TableColumn title='State' labelKey='state' tooltipKey='error' labelModifier={this.stateLabelModifier} labelStyle={this.stateLabelStyle} />
<TableColumn title='Category' dataKeys={['properties.category', 'rawProperties.category']} />
<TableColumn title='Type' dataKeys={['properties.type', 'rawProperties.type']} />
<TableColumn title='Location' dataKeys={['properties.location', 'rawProperties.location']} />
{/* <TableColumn title='Realm' dataKeys={['properties.realm', 'rawProperties.realm']} /> */}
<TableColumn title='Host' dataKey='host' />
<TableColumn title='Last Update' dataKey='stateUpdatedAt' modifier={this.stateUpdateModifier} />
<TableColumn
width='100'
editButton
exportButton
deleteButton
onEdit={index => this.setState({ editModal: true, modalSimulator: this.state.simulators[index], modalIndex: index })}
onExport={index => this.exportSimulator(index)}
onDelete={index => this.setState({ deleteModal: true, modalSimulator: this.state.simulators[index], modalIndex: index })}
/>
</Table>
<div style={{ float: 'left' }}>
<SimulatorAction
runDisabled={this.state.selectedSimulators.length === 0}
runAction={this.runAction}
actions={[ { id: '0', title: 'Reset', data: { action: 'reset' } }, { id: '1', title: 'Shutdown', data: { action: 'shutdown' } } ]}/>
</div>
<div style={{ float: 'right' }}>
<Button onClick={() => this.setState({ newModal: true })} style={buttonStyle}><Icon icon="plus" /> Simulator</Button>
<Button onClick={() => this.setState({ importModal: true })} style={buttonStyle}><Icon icon="upload" /> Import</Button>
</div>
<div style={{ clear: 'both' }} />
<NewSimulatorDialog show={this.state.newModal} onClose={data => this.closeNewModal(data)} />
<EditSimulatorDialog show={this.state.editModal} onClose={data => this.closeEditModal(data)} simulator={this.state.modalSimulator} />
<ImportSimulatorDialog show={this.state.importModal} onClose={data => this.closeImportModal(data)} />
<DeleteDialog title="simulator" name={_.get(this.state.modalSimulator, 'properties.name') || _.get(this.state.modalSimulator, 'rawProperties.name') || 'Unknown'} show={this.state.deleteModal} onClose={this.closeDeleteModal} />
</div>
);
}
}
export default Container.create(Simulators);

View file

@ -1,550 +0,0 @@
/**
* File: visualization.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 02.03.2017
*
* 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 { Container } from 'flux/utils';
import { Button, ButtonToolbar } from 'react-bootstrap';
import { ContextMenu, Item, Separator } from 'react-contexify';
import Fullscreenable from 'react-fullscreenable';
import Slider from 'rc-slider';
import classNames from 'classnames';
import Icon from '../components/icon';
import WidgetFactory from '../components/widget-factory';
import ToolboxItem from '../components/toolbox-item';
import Dropzone from '../components/dropzone';
import Widget from './widget';
import EditWidget from '../components/dialogs/edit-widget';
import Grid from '../components/grid';
import UserStore from '../stores/user-store';
import VisualizationStore from '../stores/visualization-store';
import ProjectStore from '../stores/project-store';
import SimulationStore from '../stores/simulation-store';
import SimulationModelStore from '../stores/simulation-model-store';
import FileStore from '../stores/file-store';
import AppDispatcher from '../app-dispatcher';
import NotificationsDataManager from '../data-managers/notifications-data-manager';
import NotificationsFactory from '../data-managers/notifications-factory';
import 'react-contexify/dist/ReactContexify.min.css';
class Visualization extends React.Component {
static getStores() {
return [ VisualizationStore, ProjectStore, SimulationStore, SimulationModelStore, FileStore, UserStore ];
}
static calculateState(prevState, props) {
if (prevState == null) {
prevState = {};
}
let simulationModels = [];
if (prevState.simulation != null) {
simulationModels = SimulationModelStore.getState().filter(m => prevState.simulation.models.includes(m._id));
}
return {
sessionToken: UserStore.getState().token,
visualizations: VisualizationStore.getState(),
projects: ProjectStore.getState(),
simulations: SimulationStore.getState(),
files: FileStore.getState(),
visualization: prevState.visualization || {},
project: prevState.project || null,
simulation: prevState.simulation || null,
simulationModels,
editing: prevState.editing || false,
paused: prevState.paused || false,
editModal: prevState.editModal || false,
modalData: prevState.modalData || null,
modalIndex: prevState.modalIndex || null,
maxWidgetHeight: prevState.maxWidgetHeight || 0,
dropZoneHeight: prevState.dropZoneHeight || 0
};
}
componentWillMount() {
// TODO: Don't fetch token from local, use user-store!
const token = localStorage.getItem('token');
//document.addEventListener('keydown', this.handleKeydown.bind(this));
AppDispatcher.dispatch({
type: 'visualizations/start-load',
token
});
}
componentWillUnmount() {
//document.removeEventListener('keydown', this.handleKeydown.bind(this));
}
componentDidUpdate() {
if (this.state.visualization._id !== this.props.match.params.visualization) {
this.reloadVisualization();
}
// load depending project
if (this.state.project == null && this.state.projects) {
this.state.projects.forEach((project) => {
if (project._id === this.state.visualization.project) {
this.setState({ project: project, simulation: null });
const token = localStorage.getItem('token');
AppDispatcher.dispatch({
type: 'simulations/start-load',
data: project.simulation,
token
});
}
});
}
// load depending simulation
if (this.state.simulation == null && this.state.simulations && this.state.project) {
this.state.simulations.forEach((simulation) => {
if (simulation._id === this.state.project.simulation) {
this.setState({ simulation: simulation });
}
});
}
}
/*handleKeydown(e) {
switch (e.key) {
case ' ':
case 'p':
this.setState({ paused: !this.state.paused });
break;
case 'e':
this.setState({ editing: !this.state.editing });
break;
case 'f':
this.props.toggleFullscreen();
break;
default:
}
}*/
transformToWidgetsDict(widgets) {
var widgetsDict = {};
// Create a new key and make a copy of the widget object
var key = 0;
widgets.forEach( (widget) => widgetsDict[key++] = Object.assign({}, widget) );
return widgetsDict;
}
transformToWidgetsList(widgets) {
return Object.keys(widgets).map( (key) => widgets[key]);
}
reloadVisualization() {
// select visualization by param id
this.state.visualizations.forEach((tempVisualization) => {
if (tempVisualization._id === this.props.match.params.visualization) {
// convert widgets list to a dictionary
var visualization = Object.assign({}, tempVisualization, {
widgets: tempVisualization.widgets ? this.transformToWidgetsDict(tempVisualization.widgets) : {}
});
this.computeHeightWithWidgets(visualization.widgets);
this.setState({ visualization: visualization, project: null });
const token = localStorage.getItem('token');
AppDispatcher.dispatch({
type: 'projects/start-load',
data: visualization.project,
token
});
}
});
}
snapToGrid(value) {
if (this.state.visualization.grid === 1) return value;
return Math.round(value / this.state.visualization.grid) * this.state.visualization.grid;
}
handleDrop(item, position) {
let widget = null;
let defaultSimulationModel = null;
if (this.state.simulation.models && this.state.simulation.models.length === 0) {
NotificationsDataManager.addNotification(NotificationsFactory.NO_SIM_MODEL_AVAILABLE);
} else {
defaultSimulationModel = this.state.simulation.models[0];
}
// snap position to grid
position.x = this.snapToGrid(position.x);
position.y = this.snapToGrid(position.y);
// create new widget
widget = WidgetFactory.createWidgetOfType(item.name, position, defaultSimulationModel);
var new_widgets = this.state.visualization.widgets;
var new_key = Object.keys(new_widgets).length;
new_widgets[new_key] = widget;
var visualization = Object.assign({}, this.state.visualization, {
widgets: new_widgets
});
this.increaseHeightWithWidget(widget);
this.setState({ visualization: visualization });
}
widgetStatusChange(updated_widget, key) {
// Widget changed internally, make changes effective then save them
this.widgetChange(updated_widget, key, this.saveChanges);
}
widgetChange(updated_widget, key, callback = null) {
var widgets_update = {};
widgets_update[key] = updated_widget;
var new_widgets = Object.assign({}, this.state.visualization.widgets, widgets_update);
var visualization = Object.assign({}, this.state.visualization, {
widgets: new_widgets
});
// Check if the height needs to be increased, the section may have shrunk if not
if (!this.increaseHeightWithWidget(updated_widget)) {
this.computeHeightWithWidgets(visualization.widgets);
}
this.setState({ visualization: visualization }, callback);
}
/*
* Set the initial height state based on the existing widgets
*/
computeHeightWithWidgets(widgets) {
// Compute max height from widgets
let maxHeight = Object.keys(widgets).reduce( (maxHeightSoFar, widgetKey) => {
let thisWidget = widgets[widgetKey];
let thisWidgetHeight = thisWidget.y + thisWidget.height;
return thisWidgetHeight > maxHeightSoFar? thisWidgetHeight : maxHeightSoFar;
}, 0);
this.setState({
maxWidgetHeight: maxHeight,
dropZoneHeight: maxHeight + 80
});
}
/*
* Adapt the area's height with the position of the new widget.
* Return true if the height increased, otherwise false.
*/
increaseHeightWithWidget(widget) {
let increased = false;
let thisWidgetHeight = widget.y + widget.height;
if (thisWidgetHeight > this.state.maxWidgetHeight) {
increased = true;
this.setState({
maxWidgetHeight: thisWidgetHeight,
dropZoneHeight: thisWidgetHeight + 40
});
}
return increased;
}
editWidget(e, data) {
this.setState({ editModal: true, modalData: this.state.visualization.widgets[data.key], modalIndex: data.key });
}
closeEdit(data) {
if (data) {
// save changes temporarily
var widgets_update = {};
widgets_update[this.state.modalIndex] = data;
var new_widgets = Object.assign({}, this.state.visualization.widgets, widgets_update);
var visualization = Object.assign({}, this.state.visualization, {
widgets: new_widgets
});
this.setState({ editModal: false, visualization: visualization });
} else {
this.setState({ editModal: false });
}
}
deleteWidget(e, data) {
delete this.state.visualization.widgets[data.key];
var visualization = Object.assign({}, this.state.visualization, {
widgets: this.state.visualization.widgets
});
this.setState({ visualization: visualization });
}
stopEditing() {
// Provide the callback so it can be called when state change is applied
this.setState({ editing: false }, this.saveChanges );
}
saveChanges() {
// Transform to a list
var visualization = Object.assign({}, this.state.visualization, {
widgets: this.transformToWidgetsList(this.state.visualization.widgets)
});
const token = localStorage.getItem('token');
AppDispatcher.dispatch({
type: 'visualizations/start-edit',
data: visualization,
token
});
}
discardChanges() {
this.setState({ editing: false, visualization: {} });
this.reloadVisualization();
}
moveWidget(e, data, applyDirection) {
var widget = this.state.visualization.widgets[data.key];
var updated_widgets = {};
updated_widgets[data.key] = applyDirection(widget);
var new_widgets = Object.assign({}, this.state.visualization.widgets, updated_widgets);
var visualization = Object.assign({}, this.state.visualization, {
widgets: new_widgets
});
this.setState({ visualization: visualization });
}
moveAbove(widget) {
// increase z-Order
widget.z++;
return widget;
}
moveToFront(widget) {
// increase z-Order
widget.z = 100;
return widget;
}
moveUnderneath(widget) {
// decrease z-Order
widget.z--;
if (widget.z < 0) {
widget.z = 0;
}
return widget;
}
moveToBack(widget) {
// increase z-Order
widget.z = 0;
return widget;
}
setGrid(value) {
// value 0 would block all widgets, set 1 as 'grid disabled'
if (value === 0) {
value = 1;
}
let visualization = Object.assign({}, this.state.visualization, {
grid: value
});
this.setState({ visualization });
}
lockWidget(data) {
// lock the widget
let widget = this.state.visualization.widgets[data.key];
widget.locked = true;
// update visualization
let widgets = {};
widgets[data.key] = widget;
widgets = Object.assign({}, this.state.visualization.widgets, widgets);
const visualization = Object.assign({}, this.state.visualization, { widgets });
this.setState({ visualization });
}
unlockWidget(data) {
// lock the widget
let widget = this.state.visualization.widgets[data.key];
widget.locked = false;
// update visualization
let widgets = {};
widgets[data.key] = widget;
widgets = Object.assign({}, this.state.visualization.widgets, widgets);
const visualization = Object.assign({}, this.state.visualization, { widgets });
this.setState({ visualization });
}
pauseData = () => {
this.setState({ paused: true });
}
unpauseData = () => {
this.setState({ paused: false });
}
render() {
const current_widgets = this.state.visualization.widgets;
let boxClasses = classNames('section', 'box', { 'fullscreen-container': this.props.isFullscreen });
let buttons = []
let editingControls = [];
let gridControl = {};
if (this.state.editing) {
buttons.push({ click: () => this.stopEditing(), icon: 'save', text: 'Save' });
buttons.push({ click: () => this.discardChanges(), icon: 'ban', text: 'Cancel' });
gridControl = <div key={editingControls.length}>
<span>Grid: {this.state.visualization.grid > 1 ? this.state.visualization.grid : 'Disabled'}</span>
<Slider value={this.state.visualization.grid} style={{ width: '80px' }} step={5} onChange={value => this.setGrid(value)} />
</div>
}
if (!this.props.isFullscreen) {
buttons.push({ click: this.props.toggleFullscreen, icon: 'expand', text: 'Fullscreen' });
buttons.push({ click: this.state.paused ? this.unpauseData : this.pauseData, icon: this.state.paused ? 'play' : 'pause', text: this.state.paused ? 'Live' : 'Pause' });
if (!this.state.editing)
buttons.push({ click: () => this.setState({ editing: true }), icon: 'edit', text: 'Edit' });
}
const buttonList = buttons.map((btn, idx) =>
<Button key={idx} bsStyle="info" onClick={btn.click} style={{ marginLeft: '8px' }}>
<Icon icon={btn.icon} /> {btn.text}
</Button>
);
// Only one topology widget at the time is supported
let thereIsTopologyWidget = current_widgets && Object.values(current_widgets).filter( widget => widget.type === 'Topology').length > 0;
let topologyItemMsg = !thereIsTopologyWidget? '' : 'Currently only one is supported';
return (
<div className={boxClasses} >
<div className='section-header box-header'>
<div className="section-title">
<span>{this.state.visualization.name}</span>
</div>
<div className="section-buttons-group-right">
{ this.state.editing && gridControl }
{ buttonList }
</div>
</div>
<div className="box box-content" onContextMenu={ (e) => e.preventDefault() }>
{this.state.editing &&
<div className="toolbar">
<ButtonToolbar className="section-buttons-group-right">
{ editingControls }
</ButtonToolbar>
<ButtonToolbar className="toolbox box-header">
<ToolboxItem icon="star" name="CustomAction" type="widget" />
<ToolboxItem icon="play" name="Action" type="widget" disabled={true} />
<ToolboxItem icon="lightbulb" name="Lamp" type="widget" />
<ToolboxItem icon="font" name="Value" type="widget" />
<ToolboxItem icon="chart-area" name="Plot" type="widget" />
<ToolboxItem icon="table" name="Table" type="widget" />
<ToolboxItem icon="tag" name="Label" type="widget" />
<ToolboxItem icon="image" name="Image" type="widget" />
<ToolboxItem icon="table" name="PlotTable" type="widget" />
<ToolboxItem icon="dot-circle" name="Button" type="widget" />
<ToolboxItem icon="i-cursor" name="Input" type="widget" />
<ToolboxItem icon="sliders-h" name="Slider" type="widget" />
<ToolboxItem icon="tachometer-alt" name="Gauge" type="widget" />
<ToolboxItem icon="square" name="Box" type="widget" />
<ToolboxItem icon="code" name="HTML" type="html" />
<ToolboxItem icon="project-diagram" name="Topology" type="widget" disabled={thereIsTopologyWidget} title={topologyItemMsg}/>
</ButtonToolbar>
</div>
}
<Dropzone height={this.state.dropZoneHeight} onDrop={(item, position) => this.handleDrop(item, position)} editing={this.state.editing}>
{current_widgets != null &&
Object.keys(current_widgets).map(widget_key => (
<Widget
key={widget_key}
data={current_widgets[widget_key]}
simulation={this.state.simulation}
onWidgetChange={(w, k) => this.widgetChange(w, k)}
onWidgetStatusChange={(w, k) => this.widgetStatusChange(w, k)}
editing={this.state.editing}
index={widget_key}
grid={this.state.visualization.grid}
paused={this.state.paused}
/>
))}
<Grid size={this.state.visualization.grid} disabled={this.state.visualization.grid === 1 || !this.state.editing} />
</Dropzone>
{current_widgets != null &&
Object.keys(current_widgets).map(widget_key => {
const data = { key: widget_key };
const locked = this.state.visualization.widgets[widget_key].locked;
const disabledMove = locked || this.state.visualization.widgets[widget_key].type === 'Box';
return <ContextMenu style={{zIndex: 100}} id={'widgetMenu'+ widget_key} key={widget_key}>
<Item disabled={locked} onClick={e => this.editWidget(e, data)}>Edit</Item>
<Item disabled={locked} onClick={e => this.deleteWidget(e, data)}>Delete</Item>
<Separator />
<Item disabled={disabledMove} onClick={e => this.moveWidget(e, data, this.moveAbove)}>Move above</Item>
<Item disabled={disabledMove} onClick={e => this.moveWidget(e, data, this.moveToFront)}>Move to front</Item>
<Item disabled={disabledMove} onClick={e => this.moveWidget(e, data, this.moveUnderneath)}>Move underneath</Item>
<Item disabled={disabledMove} onClick={e => this.moveWidget(e, data, this.moveToBack)}>Move to back</Item>
<Separator />
<Item disabled={locked} onClick={e => this.lockWidget(data)}>Lock</Item>
<Item disabled={!locked} onClick={e => this.unlockWidget(data)}>Unlock</Item>
</ContextMenu>
})}
<EditWidget sessionToken={this.state.sessionToken} show={this.state.editModal} onClose={(data) => this.closeEdit(data)} widget={this.state.modalData} simulationModels={this.state.simulationModels} files={this.state.files} />
</div>
</div>
);
}
}
export default Fullscreenable()(Container.create(Visualization, { withProps: true }));

View file

@ -1,297 +0,0 @@
/**
* File: widget.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 04.03.2017
*
* 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 { Container } from 'flux/utils';
import { ContextMenuProvider } from 'react-contexify';
import Rnd from 'react-rnd';
import classNames from 'classnames';
import AppDispatcher from '../app-dispatcher';
import UserStore from '../stores/user-store';
import SimulatorDataStore from '../stores/simulator-data-store';
import SimulationModelStore from '../stores/simulation-model-store';
import FileStore from '../stores/file-store';
import WidgetCustomAction from '../components/widgets/custom-action';
import WidgetAction from '../components/widgets/action';
import WidgetLamp from '../components/widgets/lamp';
import WidgetValue from '../components/widgets/value';
import WidgetPlot from '../components/widgets/plot';
import WidgetTable from '../components/widgets/table';
import WidgetLabel from '../components/widgets/label';
import WidgetPlotTable from '../components/widgets/plot-table';
import WidgetImage from '../components/widgets/image';
import WidgetButton from '../components/widgets/button';
import WidgetInput from '../components/widgets/input';
import WidgetSlider from '../components/widgets/slider';
import WidgetGauge from '../components/widgets/gauge';
import WidgetBox from '../components/widgets/box';
import WidgetHTML from '../components/widgets/html';
import WidgetTopology from '../components/widgets/topology';
import '../styles/widgets.css';
class Widget extends React.Component {
static getStores() {
return [ SimulatorDataStore, SimulationModelStore, FileStore, UserStore ];
}
static calculateState(prevState, props) {
const sessionToken = UserStore.getState().token;
let simulatorData = {};
if (props.paused) {
if (prevState && prevState.simulatorData) {
simulatorData = JSON.parse(JSON.stringify(prevState.simulatorData));
}
} else {
simulatorData = SimulatorDataStore.getState();
}
if (prevState) {
return {
sessionToken,
simulatorData,
files: FileStore.getState(),
sequence: prevState.sequence + 1,
simulationModels: SimulationModelStore.getState()
};
} else {
return {
sessionToken,
simulatorData,
files: FileStore.getState(),
sequence: 0,
simulationModels: SimulationModelStore.getState()
};
}
}
constructor(props) {
super(props);
// Reference to the context menu element
this.contextMenuTriggerViaDraggable = null;
}
componentWillMount() {
// If loading for the first time
if (this.state.sessionToken) {
AppDispatcher.dispatch({
type: 'files/start-load',
token: this.state.sessionToken
});
AppDispatcher.dispatch({
type: 'simulationModels/start-load',
token: this.state.sessionToken
});
}
}
snapToGrid(value) {
if (this.props.grid === 1)
return value;
return Math.round(value / this.props.grid) * this.props.grid;
}
drag(event, data) {
const x = this.snapToGrid(data.x);
const y = this.snapToGrid(data.y);
if (x !== data.x || y !== data.y) {
this.rnd.updatePosition({ x, y });
}
}
dragStop(event, data) {
// update widget
let widget = this.props.data;
widget.x = this.snapToGrid(data.x);
widget.y = this.snapToGrid(data.y);
this.props.onWidgetChange(widget, this.props.index);
}
resizeStop(direction, delta, event) {
// update widget
let widget = Object.assign({}, this.props.data);
// resize depends on direction
if (direction === 'left' || direction === 'topLeft' || direction === 'bottomLeft') {
widget.x -= delta.width;
}
if (direction === 'top' || direction === 'topLeft' || direction === 'topRight') {
widget.y -= delta.height;
}
widget.width += delta.width;
widget.height += delta.height;
this.props.onWidgetChange(widget, this.props.index);
}
borderWasClicked(e) {
// check if it was triggered by the right button
if (e.button === 2) {
// launch the context menu using the reference
if(this.contextMenuTriggerViaDraggable) {
this.contextMenuTriggerViaDraggable.handleContextClick(e);
}
}
}
inputDataChanged(widget, data) {
let simulationModel = null;
for (let model of this.state.simulationModels) {
if (model._id !== widget.simulationModel) {
continue;
}
simulationModel = model;
}
AppDispatcher.dispatch({
type: 'simulatorData/inputChanged',
simulator: simulationModel.simulator,
signal: widget.signal,
data
});
}
render() {
// configure grid
const grid = [this.props.grid, this.props.grid];
// get widget element
const widget = this.props.data;
let borderedWidget = false;
let element = null;
let zIndex = Number(widget.z);
let simulationModel = null;
for (let model of this.state.simulationModels) {
if (model._id !== widget.simulationModel) {
continue;
}
simulationModel = model;
}
// dummy is passed to widgets to keep updating them while in edit mode
if (widget.type === 'CustomAction') {
element = <WidgetCustomAction widget={widget} data={this.state.simulatorData} dummy={this.state.sequence} simulationModel={simulationModel} />
} else if (widget.type === 'Action') {
element = <WidgetAction widget={widget} data={this.state.simulatorData} dummy={this.state.sequence} simulationModel={simulationModel} />
} else if (widget.type === 'Lamp') {
element = <WidgetLamp widget={widget} data={this.state.simulatorData} dummy={this.state.sequence} simulationModel={simulationModel} />
} else if (widget.type === 'Value') {
element = <WidgetValue widget={widget} data={this.state.simulatorData} dummy={this.state.sequence} simulationModel={simulationModel} />
} else if (widget.type === 'Plot') {
element = <WidgetPlot widget={widget} data={this.state.simulatorData} dummy={this.state.sequence} simulationModel={simulationModel} paused={this.props.paused} />
} else if (widget.type === 'Table') {
element = <WidgetTable widget={widget} data={this.state.simulatorData} dummy={this.state.sequence} simulationModel={simulationModel} />
} else if (widget.type === 'Label') {
element = <WidgetLabel widget={widget} />
} else if (widget.type === 'PlotTable') {
element = <WidgetPlotTable widget={widget} data={this.state.simulatorData} dummy={this.state.sequence} simulationModel={simulationModel} editing={this.props.editing} onWidgetChange={(w) => this.props.onWidgetStatusChange(w, this.props.index)} paused={this.props.paused} />
} else if (widget.type === 'Image') {
element = <WidgetImage widget={widget} files={this.state.files} token={this.state.sessionToken} />
} else if (widget.type === 'Button') {
element = <WidgetButton widget={widget} editing={this.props.editing} simulationModel={simulationModel} onInputChanged={(value) => this.inputDataChanged(widget, value)} />
} else if (widget.type === 'Input') {
element = <WidgetInput widget={widget} editing={this.props.editing} simulationModel={simulationModel} onInputChanged={(value) => this.inputDataChanged(widget, value)} />
} else if (widget.type === 'Slider') {
element = <WidgetSlider widget={widget} editing={this.props.editing} simulationModel={simulationModel} onInputChanged={(value) => this.inputDataChanged(widget, value)} onWidgetChange={(w) => this.props.onWidgetStatusChange(w, this.props.index) } />
} else if (widget.type === 'Gauge') {
element = <WidgetGauge widget={widget} data={this.state.simulatorData} editing={this.props.editing} simulationModel={simulationModel} />
} else if (widget.type === 'Box') {
element = <WidgetBox widget={widget} editing={this.props.editing} />
} else if (widget.type === 'HTML') {
element = <WidgetHTML widget={widget} editing={this.props.editing} />
} else if (widget.type === 'Topology') {
element = <WidgetTopology widget={widget} files={this.state.files} />
}
if (widget.type === 'Box')
zIndex = 0;
const widgetClasses = classNames({
'widget': !this.props.editing,
'editing-widget': this.props.editing,
'border': borderedWidget,
'unselectable': false,
'locked': widget.locked && this.props.editing
});
if (this.props.editing) {
const resizing = { bottom: !widget.locked, bottomLeft: !widget.locked, bottomRight: !widget.locked, left: !widget.locked, right: !widget.locked, top: !widget.locked, topLeft: !widget.locked, topRight: !widget.locked};
return (
<Rnd
ref={c => { this.rnd = c; }}
default={{ x: Number(widget.x), y: Number(widget.y), width: widget.width, height: widget.height }}
minWidth={widget.minWidth}
minHeight={widget.minHeight}
lockAspectRatio={Boolean(widget.lockAspect)}
bounds={'parent'}
className={ widgetClasses }
onResizeStart={(event, direction, ref) => this.borderWasClicked(event)}
onResizeStop={(event, direction, ref, delta) => this.resizeStop(direction, delta, event)}
onDrag={(event, data) => this.drag(event, data)}
onDragStop={(event, data) => this.dragStop(event, data)}
dragGrid={grid}
resizeGrid={grid}
z={zIndex}
enableResizing={resizing}
disableDragging={widget.locked}
>
<ContextMenuProvider className={'full'} id={'widgetMenu' + this.props.index}>
{element}
</ContextMenuProvider>
</Rnd>
);
} else {
return (
<div
className={ widgetClasses }
style={{
width: Number(widget.width), height: Number(widget.height),
left: Number(widget.x), top: Number(widget.y),
zIndex: zIndex,
position: 'absolute'
}}>
{element}
</div>
);
}
}
}
export default Container.create(Widget, { withProps: true });

View file

@ -0,0 +1,138 @@
/**
* 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 PropTypes from 'prop-types';
import { Button,OverlayTrigger, Tooltip } from 'react-bootstrap';
import Icon from "../common/icon";
class DashboardButtonGroup extends React.Component {
render() {
const buttonStyle = {
marginLeft: '12px',
height: '44px',
width : '35px'
};
const iconStyle = {
color: '#007bff',
height: '25px',
width : '25px'
}
const buttons = [];
let key = 0;
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} />
</Button>
</OverlayTrigger>,
<OverlayTrigger key={key++} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"cancel"}`}> Discard changes </Tooltip>} >
<Button key={key} variant= 'light' size="lg" onClick={this.props.onCancel} style={buttonStyle}>
<Icon icon="times" style={iconStyle}/>
</Button>
</OverlayTrigger>
);
} else {
if (this.props.fullscreen !== true) {
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}/>
</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}/>
</Button>
</OverlayTrigger>
);
} else {
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}/>
</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>
);
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>
);
}
return <div className='section-buttons-group-right'>
{buttons}
</div>;
}
}
DashboardButtonGroup.propTypes = {
editing: PropTypes.bool,
fullscreen: PropTypes.bool,
paused: PropTypes.bool,
onEdit: PropTypes.func,
onSave: PropTypes.func,
onCancel: PropTypes.func,
onFullscreen: PropTypes.func,
onPause: PropTypes.func,
onUnpause: PropTypes.func
};
export default DashboardButtonGroup;

View file

@ -0,0 +1,57 @@
/**
* 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 DashboardsDataManager from './dashboards-data-manager';
class DashboardStore extends ArrayStore {
constructor() {
super('dashboards', DashboardsDataManager);
}
reduce(state, action) {
switch (action.type) {
case 'dashboards/start-add':
// Check if this is a recursive dashboard import or not
if (action.data.hasOwnProperty("widgets")) {
// import
let subObjects = []
let widgets = {}
widgets["widgets"] = action.data.widgets
subObjects.push(widgets)
delete action.data.widgets; // remove widgets from dashboard object
// action.data should now contain the dashboard and no sub-objects
// sub-objects are treated in add method of RestDataManager
this.dataManager.add(action.data, action.token,action.param, subObjects);
return state
} else {
// no import
return super.reduce(state, action);
}
default:
return super.reduce(state, action);
}
}
}
export default new DashboardStore();

607
src/dashboard/dashboard.js Normal file
View file

@ -0,0 +1,607 @@
/**
* 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 { Container } from 'flux/utils';
import Fullscreenable from 'react-fullscreenable';
import classNames from 'classnames';
import EditWidget from '../widget/edit-widget/edit-widget';
import EditFiles from '../file/edit-files';
import EditSignalMapping from "../signal/edit-signal-mapping";
import WidgetContextMenu from '../widget/widget-context-menu';
import WidgetToolbox from '../widget/widget-toolbox';
import WidgetArea from '../widget/widget-area';
import DashboardButtonGroup from './dashboard-button-group';
import DashboardStore from './dashboard-store';
import SignalStore from '../signal/signal-store'
import FileStore from '../file/file-store';
import WidgetStore from '../widget/widget-store';
import ICStore from '../ic/ic-store'
import ConfigStore from '../componentconfig/config-store'
import AppDispatcher from '../common/app-dispatcher';
import 'react-contexify/dist/ReactContexify.min.css';
import WidgetContainer from '../widget/widget-container';
import EditableWidgetContainer from '../widget/editable-widget-container';
class Dashboard extends Component {
static lastWidgetKey = 0;
static webSocketsOpened = false;
static getStores() {
return [DashboardStore, FileStore, WidgetStore, SignalStore, ConfigStore, ICStore];
}
static calculateState(prevState, props) {
if (prevState == null) {
prevState = {};
}
const sessionToken = localStorage.getItem("token");
let dashboard = DashboardStore.getState().find(d => d.id === parseInt(props.match.params.dashboard, 10));
if (dashboard == null) {
AppDispatcher.dispatch({
type: 'dashboards/start-load',
data: props.match.params.dashboard,
token: sessionToken
});
}
// obtain all widgets of this dashboard
let widgets = WidgetStore.getState().filter(w => w.dashboardID === parseInt(props.match.params.dashboard, 10));
// compute max y coordinate
let maxHeight = null;
maxHeight = Object.keys(widgets).reduce((maxHeightSoFar, widgetKey) => {
let thisWidget = widgets[widgetKey];
let thisWidgetHeight = thisWidget.y + thisWidget.height;
return thisWidgetHeight > maxHeightSoFar ? thisWidgetHeight : maxHeightSoFar;
}, 0);
// filter component configurations to the ones that belong to this scenario
let configs = [];
let files = [];
if (dashboard !== undefined) {
configs = ConfigStore.getState().filter(config => config.scenarioID === dashboard.scenarioID);
files = FileStore.getState().filter(file => file.scenarioID === dashboard.scenarioID);
if (dashboard.height === 0) {
dashboard.height = 400;
}
else if (maxHeight + 80 > dashboard.height) {
dashboard.height = maxHeight + 80;
}
}
// filter signals to the ones belonging to the scenario at hand
let signals = []
let allSignals = SignalStore.getState();
let sig, con;
for (sig of allSignals) {
for (con of configs) {
if (sig.configID === con.id) {
signals.push(sig);
}
}
}
// filter ICs to the ones used by this scenario
let ics = []
if (configs.length > 0) {
ics = ICStore.getState().filter(ic => {
let ICused = false;
for (let config of configs) {
if (ic.id === config.icID) {
ICused = true;
break;
}
}
return ICused;
});
}
let editOutputSignalsModal = prevState.editOutputSignalsModal;
let editInputSignalsModal = prevState.editInputSignalsModal;
return {
dashboard,
widgets,
signals,
sessionToken,
files,
configs,
ics,
editing: prevState.editing || false,
paused: prevState.paused || false,
editModal: prevState.editModal || false,
editOutputSignalsModal: editOutputSignalsModal,
editInputSignalsModal: editInputSignalsModal,
filesEditModal: prevState.filesEditModal || false,
filesEditSaveState: prevState.filesEditSaveState || [],
modalData: null,
modalIndex: null,
widgetChangeData: [],
widgetOrigIDs: prevState.widgetOrigIDs || [],
maxWidgetHeight: maxHeight || null,
};
}
static getNewWidgetKey() {
const widgetKey = this.lastWidgetKey;
this.lastWidgetKey++;
return widgetKey;
}
componentDidMount() {
Dashboard.webSocketsOpened = false;
// load widgets of dashboard
AppDispatcher.dispatch({
type: 'widgets/start-load',
token: this.state.sessionToken,
param: '?dashboardID=' + parseInt(this.props.match.params.dashboard, 10),
});
// load ICs to enable that component configs and dashboards work with them
AppDispatcher.dispatch({
type: 'ics/start-load',
token: this.state.sessionToken
});
}
componentDidUpdate(prevProps: Readonly<P>, prevState: Readonly<S>, snapshot: SS) {
// open web sockets if ICs are already known and sockets are not opened yet
if (this.state.ics !== undefined && !Dashboard.webSocketsOpened) {
if (this.state.ics.length > 0) {
console.log("Starting to open IC websockets:", this.state.ics);
AppDispatcher.dispatch({
type: 'ics/open-sockets',
data: this.state.ics
});
Dashboard.webSocketsOpened = true;
}
}
if (prevState.dashboard === undefined && this.state.dashboard !== undefined) {
// the dashboard was loaded, so that the scenarioID is available
// load configs of scenario
AppDispatcher.dispatch({
type: 'configs/start-load',
token: this.state.sessionToken,
param: '?scenarioID=' + this.state.dashboard.scenarioID
});
// load files of scenario
AppDispatcher.dispatch({
type: 'files/start-load',
param: '?scenarioID=' + this.state.dashboard.scenarioID,
token: this.state.sessionToken
});
}
}
componentWillUnmount() {
// close web sockets of ICs
console.log("Starting to close all web sockets");
AppDispatcher.dispatch({
type: 'ics/close-sockets',
});
}
handleKeydown(e) {
switch (e.key) {
case ' ':
case 'p':
this.setState({ paused: !this.state.paused });
break;
case 'e':
this.setState({ editing: !this.state.editing });
break;
case 'f':
this.props.toggleFullscreen();
break;
default:
}
}
transformToWidgetsList(widgets) {
return Object.keys(widgets).map((key) => widgets[key]);
}
handleDrop(widget) {
widget.dashboardID = this.state.dashboard.id;
AppDispatcher.dispatch({
type: 'widgets/start-add',
token: this.state.sessionToken,
data: widget
});
};
widgetStatusChange(updated_widget, key) {
// Widget changed internally, make changes effective then save them
this.widgetChange(updated_widget, key, this.saveChanges);
}
widgetChange(widget, index, callback = null) {
let temp = this.state.widgetChangeData;
temp.push(widget);
this.setState({ widgetChangeData: temp });
}
editWidget(widget, index) {
this.setState({ editModal: true, modalData: widget, modalIndex: index });
};
duplicateWidget(widget) {
let widgetCopy = JSON.parse(JSON.stringify(widget));
delete widgetCopy.id;
widgetCopy.x = widgetCopy.x + 50;
widgetCopy.y = widgetCopy.y + 50;
AppDispatcher.dispatch({
type: 'widgets/start-add',
token: this.state.sessionToken,
data: widgetCopy
});
};
startEditFiles() {
let tempFiles = [];
this.state.files.forEach(file => {
tempFiles.push({
id: file.id,
name: file.name
});
})
this.setState({ filesEditModal: true, filesEditSaveState: tempFiles });
}
closeEditFiles() {
this.state.widgets.map(widget => {
if(widget.type === "Image"){
widget.customProperties.update = true;
}
})
this.setState({ filesEditModal: false });
}
closeEdit(data) {
if (data == null) {
AppDispatcher.dispatch({
type: 'widgets/start-load',
token: this.state.sessionToken,
param: '?dashboardID=' + this.state.dashboard.id
});
this.setState({ editModal: false });
return;
}
if(data.type === "Image")
{
data.customProperties.update = true;
}
AppDispatcher.dispatch({
type: 'widgets/start-edit',
token: this.state.sessionToken,
data: data
});
this.setState({ editModal: false });
};
deleteWidget(widget, index) {
AppDispatcher.dispatch({
type: 'widgets/start-remove',
data: widget,
token: this.state.sessionToken
});
};
startEditing() {
let originalIDs = [];
this.state.widgets.forEach(widget => {
originalIDs.push(widget.id);
if (widget.type === 'Slider' || widget.type === 'NumberInput' || widget.type === 'Button') {
AppDispatcher.dispatch({
type: 'widgets/start-edit',
token: this.state.sessionToken,
data: widget
});
}
});
this.setState({ editing: true, widgetOrigIDs: originalIDs });
};
saveEditing() {
// Provide the callback so it can be called when state change is applied
// TODO: Check if callback is needed
AppDispatcher.dispatch({
type: 'dashboards/start-edit',
data: this.state.dashboard,
token: this.state.sessionToken
});
this.state.widgetChangeData.forEach(widget => {
AppDispatcher.dispatch({
type: 'widgets/start-edit',
token: this.state.sessionToken,
data: widget
});
});
this.setState({ editing: false, widgetChangeData: [] });
};
saveChanges() {
// Transform to a list
const dashboard = Object.assign({}, this.state.dashboard.toJS(), {
widgets: this.transformToWidgetsList(this.state.widgets)
});
AppDispatcher.dispatch({
type: 'dashboards/start-edit',
data: dashboard,
token: this.state.sessionToken
});
}
cancelEditing() {
//raw widget has no id -> cannot be deleted in its original form
this.state.widgets.forEach(widget => {
let tempID = this.state.widgetOrigIDs.find(element => element === widget.id);
if (typeof tempID === 'undefined') {
AppDispatcher.dispatch({
type: 'widgets/start-remove',
data: widget,
token: this.state.sessionToken
});
}
})
AppDispatcher.dispatch({
type: 'widgets/start-load',
token: this.state.sessionToken,
param: '?dashboardID=' + this.state.dashboard.id
});
AppDispatcher.dispatch({
type: 'dashboards/start-load',
data: this.props.match.params.dashboard,
token: this.state.sessionToken
});
this.setState({ editing: false, widgetChangeData: [] });
};
setGrid(value) {
let dashboard = this.state.dashboard;
dashboard.grid = value;
this.setState({ dashboard });
this.forceUpdate();
};
setDashboardSize(value) {
const maxHeight = Object.values(this.state.widgets).reduce((currentHeight, widget) => {
const absolutHeight = widget.y + widget.height;
return absolutHeight > currentHeight ? absolutHeight : currentHeight;
}, 0);
let dashboard = this.state.dashboard;
if (value === -1) {
let tempHeight = this.state.dashboard.height - 50;
if (tempHeight >= 400 && tempHeight >= (maxHeight + 80)) {
dashboard.height = tempHeight;
this.setState({ dashboard });
}
}
else {
dashboard.height = this.state.dashboard.height + 50;
this.setState({ dashboard });
}
this.forceUpdate();
}
pauseData() {
this.setState({ paused: true });
};
unpauseData() {
this.setState({ paused: false });
};
editInputSignals() {
this.setState({ editInputSignalsModal: true });
};
editOutputSignals() {
this.setState({ editOutputSignalsModal: true });
};
closeEditSignalsModal(direction) {
if (direction === "in") {
this.setState({ editInputSignalsModal: false });
} else if (direction === "out") {
this.setState({ editOutputSignalsModal: false });
}
}
render() {
if (this.state.dashboard === undefined) {
return <div className="section-title"> <span>{"Loading Dashboard..."}</span> </div>
}
const grid = this.state.dashboard.grid;
const boxClasses = classNames('section', 'box', { 'fullscreen-padding': this.props.isFullscreen });
let draggable = this.state.editing;
let dropZoneHeight = this.state.dashboard.height;
return <div className={boxClasses} >
<div className='section-header box-header'>
<div className="section-title">
<span>{this.state.dashboard.name}</span>
</div>
<DashboardButtonGroup
editing={this.state.editing}
onEdit={this.startEditing.bind(this)}
fullscreen={this.props.isFullscreen}
paused={this.state.paused}
onSave={this.saveEditing.bind(this)}
onCancel={this.cancelEditing.bind(this)}
onFullscreen={this.props.toggleFullscreen}
onPause={this.pauseData.bind(this)}
onUnpause={this.unpauseData.bind(this)}
onEditFiles={this.startEditFiles.bind(this)}
onEditOutputSignals={this.editOutputSignals.bind(this)}
onEditInputSignals={this.editInputSignals.bind(this)}
/>
</div>
<div className="box box-content" onContextMenu={(e) => e.preventDefault()}>
{this.state.editing &&
<WidgetToolbox grid={grid} onGridChange={this.setGrid.bind(this)} dashboard={this.state.dashboard} onDashboardSizeChange={this.setDashboardSize.bind(this)} widgets={this.state.widgets} />
}
{!draggable ? (
<WidgetArea widgets={this.state.widgets} dropZoneHeight={dropZoneHeight} editing={this.state.editing} grid={grid} onWidgetAdded={this.handleDrop.bind(this)}>
{this.state.widgets != null && Object.keys(this.state.widgets).map(widgetKey => (
<WidgetContainer widget={this.state.widgets[widgetKey]}>
<WidgetContextMenu
key={widgetKey}
index={parseInt(widgetKey, 10)}
widget={this.state.widgets[widgetKey]}
onEdit={this.editWidget.bind(this)}
onDuplicate={this.duplicateWidget.bind(this)}
onDelete={this.deleteWidget.bind(this)}
onChange={this.widgetChange.bind(this)}
onWidgetChange={this.widgetChange.bind(this)}
onWidgetStatusChange={this.widgetStatusChange.bind(this)}
editing={this.state.editing}
grid={grid}
paused={this.state.paused}
/>
</WidgetContainer>
))}
</WidgetArea>
) : (
<WidgetArea widgets={this.state.widgets} editing={this.state.editing} dropZoneHeight={dropZoneHeight} grid={grid} onWidgetAdded={this.handleDrop.bind(this)}>
{this.state.widgets != null && Object.keys(this.state.widgets).map(widgetKey => (
<EditableWidgetContainer
widget={this.state.widgets[widgetKey]}
grid={grid}
index={parseInt(widgetKey, 10)}
onWidgetChange={this.widgetChange.bind(this)}>
<WidgetContextMenu
key={widgetKey}
index={parseInt(widgetKey, 10)}
widget={this.state.widgets[widgetKey]}
onEdit={this.editWidget.bind(this)}
onDuplicate={this.duplicateWidget.bind(this)}
onDelete={this.deleteWidget.bind(this)}
onChange={this.widgetChange.bind(this)}
onWidgetChange={this.widgetChange.bind(this)}
onWidgetStatusChange={this.widgetStatusChange.bind(this)}
editing={this.state.editing}
paused={this.state.paused}
/>
</EditableWidgetContainer>
))}
</WidgetArea>
)}
<EditWidget
sessionToken={this.state.sessionToken}
show={this.state.editModal}
onClose={this.closeEdit.bind(this)}
widget={this.state.modalData}
signals={this.state.signals}
files={this.state.files}
/>
<EditFiles
sessionToken={this.state.sessionToken}
show={this.state.filesEditModal}
onClose={this.closeEditFiles.bind(this)}
signals={this.state.signals}
files={this.state.files}
scenarioID={this.state.dashboard.scenarioID}
/>
<EditSignalMapping
show={this.state.editOutputSignalsModal}
onCloseEdit={(direction) => this.closeEditSignalsModal(direction)}
direction="Output"
signals={this.state.signals}
configID={null}
configs={this.state.configs}
sessionToken={this.state.sessionToken}
/>
<EditSignalMapping
show={this.state.editInputSignalsModal}
onCloseEdit={(direction) => this.closeEditSignalsModal(direction)}
direction="Input"
signals={this.state.signals}
configID={null}
configs={this.state.configs}
sessionToken={this.state.sessionToken}
/>
</div>
</div>;
}
}
let fluxContainerConverter = require('../common/FluxContainerConverter');
export default Fullscreenable()(Container.create(fluxContainerConverter.convert(Dashboard), { withProps: true }));

View file

@ -1,8 +1,4 @@
/**
* File: project-store.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 07.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -19,7 +15,30 @@
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import ArrayStore from './array-store';
import ProjectsDataManager from '../data-managers/projects-data-manager';
import RestDataManager from '../common/data-managers/rest-data-manager';
import AppDispatcher from "../common/app-dispatcher";
export default new ArrayStore('projects', ProjectsDataManager);
class DashboardsDataManager extends RestDataManager{
constructor() {
super('dashboard', '/dashboards');
this.onLoad = this.onDashboardsLoad
}
onDashboardsLoad(data, token){
if (!Array.isArray(data)) {
data = [data];
}
for (let dashboard of data){
AppDispatcher.dispatch({
type: 'widgets/start-load',
token: token,
param: '?dashboardID=' + dashboard.id
});
}
}
}
export default new DashboardsDataManager();

View file

@ -1,8 +1,4 @@
/**
* File: new-visualization.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 03.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -20,18 +16,19 @@
******************************************************************************/
import React from 'react';
import { FormGroup, FormControl, ControlLabel } from 'react-bootstrap';
import { FormGroup, FormControl, FormLabel } from 'react-bootstrap';
import Dialog from './dialog';
import Dialog from '../common/dialogs/dialog';
class NewVisualzationDialog extends React.Component {
valid: false;
class EditDashboardDialog extends React.Component {
valid = true;
constructor(props) {
super(props);
this.state = {
name: ''
name: '',
id: ''
}
}
@ -50,7 +47,10 @@ class NewVisualzationDialog extends React.Component {
}
resetState() {
this.setState({ name: '' });
this.setState({
name: this.props.dashboard.name,
id: this.props.dashboard.id
});
}
validateForm(target) {
@ -71,10 +71,10 @@ class NewVisualzationDialog extends React.Component {
render() {
return (
<Dialog show={this.props.show} title="New Visualization" buttonTitle="Add" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<Dialog show={this.props.show} title="Edit Dashboard" buttonTitle="Save" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<form>
<FormGroup controlId="name" validationState={this.validateForm('name')}>
<ControlLabel>Name</ControlLabel>
<FormLabel>Name</FormLabel>
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
@ -84,4 +84,4 @@ class NewVisualzationDialog extends React.Component {
}
}
export default NewVisualzationDialog;
export default EditDashboardDialog;

View file

@ -1,8 +1,4 @@
/**
* File: import-simulator.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 04.04.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -20,11 +16,11 @@
******************************************************************************/
import React from 'react';
import { FormGroup, FormControl, ControlLabel } from 'react-bootstrap';
import { FormGroup, FormControl, FormLabel } from 'react-bootstrap';
import Dialog from './dialog';
import Dialog from '../common/dialogs/dialog';
class ImportVisualizationDialog extends React.Component {
class ImportDashboardDialog extends React.Component {
valid = false;
imported = false;
@ -68,32 +64,12 @@ class ImportVisualizationDialog extends React.Component {
var self = this;
reader.onload = function(event) {
// read simulator
const visualization = JSON.parse(event.target.result);
let defaultSimulator = "";
if (self.props.simulation.models != null) {
defaultSimulator = self.props.simulation.models[0].simulator;
}
visualization.widgets.forEach(widget => {
switch (widget.type) {
case 'Value':
case 'Plot':
case 'Table':
case 'PlotTable':
case 'Gauge':
widget.simulator = defaultSimulator;
break;
default:
break;
}
});
// read IC
const dashboard = JSON.parse(event.target.result);
self.imported = true;
self.valid = true;
self.setState({ name: visualization.name, widgets: visualization.widgets, grid: visualization.grid });
self.setState({ name: dashboard.name, widgets: dashboard.widgets, grid: dashboard.grid });
};
reader.readAsText(file);
@ -110,21 +86,36 @@ class ImportVisualizationDialog extends React.Component {
this.valid = name;
// return state to control
if (target === 'name') return name ? "success" : "error";
if (target === 'name'){
return name;
}
}
render() {
return (
<Dialog show={this.props.show} title="Import Visualization" buttonTitle="Import" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<Dialog
show={this.props.show}
title="Import Dashboard"
buttonTitle="Import"
onClose={(c) => this.onClose(c)}
onReset={() => this.resetState()}
valid={this.valid}>
<form>
<FormGroup controlId="file">
<ControlLabel>Visualization File</ControlLabel>
<FormLabel>Dashboard File</FormLabel>
<FormControl type="file" onChange={(e) => this.loadFile(e.target.files)} />
</FormGroup>
<FormGroup controlId="name" validationState={this.validateForm('name')}>
<ControlLabel>Name</ControlLabel>
<FormControl readOnly={!this.imported} type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormGroup controlId="name" >
<FormLabel>Name</FormLabel>
<FormControl
readOnly={!this.imported}
isValid={this.validateForm('name')}
type="text"
placeholder="Enter name"
value={this.state.name}
onChange={(e) => this.handleChange(e)}
/>
<FormControl.Feedback />
</FormGroup>
</form>
@ -133,4 +124,4 @@ class ImportVisualizationDialog extends React.Component {
}
}
export default ImportVisualizationDialog;
export default ImportDashboardDialog;

View file

@ -1,8 +1,4 @@
/**
* File: new-visualization.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 03.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -20,19 +16,18 @@
******************************************************************************/
import React from 'react';
import { FormGroup, FormControl, ControlLabel } from 'react-bootstrap';
import { FormGroup, FormControl, FormLabel } from 'react-bootstrap';
import Dialog from './dialog';
import Dialog from '../common/dialogs/dialog';
class EditVisualizationDialog extends React.Component {
class NewDashboardDialog extends React.Component {
valid: false;
constructor(props) {
super(props);
this.state = {
name: '',
_id: ''
name: ''
}
}
@ -51,10 +46,7 @@ class EditVisualizationDialog extends React.Component {
}
resetState() {
this.setState({
name: this.props.visualization.name,
_id: this.props.visualization._id
});
this.setState({ name: '' });
}
validateForm(target) {
@ -75,10 +67,10 @@ class EditVisualizationDialog extends React.Component {
render() {
return (
<Dialog show={this.props.show} title="Edit Visualization" buttonTitle="Save" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<Dialog show={this.props.show} title="New Dashboard" buttonTitle="Add" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
<form>
<FormGroup controlId="name" validationState={this.validateForm('name')}>
<ControlLabel>Name</ControlLabel>
<FormGroup controlId="name" validationstate={this.validateForm('name')}>
<FormLabel>Name</FormLabel>
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
@ -88,4 +80,4 @@ class EditVisualizationDialog extends React.Component {
}
}
export default EditVisualizationDialog;
export default NewDashboardDialog;

View file

@ -1,149 +0,0 @@
/**
* File: rest-data-manager.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 03.03.2017
*
* 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 RestAPI from '../api/rest-api';
import AppDispatcher from '../app-dispatcher';
const API_URL = '/api/v1';
class RestDataManager {
constructor(type, url, keyFilter) {
this.url = url;
this.type = type;
this.keyFilter = keyFilter;
this.onLoad = null;
}
makeURL(part) {
return API_URL + part;
}
filterKeys(object) {
// don't change anything if no filter is set
if (this.keyFilter == null || Array.isArray(this.keyFilter) === false) {
return object;
}
// remove all keys not in the filter
Object.keys(object).filter(key => {
return this.keyFilter.indexOf(key) === -1;
}).forEach(key => {
delete object[key];
});
return object;
}
load(id, token = null) {
if (id != null) {
// load single object
RestAPI.get(this.makeURL(this.url + '/' + id), token).then(response => {
const data = this.filterKeys(response[this.type]);
AppDispatcher.dispatch({
type: this.type + 's/loaded',
data: data
});
if (this.onLoad != null) {
this.onLoad(data);
}
}).catch(error => {
AppDispatcher.dispatch({
type: this.type + 's/load-error',
error: error
});
});
} else {
// load all objects
RestAPI.get(this.makeURL(this.url), token).then(response => {
const data = response[this.type + 's'].map(element => {
return this.filterKeys(element);
});
AppDispatcher.dispatch({
type: this.type + 's/loaded',
data: data
});
if (this.onLoad != null) {
this.onLoad(data);
}
}).catch(error => {
AppDispatcher.dispatch({
type: this.type + 's/load-error',
error: error
});
});
}
}
add(object, token = null) {
var obj = {};
obj[this.type] = this.filterKeys(object);
RestAPI.post(this.makeURL(this.url), obj, token).then(response => {
AppDispatcher.dispatch({
type: this.type + 's/added',
data: response[this.type]
});
}).catch(error => {
AppDispatcher.dispatch({
type: this.type + 's/add-error',
error: error
});
});
}
remove(object, token = null) {
RestAPI.delete(this.makeURL(this.url + '/' + object._id), token).then(response => {
AppDispatcher.dispatch({
type: this.type + 's/removed',
data: response[this.type],
original: object
});
}).catch(error => {
AppDispatcher.dispatch({
type: this.type + 's/remove-error',
error: error
});
});
}
update(object, token = null) {
var obj = {};
obj[this.type] = this.filterKeys(object);
RestAPI.put(this.makeURL(this.url + '/' + object._id), obj, token).then(response => {
AppDispatcher.dispatch({
type: this.type + 's/edited',
data: response[this.type]
});
}).catch(error => {
AppDispatcher.dispatch({
type: this.type + 's/edit-error',
error: error
});
});
}
};
export default RestDataManager;

View file

@ -1,50 +0,0 @@
/**
* File: simulation-models-data-manager.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 20.04.2018
*
* 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 './rest-data-manager';
import AppDispatcher from '../app-dispatcher';
class SimulationModelDataManager extends RestDataManager {
constructor() {
super('simulationModel', '/models');
this.onLoad = this.onModelsLoad;
}
onModelsLoad(data) {
if (!Array.isArray(data))
data = [ data ];
for (let model of data)
this.loadModelData(model);
}
loadModelData(model) {
AppDispatcher.dispatch({
type: 'simulatorData/prepare',
inputLength: parseInt(model.inputLength, 10),
outputLength: parseInt(model.outputLength, 10),
id: model.simulator
});
}
}
export default new SimulationModelDataManager();

View file

@ -0,0 +1,82 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import {FormGroup, FormControl, Button, Col} from 'react-bootstrap';
import AppDispatcher from "../common/app-dispatcher";
import Dialog from '../common/dialogs/dialog';
class EditFileContent extends React.Component {
valid = true;
constructor(props) {
super(props);
this.state = {
uploadFile: null,
};
}
selectUploadFile(event) {
this.setState({ uploadFile: event.target.files[0] });
};
startEditContent(){
const formData = new FormData();
formData.append("file", this.state.uploadFile);
AppDispatcher.dispatch({
type: 'files/start-edit',
data: formData,
token: this.props.sessionToken,
id: this.props.file.id
});
this.setState({ uploadFile: null });
};
onClose = () => {
this.props.onClose();
};
render() {
return <Dialog show={this.props.show} title='Edit File Content' buttonTitle='Close' onClose={() => this.onClose()} blendOutCancel = {true} valid={true}>
<FormGroup as={Col} >
<FormControl
disabled={false}
type='file'
onChange={(event) => this.selectUploadFile(event)} />
</FormGroup>
<FormGroup as={Col} >
<Button
disabled={this.state.uploadFile === null}
onClick={() => this.startEditContent()}>
Upload
</Button>
</FormGroup>
</Dialog>;
}
}
export default EditFileContent;

175
src/file/edit-files.js Normal file
View file

@ -0,0 +1,175 @@
/**
* 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, Button, Col, ProgressBar} from 'react-bootstrap';
import Dialog from '../common/dialogs/dialog';
import AppDispatcher from "../common/app-dispatcher";
import Table from "../common/table";
import TableColumn from "../common/table-column";
import EditFileContent from "./edit-file-content";
class EditFilesDialog extends React.Component {
valid = true;
constructor(props) {
super(props);
this.state = {
uploadFile: null,
uploadProgress: 0,
editModal: false,
modalFile: {}
};
}
onClose() {
this.props.onClose();
}
selectUploadFile(event) {
this.setState({ uploadFile: event.target.files[0] });
};
startFileUpload(){
// upload file
const formData = new FormData();
formData.append("file", this.state.uploadFile);
AppDispatcher.dispatch({
type: 'files/start-upload',
data: formData,
token: this.props.sessionToken,
progressCallback: this.updateUploadProgress,
finishedCallback: this.clearProgress,
scenarioID: this.props.scenarioID,
});
this.setState({ uploadFile: null });
};
updateUploadProgress = (event) => {
this.setState({ uploadProgress: parseInt(event.percent.toFixed(), 10) });
};
clearProgress = (newFileID) => {
/*if (this.props.onChange != null) {
let event = {}
event["target"] = {}
event.target["value"] = newFileID
this.props.onChange(event);
}
*/
this.setState({ uploadProgress: 0 });
};
closeEditModal(){
this.setState({editModal: false});
}
deleteFile(index){
let file = this.props.files[index]
AppDispatcher.dispatch({
type: 'files/start-remove',
data: file,
token: this.props.sessionToken
});
}
render() {
let fileOptions = [];
if (this.props.files.length > 0){
fileOptions.push(
<option key = {0} default>Select image file</option>
)
fileOptions.push(this.props.files.map((file, index) => (
<option key={index+1} value={file.id}>{file.name}</option>
)))
} else {
fileOptions = <option disabled value style={{ display: 'none' }}>No files found, please upload one first.</option>
}
const progressBarStyle = {
marginLeft: '100px',
marginTop: '-40px'
};
return (
<Dialog show={this.props.show} title="Edit Files of scenario" buttonTitle="Close" onClose={() => this.onClose()} blendOutCancel = {true} valid={true} size = 'lg'>
<div>
<Table data={this.props.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)}
editButton
onEdit={index => this.setState({ editModal: true, modalFile: this.props.files[index] })}
/>
</Table>
<FormGroup as={Col} >
<FormControl
disabled={this.props.disabled}
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 + '%'}
style={progressBarStyle}
/>
</FormGroup>
<div style={{ clear: 'both' }} />
<EditFileContent show={this.state.editModal} onClose={(data) => this.closeEditModal(data)} sessionToken={this.props.sessionToken} file={this.state.modalFile} />
</div>
</Dialog>
);
}
}
export default EditFilesDialog;

88
src/file/file-store.js Normal file
View 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 FilesDataManager from './files-data-manager';
class FileStore extends ArrayStore {
constructor() {
super('files', FilesDataManager);
}
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
}
reduce(state, action) {
switch (action.type) {
case 'files/start-download':
FilesDataManager.download(action)
return state
case 'files/start-upload':
FilesDataManager.upload(action.data, action.token, action.progressCallback, action.finishedCallback, action.scenarioID);
return state;
case 'files/uploaded':
//console.log('ready uploaded');
return state;
case 'files/upload-error':
console.log(action.error);
return state;
case 'files/downloaded':
// in this case a file is contained in the response (no JSON)
return this.saveFile(state, action);
case 'files/start-edit':
FilesDataManager.update(action.data, action.token, action.id);
return state;
case 'files/edited':
return this.updateElements(state, [action.data]);
case this.type + '/edit-error':
return state;
default:
return super.reduce(state, action);
}
}
}
export default new FileStore();

View file

@ -1,8 +1,4 @@
/**
* File: files-data-manager.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 16.03.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
@ -19,17 +15,17 @@
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import RestDataManager from './rest-data-manager';
import RestAPI from '../api/rest-api';
import AppDispatcher from '../app-dispatcher';
import RestDataManager from '../common/data-managers/rest-data-manager';
import RestAPI from '../common/api/rest-api';
import AppDispatcher from '../common/app-dispatcher';
class FilesDataManager extends RestDataManager {
constructor() {
super('file', '/files');
}
upload(file, token = null, progressCallback = null, finishedCallback = null) {
RestAPI.upload(this.makeURL('/upload'), file, token, progressCallback).then(response => {
upload(file, token = null, progressCallback = null, finishedCallback = null, scenarioID) {
RestAPI.upload(this.makeURL(this.url), file, token, progressCallback, scenarioID).then(response => {
AppDispatcher.dispatch({
type: 'files/uploaded',
@ -38,11 +34,12 @@ class FilesDataManager extends RestDataManager {
// Trigger a files reload
AppDispatcher.dispatch({
type: 'files/start-load',
token
param: '?scenarioID=' + scenarioID,
token: token
});
if (finishedCallback) {
finishedCallback();
finishedCallback(response.file.id);
}
}).catch(error => {
AppDispatcher.dispatch({
@ -51,6 +48,38 @@ class FilesDataManager extends RestDataManager {
});
});
}
download(action){
RestAPI.download(this.makeURL(this.url), action.token, action.data).then(response => {
AppDispatcher.dispatch({
type: 'files/downloaded',
data: response,
id: action.data,
token: action.token
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'files/load-error',
error: error
});
});
}
update(file, token, id) {
RestAPI.put(this.makeURL(this.url + '/' + id), file, token).then(response => {
AppDispatcher.dispatch({
type: this.type + 's/edited',
data: response[this.type]
});
}).catch(error => {
AppDispatcher.dispatch({
type: this.type + 's/edit-error',
error: error
});
});
}
}
export default new FilesDataManager();

137
src/file/select-file.js Normal file
View file

@ -0,0 +1,137 @@
/**
* 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, Button, Col, ProgressBar } from 'react-bootstrap';
import AppDispatcher from '../common/app-dispatcher';
class SelectFile extends React.Component {
constructor() {
super();
this.state = {
uploadFile: null,
uploadProgress: 0
}
}
selectUploadFile(event) {
this.setState({ uploadFile: event.target.files[0] });
};
startFileUpload(){
// upload file
const formData = new FormData();
formData.append("file", this.state.uploadFile);
AppDispatcher.dispatch({
type: 'files/start-upload',
data: formData,
token: this.props.sessionToken,
progressCallback: this.updateUploadProgress,
finishedCallback: this.clearProgress,
scenarioID: this.props.scenarioID,
});
// TODO make sure that edit config dialog remains open after clicking "Upload" button
};
updateUploadProgress = (event) => {
this.setState({ uploadProgress: parseInt(event.percent.toFixed(), 10) });
};
clearProgress = (newFileID) => {
/*if (this.props.onChange != null) {
let event = {}
event["target"] = {}
event.target["value"] = newFileID
this.props.onChange(event);
}
this.setState({ uploadProgress: 0 });
*/
};
render() {
let fileOptions = [];
if (this.props.files.length > 0){
fileOptions.push(
<option key = {0} value={-1}>Select file</option>
)
fileOptions.push(this.props.files.map(f =>
<option key={f.id} value={f.id}>{f.name}</option>
));
} else {
fileOptions = <option >No files available</option>
}
const progressBarStyle = {
marginLeft: '100px',
marginTop: '-40px'
};
return <div>
<FormGroup>
<FormLabel sm={3} md={2}>
{this.props.name}
</FormLabel>
<FormGroup as={Col} sm={9} md={10}>
<FormControl
as="select"
value={this.props.value}
disabled={this.props.disabled}
placeholder='Select file'
onChange={(event) => this.props.onChange(event)}>
{fileOptions}
</FormControl>
</FormGroup>
</FormGroup>
<FormGroup as={Col} >
<FormControl
disabled={this.props.disabled}
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 + '%'}
style={progressBarStyle}
/>
</FormGroup>
</div>;
}
}
export default SelectFile;

210
src/ic/edit-ic.js Normal file
View file

@ -0,0 +1,210 @@
/**
* 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, FormCheck } from 'react-bootstrap';
import _ from 'lodash';
import {Collapse} from 'react-collapse';
import Dialog from '../common/dialogs/dialog';
import ParametersEditor from '../common/parameters-editor';
class EditICDialog extends React.Component {
valid = true;
constructor(props) {
super(props);
this.state = {
name: '',
websocketurl: '',
apiurl: '',
location: '',
description: '',
type: '',
category: '',
managedexternally: false,
startParameterScheme: {},
};
}
onClose(canceled) {
if (canceled === false) {
if (this.valid) {
let data = this.props.ic;
if (this.state.name != null && this.state.name !== "" && this.state.name !== this.props.ic.name) {
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;
}
if (this.state.location != null && this.state.location !== this.props.ic.location) {
data.location = this.state.location;
}
if (this.state.description != null && this.state.description !== this.props.ic.description) {
data.description = this.state.description;
}
if (this.state.type != null && this.state.type !== "" && this.state.type !== this.props.ic.type) {
data.type = this.state.type;
}
if (this.state.category != null && this.state.category !== "" && this.state.category !== this.props.ic.category) {
data.category = this.state.category;
}
if (this.state.startParameterScheme !== {}) {
data.startParameterScheme = this.state.startParameterScheme
}
data.managedexternally = this.state.managedexternally;
this.props.onClose(data);
this.setState({managedexternally: false});
}
} else {
this.props.onClose();
this.setState({managedexternally: false});
}
}
handleChange(e) {
if(e.target.id === "managedexternally"){
this.setState({ managedexternally : !this.state.managedexternally});
}
else{
this.setState({ [e.target.id]: e.target.value });
}
}
handleStartParameterSchemeChange(data) {
this.setState({ startParameterScheme: data });
}
resetState() {
this.setState({
name: this.props.ic.name,
websocketurl: this.props.ic.websocketurl,
apiurl: this.props.ic.apiurl,
type: this.props.ic.type,
location: this.props.ic.location,
description: this.props.ic.description,
category: this.props.ic.category,
managedexternally: false,
startParameterScheme: this.props.ic.startParameterScheme,
});
}
render() {
let typeOptions = [];
switch(this.state.category){
case "simulator":
typeOptions = ["dummy","generic","dpsim","rtlab","rscad"];
break;
case "controller":
typeOptions = ["kubernetes","villas-controller"];
break;
case "gateway":
typeOptions = ["villas-node","villas-relay"];
break;
case "service":
typeOptions = ["ems","custom"];
break;
case "equipment":
typeOptions = ["chroma-emulator","chroma-loads","sma-sunnyboy","fleps","sonnenbatterie"];
break;
default:
typeOptions =[];
}
return (
<Dialog
show={this.props.show}
title="Edit Infrastructure Component"
buttonTitle="Save"
onClose={(c) => this.onClose(c)}
onReset={() => this.resetState()}
valid={this.valid}
size='lg'
>
<form>
<FormLabel column={false}>UUID: {this.props.ic.uuid}</FormLabel>
<FormGroup controlId="name">
<FormLabel column={false}>Name</FormLabel>
<FormControl type="text" placeholder={this.props.ic.name} value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="category">
<FormLabel column={false}>Category</FormLabel>
<FormControl as="select" value={this.state.category} onChange={(e) => this.handleChange(e)}>
<option>simulator</option>
<option>controller</option>
<option>service</option>
<option>gateway</option>
<option>equipment</option>
</FormControl>
</FormGroup>
<FormGroup controlId="type">
<FormLabel column={false}>Type</FormLabel>
<FormControl as="select" value={this.state.type} onChange={(e) => this.handleChange(e)}>
<option default>Select type</option>
{typeOptions.map((name,index) => (
<option key={index}>{name}</option>
))}
</FormControl>
</FormGroup>
<FormGroup controlId="websocketurl">
<FormLabel column={false}>Websocket URL</FormLabel>
<FormControl type="text" placeholder={this.props.ic.websocketurl} value={this.state.websocketurl || 'http://' } onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="apiurl">
<FormLabel column={false}>API URL</FormLabel>
<FormControl type="text" placeholder={this.props.ic.apiurl} value={this.state.apiurl || 'http://' } onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="location">
<FormLabel column={false}>Location</FormLabel>
<FormControl type="text" placeholder={this.props.ic.location} value={this.state.location || '' } onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="description">
<FormLabel column={false}>Description</FormLabel>
<FormControl type="text" placeholder={this.props.ic.description} value={this.state.description || '' } onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId='startParameterScheme'>
<FormLabel column={false}>Start parameter scheme of IC</FormLabel>
<ParametersEditor
content={this.state.startParameterScheme}
disabled={false}
onChange={(data) => this.handleStartParameterSchemeChange(data)}
/>
</FormGroup>
</form>
</Dialog>
);
}
}
export default EditICDialog;

Some files were not shown because too many files have changed in this diff Show more