From 5d70a737737d63c0767e6db95e14f0a00834ac39 Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Fri, 28 Apr 2017 11:43:15 +0200 Subject: [PATCH 01/19] issues #42 and #43 --- src/api/rest-api.js | 37 +++++++++++++++++++++++++++++++++++-- src/stores/user-store.js | 18 ++++++++++++------ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/api/rest-api.js b/src/api/rest-api.js index 66e8211..6a79598 100644 --- a/src/api/rest-api.js +++ b/src/api/rest-api.js @@ -21,6 +21,36 @@ 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', + message: 'The server could not be reached. Please try again later.', + level: 'error' + }; + +const REQUEST_TIMEOUT_NOTIFICATION = { + title: 'Request timeout', + message: 'Request timed out. Please try again later.', + level: 'error' + }; + +// Check if the error was due to network failure, timeouts, etc. +// Can be used for the rest of requests +function isNetworkError(err) { + let result = false; + + // If not status nor response fields, it is a network error. TODO: Handle timeouts + if (err.status == null || err.response == null) { + result = true; + + let notification = err.timeout? REQUEST_TIMEOUT_NOTIFICATION : SERVER_NOT_REACHABLE_NOTIFICATION; + NotificationsDataManager.addNotification(notification); + } + return result; +} class RestAPI { get(url, token) { @@ -43,14 +73,17 @@ class RestAPI { post(url, body, token) { return new Promise(function (resolve, reject) { - var req = request.post(url).send(body); + 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.end(function (error, res) { if (res == null || res.status !== 200) { + + error.handled = isNetworkError(error); + reject(error); } else { resolve(JSON.parse(res.text)); diff --git a/src/stores/user-store.js b/src/stores/user-store.js index 37fc363..aeed56f 100644 --- a/src/stores/user-store.js +++ b/src/stores/user-store.js @@ -67,12 +67,18 @@ class UserStore extends ReduceStore { return Object.assign({}, state, { currentUser: null, token: null }); case 'users/login-error': - // server offline - NotificationsDataManager.addNotification({ - title: 'Server offline', - message: 'The server is offline. Please try again later.', - level: 'error' - }); + + if (action.error && !action.error.handled) { + // If it was an error and hasn't been handled, the credentials must have been wrong. + const WRONG_CREDENTIALS_NOTIFICATION = { + title: 'Incorrect credentials', + message: 'Please modify and try again.', + level: 'error' + } + NotificationsDataManager.addNotification(WRONG_CREDENTIALS_NOTIFICATION); + + } + return state; default: From 560713e90184e1cf37de56ed34acbdf6de84cff1 Mon Sep 17 00:00:00 2001 From: Markus Grigull Date: Thu, 27 Apr 2017 17:39:44 +0200 Subject: [PATCH 02/19] Add backend environment support Add proxy for development --- docker-compose.yml | 7 ++++--- nginx/villas.conf | 6 +++--- package.json | 1 + src/data-managers/rest-data-manager.js | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index afdd7a7..1f3a63e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,9 +16,10 @@ services: image: villasweb-backend links: - database + environment: + - NODE_ENV=production database: image: mongo:latest - volumes: - - /opt/database:/data/db - +# volumes: +# - /opt/database:/data/db diff --git a/nginx/villas.conf b/nginx/villas.conf index f3417de..7fd9ee9 100644 --- a/nginx/villas.conf +++ b/nginx/villas.conf @@ -7,10 +7,10 @@ server { proxy_redirect off; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - + # rewrite url to exclude /api on context broker side - rewrite ^/api/?(.*) /api/v1/$1 break; - + rewrite ^/api/?(.*) /api/$1 break; + proxy_pass http://backend:4000/; } diff --git a/package.json b/package.json index 0116f0a..81c7ea6 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "villasweb-frontend", "version": "0.1.0", "private": true, + "proxy": "http://localhost:4000", "dependencies": { "bootstrap": "^3.3.7", "classnames": "^2.2.5", diff --git a/src/data-managers/rest-data-manager.js b/src/data-managers/rest-data-manager.js index 10fb65a..f4fd82f 100644 --- a/src/data-managers/rest-data-manager.js +++ b/src/data-managers/rest-data-manager.js @@ -22,7 +22,7 @@ import RestAPI from '../api/rest-api'; import AppDispatcher from '../app-dispatcher'; -const API_URL = 'http://localhost:4000/api/v1'; +const API_URL = '/api/v1'; class RestDataManager { constructor(type, url, keyFilter) { From 2fbd71f28f62aaf63a30ad85ef72902ceb3b354e Mon Sep 17 00:00:00 2001 From: Markus Grigull Date: Fri, 28 Apr 2017 12:37:16 +0200 Subject: [PATCH 03/19] Add automated react build to docker-compose --- .dockerignore | 4 ++++ Dockerfile | 10 ++++++++++ docker-compose.yml | 21 +++++++++++++++++---- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..84eb9b1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules/ +nginx/ +doc/ +build/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b014976 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM node:latest + +RUN mkdir /react +RUN mkdir /result + +VOLUME /result + +WORKDIR /react + +CMD npm install && npm run build && cp -R /react/build/* /result/ diff --git a/docker-compose.yml b/docker-compose.yml index 1f3a63e..c41ab1f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,23 @@ version: "2" services: - frontend: + webserver: image: nginx:stable volumes: - ./nginx:/etc/nginx/conf.d/ - - ./build:/www + - website-volume:/www links: - backend ports: - "80:80" - "443:443" + restart: always + + frontend: + build: . + volumes: + - ./:/react + - website-volume:/result backend: image: villasweb-backend @@ -18,8 +25,14 @@ services: - database environment: - NODE_ENV=production + restart: always database: image: mongo:latest -# volumes: -# - /opt/database:/data/db + volumes: + - data-volume:/data/db + restart: always + +volumes: + data-volume: + website-volume: From b2728b31be7e3432789bfb325767f4cdb4bc8380 Mon Sep 17 00:00:00 2001 From: Markus Grigull Date: Fri, 28 Apr 2017 12:53:01 +0200 Subject: [PATCH 04/19] Fix for mongodb on linux --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index c41ab1f..579c30b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,7 @@ services: volumes: - data-volume:/data/db restart: always + user: mongodb volumes: data-volume: From 56f49da1bb014c68ad25947b2c230b1db5c38694 Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Tue, 2 May 2017 11:38:48 +0200 Subject: [PATCH 05/19] Issue #56: tab jump in tables --- src/components/table.js | 70 +++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/src/components/table.js b/src/components/table.js index f0b94d1..f0798c5 100644 --- a/src/components/table.js +++ b/src/components/table.js @@ -29,6 +29,7 @@ class CustomTable extends Component { constructor(props) { super(props); + this.activeInput = null; this.state = { rows: [], editCell: [ -1, -1 ] @@ -125,6 +126,23 @@ class CustomTable extends Component { this.setState({ rows: rows }); } + componentDidUpdate() { + // A cell will not be selected at initial render, hence no need to call this in 'componentDidMount' + if (this.activeInput) { + this.activeInput.focus(); + } + } + + onCellFocus(index) { + // When a cell focus is detected, update the current state in order to uncover the input element + this.setState({ editCell: [ index.cell, index.row ]}); + } + + cellLostFocus() { + // Reset cell selection state + this.setState({ editCell: [ -1, -1 ] }); + } + render() { // get children var children = this.props.children; @@ -140,23 +158,41 @@ class CustomTable extends Component { - {this.state.rows.map((row, rowIndex) => ( - - {row.map((cell, cellIndex) => ( - this.onClick(event, rowIndex, cellIndex) : () => {}}> - {(this.state.editCell[0] === cellIndex && this.state.editCell[1] === rowIndex ) ? ( - children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} /> - ) : ( - - {cell.map((element, elementIndex) => ( - {element} - ))} - - )} - - ))} - - ))} + { + this.state.rows.map((row, rowIndex) => ( + + { + row.map((cell, cellIndex) => { + + let isCellInlineEditable = children[cellIndex].props.inlineEditable === true; + + let tabIndex = isCellInlineEditable? 0 : -1; + + let evtHdls = isCellInlineEditable ? { + onCellClick: (event) => this.onClick(event, rowIndex, cellIndex), + onCellFocus: () => this.onCellFocus({cell: cellIndex, row: rowIndex}), + onCellBlur: () => this.cellLostFocus() + } : { + onCellClick: () => {}, + onCellFocus: () => {}, + onCellBlur: () => {} + }; + + return ( + {(this.state.editCell[0] === cellIndex && this.state.editCell[1] === rowIndex ) ? ( + children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} inputRef={ref => { this.activeInput = ref; }} /> + ) : ( + + {cell.map((element, elementIndex) => ( + {element} + ))} + + )} + ) + }) + } + )) + } ); From 80bd1f102eb39c7ec026af4fa14992b0986fa443 Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Tue, 2 May 2017 14:23:09 +0200 Subject: [PATCH 06/19] list users --- src/components/menu-sidebar.js | 1 + src/containers/users.js | 136 ++++++++++++++++++++++++ src/data-managers/users-data-manager.js | 14 +++ src/router.js | 3 + src/stores/user-store.js | 14 +++ 5 files changed, 168 insertions(+) create mode 100644 src/containers/users.js diff --git a/src/components/menu-sidebar.js b/src/components/menu-sidebar.js index 120065d..1782304 100644 --- a/src/components/menu-sidebar.js +++ b/src/components/menu-sidebar.js @@ -33,6 +33,7 @@ class SidebarMenu extends Component {
  • Projects
  • Simulations
  • Simulators
  • +
  • User Management
  • Logout
  • diff --git a/src/containers/users.js b/src/containers/users.js new file mode 100644 index 0000000..531f112 --- /dev/null +++ b/src/containers/users.js @@ -0,0 +1,136 @@ +/** + * File: users.js + * Author: Ricardo Hernandez-Montoya + * 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 . + ******************************************************************************/ + +import React, { Component } from 'react'; +import { Container } from 'flux/utils'; +import { Button, Modal, Glyphicon } from 'react-bootstrap'; + +import AppDispatcher from '../app-dispatcher'; +import UserStore from '../stores/user-store'; + +import Table from '../components/table'; +import TableColumn from '../components/table-column'; +import NewProjectDialog from '../components/dialog/new-project'; +import EditProjectDialog from '../components/dialog/edit-project'; + +class Projects extends Component { + static getStores() { + return [ UserStore ]; + } + + static calculateState() { + return { + users: UserStore.getState().users, + + newModal: false, + editModal: false, + deleteModal: false, + modalData: {} + }; + } + + componentWillMount() { + AppDispatcher.dispatch({ + type: 'users/start-load' + }); + } + + // closeNewModal(data) { + // this.setState({ newModal: false }); + + // if (data) { + // AppDispatcher.dispatch({ + // type: 'projects/start-add', + // data: data + // }); + // } + // } + + // confirmDeleteModal() { + // this.setState({ deleteModal: false }); + + // AppDispatcher.dispatch({ + // type: 'projects/start-remove', + // data: this.state.modalData + // }); + // } + + // closeEditModal(data) { + // this.setState({ editModal: false }); + + // if (data) { + // AppDispatcher.dispatch({ + // type: 'projects/start-edit', + // data: data + // }); + // } + // } + + // 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; + // } + + render() { + + this.state.users.map( (user) => console.log('User: %o', user)); + + return ( +
    +

    Users

    + + + + + this.setState({ editModal: true, modalData: this.state.users[index] })} onDelete={index => this.setState({ deleteModal: true, modalData: this.state.users[index] })} /> +
    + + {/* + + this.closeNewModal(data)} simulations={this.state.simulations} /> + + this.closeEditModal(data)} project={this.state.modalData} simulations={this.state.simulations} /> + + + + Delete Project + + + + Are you sure you want to delete the project '{this.state.modalData.name}'? + + + + + + + */} +
    + ); + } +} + +export default Container.create(Projects); diff --git a/src/data-managers/users-data-manager.js b/src/data-managers/users-data-manager.js index 711c68d..08c3138 100644 --- a/src/data-managers/users-data-manager.js +++ b/src/data-managers/users-data-manager.js @@ -55,6 +55,20 @@ class UsersDataManager extends RestDataManager { }); }); } + + getUsers(token) { + RestAPI.get(this.makeURL('/users'), token).then(response => { + AppDispatcher.dispatch({ + type: 'users/users-loaded', + users: response.users + }); + }).catch(error => { + AppDispatcher.dispatch({ + type: 'users/users-load-error', + error: error + }); + }); + } } export default new UsersDataManager(); diff --git a/src/router.js b/src/router.js index a74a7eb..85bbe78 100644 --- a/src/router.js +++ b/src/router.js @@ -30,6 +30,7 @@ import Simulators from './containers/simulators'; import Visualization from './containers/visualization'; import Simulations from './containers/simulations'; import Simulation from './containers/simulation'; +import Users from './containers/users'; import Login from './containers/login'; import Logout from './containers/logout'; @@ -50,6 +51,8 @@ class Root extends Component { + + diff --git a/src/stores/user-store.js b/src/stores/user-store.js index aeed56f..9acf4d2 100644 --- a/src/stores/user-store.js +++ b/src/stores/user-store.js @@ -80,6 +80,20 @@ class UserStore extends ReduceStore { } return state; + + case 'users/start-load': + console.log('Sending request'); + UsersDataManager.getUsers(state.token); + + return state; + + case 'users/users-loaded': + + return Object.assign({}, state, { users: action.users }); + + case 'users/users-load-error': + // Users couldn't be loaded. Keep same state + return state; default: return state; From 6a4c31baef0570900242468dc42c9d6df45608dc Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Tue, 2 May 2017 16:29:11 +0200 Subject: [PATCH 07/19] List users reusing array-store --- src/containers/users.js | 43 ++++++++++++----------- src/data-managers/users-data-manager.js | 6 ++-- src/stores/user-store.js | 16 +-------- src/stores/users-store.js | 45 +++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 37 deletions(-) create mode 100644 src/stores/users-store.js diff --git a/src/containers/users.js b/src/containers/users.js index 531f112..b8773be 100644 --- a/src/containers/users.js +++ b/src/containers/users.js @@ -25,20 +25,33 @@ import { Button, Modal, Glyphicon } from 'react-bootstrap'; import AppDispatcher from '../app-dispatcher'; import UserStore from '../stores/user-store'; +import UsersStore from '../stores/users-store'; import Table from '../components/table'; import TableColumn from '../components/table-column'; -import NewProjectDialog from '../components/dialog/new-project'; -import EditProjectDialog from '../components/dialog/edit-project'; +// import NewUserDialog from '../components/dialog/new-user'; +// import EditUserDialog from '../components/dialog/edit-user'; -class Projects extends Component { +class Users extends Component { static getStores() { - return [ UserStore ]; + return [ UserStore, UsersStore ]; } - static calculateState() { + static calculateState(prevState, props) { + + let tokenState = UserStore.getState().token; + + // If there is a token available and this method was called as a result of loading users + if (!prevState && tokenState) { + AppDispatcher.dispatch({ + type: 'users/start-load', + token: tokenState + }); + } + return { - users: UserStore.getState().users, + token: tokenState, + users: UsersStore.getState(), newModal: false, editModal: false, @@ -47,18 +60,12 @@ class Projects extends Component { }; } - componentWillMount() { - AppDispatcher.dispatch({ - type: 'users/start-load' - }); - } - // closeNewModal(data) { // this.setState({ newModal: false }); // if (data) { // AppDispatcher.dispatch({ - // type: 'projects/start-add', + // type: 'users/start-add', // data: data // }); // } @@ -95,8 +102,6 @@ class Projects extends Component { // } render() { - - this.state.users.map( (user) => console.log('User: %o', user)); return (
    @@ -108,11 +113,11 @@ class Projects extends Component { this.setState({ editModal: true, modalData: this.state.users[index] })} onDelete={index => this.setState({ deleteModal: true, modalData: this.state.users[index] })} /> - {/* + - this.closeNewModal(data)} simulations={this.state.simulations} /> + {/* this.closeNewModal(data)} />*/} - this.closeEditModal(data)} project={this.state.modalData} simulations={this.state.simulations} /> + {/* this.closeEditModal(data)} project={this.state.modalData} simulations={this.state.simulations} /> @@ -133,4 +138,4 @@ class Projects extends Component { } } -export default Container.create(Projects); +export default Container.create(Users); diff --git a/src/data-managers/users-data-manager.js b/src/data-managers/users-data-manager.js index 08c3138..b20c3c2 100644 --- a/src/data-managers/users-data-manager.js +++ b/src/data-managers/users-data-manager.js @@ -59,12 +59,12 @@ class UsersDataManager extends RestDataManager { getUsers(token) { RestAPI.get(this.makeURL('/users'), token).then(response => { AppDispatcher.dispatch({ - type: 'users/users-loaded', - users: response.users + type: 'users/loaded', + data: response.users }); }).catch(error => { AppDispatcher.dispatch({ - type: 'users/users-load-error', + type: 'users/load-error', error: error }); }); diff --git a/src/stores/user-store.js b/src/stores/user-store.js index 9acf4d2..0c3a70a 100644 --- a/src/stores/user-store.js +++ b/src/stores/user-store.js @@ -79,21 +79,7 @@ class UserStore extends ReduceStore { } - return state; - - case 'users/start-load': - console.log('Sending request'); - UsersDataManager.getUsers(state.token); - - return state; - - case 'users/users-loaded': - - return Object.assign({}, state, { users: action.users }); - - case 'users/users-load-error': - // Users couldn't be loaded. Keep same state - return state; + return state; default: return state; diff --git a/src/stores/users-store.js b/src/stores/users-store.js new file mode 100644 index 0000000..3eb1595 --- /dev/null +++ b/src/stores/users-store.js @@ -0,0 +1,45 @@ +/** + * File: users-store.js + * Author: Markus Grigull + * 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 . + ******************************************************************************/ + +import ArrayStore from './array-store'; +import UsersDataManager from '../data-managers/users-data-manager'; + +class UsersStore extends ArrayStore { + constructor() { + super('users', UsersDataManager); + } + + reduce(state, action) { + switch (action.type) { + + // Override ArrayStore's start-load to pass token + case 'users/start-load': + UsersDataManager.getUsers(action.token); + return state; + + default: + return super.reduce(state, action); + } + } + +} + +export default new UsersStore(); From 1a54ae09a21183a2fea3ee11ee9f11c9eeabad3e Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Tue, 2 May 2017 17:26:47 +0200 Subject: [PATCH 08/19] Allow optional token inclusion in REST API calls --- src/data-managers/rest-data-manager.js | 18 +++++++++--------- src/data-managers/users-data-manager.js | 15 +-------------- src/stores/array-store.js | 11 ++++++----- src/stores/users-store.js | 5 ----- 4 files changed, 16 insertions(+), 33 deletions(-) diff --git a/src/data-managers/rest-data-manager.js b/src/data-managers/rest-data-manager.js index f4fd82f..99dbd04 100644 --- a/src/data-managers/rest-data-manager.js +++ b/src/data-managers/rest-data-manager.js @@ -51,10 +51,10 @@ class RestDataManager { return object; } - load(id) { + load(id, token = null) { if (id != null) { // load single object - RestAPI.get(this.makeURL(this.url + '/' + id)).then(response => { + RestAPI.get(this.makeURL(this.url + '/' + id), token).then(response => { const data = this.filterKeys(response[this.type]); AppDispatcher.dispatch({ @@ -69,7 +69,7 @@ class RestDataManager { }); } else { // load all objects - RestAPI.get(this.makeURL(this.url)).then(response => { + RestAPI.get(this.makeURL(this.url), token).then(response => { const data = response[this.type + 's'].map(element => { return this.filterKeys(element); }); @@ -87,11 +87,11 @@ class RestDataManager { } } - add(object) { + add(object, token = null) { var obj = {}; obj[this.type] = this.filterKeys(object); - RestAPI.post(this.makeURL(this.url), obj).then(response => { + RestAPI.post(this.makeURL(this.url), obj, token).then(response => { AppDispatcher.dispatch({ type: this.type + 's/added', data: response[this.type] @@ -104,8 +104,8 @@ class RestDataManager { }); } - remove(object) { - RestAPI.delete(this.makeURL(this.url + '/' + object._id)).then(response => { + 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], @@ -119,11 +119,11 @@ class RestDataManager { }); } - update(object) { + update(object, token = null) { var obj = {}; obj[this.type] = this.filterKeys(object); - RestAPI.put(this.makeURL(this.url + '/' + object._id), obj).then(response => { + RestAPI.put(this.makeURL(this.url + '/' + object._id, token), obj).then(response => { AppDispatcher.dispatch({ type: this.type + 's/edited', data: response[this.type] diff --git a/src/data-managers/users-data-manager.js b/src/data-managers/users-data-manager.js index b20c3c2..bd60534 100644 --- a/src/data-managers/users-data-manager.js +++ b/src/data-managers/users-data-manager.js @@ -55,20 +55,7 @@ class UsersDataManager extends RestDataManager { }); }); } - - getUsers(token) { - RestAPI.get(this.makeURL('/users'), token).then(response => { - AppDispatcher.dispatch({ - type: 'users/loaded', - data: response.users - }); - }).catch(error => { - AppDispatcher.dispatch({ - type: 'users/load-error', - error: error - }); - }); - } + } export default new UsersDataManager(); diff --git a/src/stores/array-store.js b/src/stores/array-store.js index 9ff162e..b7cfbee 100644 --- a/src/stores/array-store.js +++ b/src/stores/array-store.js @@ -69,10 +69,10 @@ class ArrayStore extends ReduceStore { case this.type + '/start-load': if (Array.isArray(action.data)) { action.data.forEach((id) => { - this.dataManager.load(id); + this.dataManager.load(id, action.token); }); } else { - this.dataManager.load(action.data); + this.dataManager.load(action.data, action.token); } return state; @@ -88,18 +88,19 @@ class ArrayStore extends ReduceStore { return state; case this.type + '/start-add': - this.dataManager.add(action.data); + this.dataManager.add(action.data, action.token); return state; case this.type + '/added': return this.updateElements(state, [action.data]); case this.type + '/add-error': + console.log('something happened'); // TODO: Add error message return state; case this.type + '/start-remove': - this.dataManager.remove(action.data); + this.dataManager.remove(action.data, action.token); return state; case this.type + '/removed': @@ -112,7 +113,7 @@ class ArrayStore extends ReduceStore { return state; case this.type + '/start-edit': - this.dataManager.update(action.data); + this.dataManager.update(action.data, action.token); return state; case this.type + '/edited': diff --git a/src/stores/users-store.js b/src/stores/users-store.js index 3eb1595..0b38e69 100644 --- a/src/stores/users-store.js +++ b/src/stores/users-store.js @@ -30,11 +30,6 @@ class UsersStore extends ArrayStore { reduce(state, action) { switch (action.type) { - // Override ArrayStore's start-load to pass token - case 'users/start-load': - UsersDataManager.getUsers(action.token); - return state; - default: return super.reduce(state, action); } From a4ea7af42933f62faca799e25ad68fecad379516 Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Tue, 2 May 2017 17:27:04 +0200 Subject: [PATCH 09/19] add users with fixed password --- src/components/dialog/new-user.js | 101 ++++++++++++++++++++++++++++++ src/containers/users.js | 35 ++++++----- 2 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 src/components/dialog/new-user.js diff --git a/src/components/dialog/new-user.js b/src/components/dialog/new-user.js new file mode 100644 index 0000000..60f845d --- /dev/null +++ b/src/components/dialog/new-user.js @@ -0,0 +1,101 @@ +/** + * File: new-user.js + * Author: Ricardo Hernandez-Montoya + * 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 . + ******************************************************************************/ + +import React, { Component, PropTypes } from 'react'; +import { FormGroup, FormControl, ControlLabel } from 'react-bootstrap'; + +import Dialog from './dialog'; + +class NewUserDialog extends Component { + static propTypes = { + show: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired + }; + + valid: false; + + constructor(props) { + super(props); + + this.state = { + username: '', + role: 'admin', + password: '1234' + }; + } + + onClose(canceled) { + if (canceled === false) { + this.props.onClose(this.state); + } else { + this.props.onClose(); + } + } + + handleChange(e) { + this.setState({ [e.target.id]: e.target.value }); + } + + resetState() { + this.setState({ + username: '', + role: 'admin', + password: '1234' + }); + } + + 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"; + } + + render() { + return ( + this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}> +
    + + Username + this.handleChange(e)} /> + + + + Simulation + this.handleChange(e)}> + + + + +
    +
    + ); + } +} + +export default NewUserDialog; diff --git a/src/containers/users.js b/src/containers/users.js index b8773be..4ab17eb 100644 --- a/src/containers/users.js +++ b/src/containers/users.js @@ -21,7 +21,8 @@ import React, { Component } from 'react'; import { Container } from 'flux/utils'; -import { Button, Modal, Glyphicon } from 'react-bootstrap'; +// import { Button, Modal, Glyphicon } from 'react-bootstrap'; +import { Button, Glyphicon } from 'react-bootstrap'; import AppDispatcher from '../app-dispatcher'; import UserStore from '../stores/user-store'; @@ -29,7 +30,7 @@ import UsersStore from '../stores/users-store'; import Table from '../components/table'; import TableColumn from '../components/table-column'; -// import NewUserDialog from '../components/dialog/new-user'; +import NewUserDialog from '../components/dialog/new-user'; // import EditUserDialog from '../components/dialog/edit-user'; class Users extends Component { @@ -60,16 +61,17 @@ class Users extends Component { }; } - // closeNewModal(data) { - // this.setState({ newModal: false }); + closeNewModal(data) { + this.setState({ newModal: false }); - // if (data) { - // AppDispatcher.dispatch({ - // type: 'users/start-add', - // data: data - // }); - // } - // } + if (data) { + AppDispatcher.dispatch({ + type: 'users/start-add', + data: data, + token: this.state.token + }); + } + } // confirmDeleteModal() { // this.setState({ deleteModal: false }); @@ -85,8 +87,9 @@ class Users extends Component { // if (data) { // AppDispatcher.dispatch({ - // type: 'projects/start-edit', - // data: data + // type: 'users/start-edit', + // data: data, + // token: this.state.token // }); // } // } @@ -115,11 +118,11 @@ class Users extends Component { - {/* this.closeNewModal(data)} />*/} + this.closeNewModal(data)} /> - {/* this.closeEditModal(data)} project={this.state.modalData} simulations={this.state.simulations} /> + {/* this.closeEditModal(data)} project={this.state.modalData} />*/} - + {/* Delete Project From fbe3576892cbd95bbe723688e4d5d5ac19ced423 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Tue, 2 May 2017 18:21:47 +0200 Subject: [PATCH 10/19] use new API of VILLASnode to detect running simulators --- src/data-managers/simulators-data-manager.js | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/data-managers/simulators-data-manager.js b/src/data-managers/simulators-data-manager.js index 9bee488..f9dfaa6 100644 --- a/src/data-managers/simulators-data-manager.js +++ b/src/data-managers/simulators-data-manager.js @@ -26,20 +26,26 @@ import AppDispatcher from '../app-dispatcher'; function isRunning(simulator) { // get path to nodes.json and simulator name var path = simulator.endpoint.substring(0, simulator.endpoint.lastIndexOf('/')); - path += '/nodes.json'; - var name = simulator.endpoint.substring(simulator.endpoint.lastIndexOf('/') + 1); + var url = 'http://' + path + '/api/v1'; + var body = { + action: 'nodes', + id: '1234' /// @todo use random generated id + }; + // send request - RestAPI.get('http://' + path).then(response => { + RestAPI.post(url, body).then(response => { // check if simulator is running simulator.running = false; - response.forEach(sim => { - if (sim.name === name) { - simulator.running = true; - } - }); + if (response.id == body.id) { + response.response.forEach(sim => { + if (sim.name === name) { + simulator.running = true; + } + }); + } AppDispatcher.dispatch({ type: 'simulators/running', From 56fbaca6802893b18118978cd952360ee87c2b05 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Tue, 2 May 2017 18:22:41 +0200 Subject: [PATCH 11/19] remove endian flag from websocket packets --- src/data-managers/simulator-data-data-manager.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/data-managers/simulator-data-data-manager.js b/src/data-managers/simulator-data-data-manager.js index d311af4..22f443b 100644 --- a/src/data-managers/simulator-data-data-manager.js +++ b/src/data-managers/simulator-data-data-manager.js @@ -90,23 +90,20 @@ class SimulatorDataDataManager { // parse incoming message into usable data var data = new DataView(blob); - let OFFSET_ENDIAN = 1; let OFFSET_TYPE = 2; let OFFSET_VERSION = 4; var bits = data.getUint8(0); - var endian = (bits >> OFFSET_ENDIAN) & 0x1 ? 0 : 1; - var length = data.getUint16(0x02, endian); + var length = data.getUint16(0x02, 1); var values = new Float32Array(data.buffer, data.byteOffset + 0x10, length); return { - endian: endian, version: (bits >> OFFSET_VERSION) & 0xF, type: (bits >> OFFSET_TYPE) & 0x3, length: length, - sequence: data.getUint32(0x04, endian), - timestamp: data.getUint32(0x08, endian) * 1e3 + data.getUint32(0x0C, endian) * 1e-6, + sequence: data.getUint32(0x04, 1), + timestamp: data.getUint32(0x08, 1) * 1e3 + data.getUint32(0x0C, 1) * 1e-6, values: values }; } From 9ec1d54a9389ba029512161cfd4ac6e85587c9be Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Tue, 2 May 2017 18:26:04 +0200 Subject: [PATCH 12/19] add note about default user and password --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d05a9a5..172ff40 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ Additional libraries are used, for a complete list see package.json. To start the website locally run `npm start`. This will open a local webserver serving the _frontend_. To make the website work, you still need to start at least the VILLASweb-backend (See repository for information). +The default user and password are configured in the `config.js` file of the _backend_. By default they are: __admin__ / __admin__. + ## Copyright 2017, Institute for Automation of Complex Power Systems, EONERC @@ -55,4 +57,4 @@ For other licensing options please consult [Prof. Antonello Monti](mailto:amonti [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) -[RWTH University Aachen, Germany](http://www.rwth-aachen.de) \ No newline at end of file +[RWTH University Aachen, Germany](http://www.rwth-aachen.de) From 8f6e1fc4399e09581781e08d55d6f24a4b478e89 Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Wed, 3 May 2017 11:05:22 +0200 Subject: [PATCH 13/19] User edit and remove --- src/components/dialog/edit-user.js | 104 +++++++++++++++++++++++++ src/containers/users.js | 60 ++++++-------- src/data-managers/rest-data-manager.js | 2 +- 3 files changed, 130 insertions(+), 36 deletions(-) create mode 100644 src/components/dialog/edit-user.js diff --git a/src/components/dialog/edit-user.js b/src/components/dialog/edit-user.js new file mode 100644 index 0000000..9485c29 --- /dev/null +++ b/src/components/dialog/edit-user.js @@ -0,0 +1,104 @@ +/** + * File: edit-user.js + * Author: Ricardo Hernandez-Montoya + * 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 . + ******************************************************************************/ + +import React, { Component, PropTypes } from 'react'; +import { FormGroup, FormControl, ControlLabel } from 'react-bootstrap'; + +import Dialog from './dialog'; + +class EditUserDialog extends Component { + static propTypes = { + show: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + user: PropTypes.object.isRequired + }; + + valid: true; + + constructor(props) { + super(props); + + this.state = { + username: '', + role: '', + _id: '' + } + } + + onClose(canceled) { + if (canceled === false) { + this.props.onClose(this.state); + } else { + this.props.onClose(); + } + } + + handleChange(e) { + this.setState({ [e.target.id]: e.target.value }); + } + + resetState() { + this.setState({ + username: this.props.user.username, + 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 ( + this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}> +
    + + Username + this.handleChange(e)} /> + + + + Simulation + this.handleChange(e)}> + + + + +
    +
    + ); + } +} + +export default EditUserDialog; diff --git a/src/containers/users.js b/src/containers/users.js index 4ab17eb..b1f573f 100644 --- a/src/containers/users.js +++ b/src/containers/users.js @@ -21,8 +21,7 @@ import React, { Component } from 'react'; import { Container } from 'flux/utils'; -// import { Button, Modal, Glyphicon } from 'react-bootstrap'; -import { Button, Glyphicon } from 'react-bootstrap'; +import { Button, Modal, Glyphicon } from 'react-bootstrap'; import AppDispatcher from '../app-dispatcher'; import UserStore from '../stores/user-store'; @@ -31,7 +30,7 @@ import UsersStore from '../stores/users-store'; import Table from '../components/table'; import TableColumn from '../components/table-column'; import NewUserDialog from '../components/dialog/new-user'; -// import EditUserDialog from '../components/dialog/edit-user'; +import EditUserDialog from '../components/dialog/edit-user'; class Users extends Component { static getStores() { @@ -73,36 +72,27 @@ class Users extends Component { } } - // confirmDeleteModal() { - // this.setState({ deleteModal: false }); + confirmDeleteModal() { + this.setState({ deleteModal: false }); - // AppDispatcher.dispatch({ - // type: 'projects/start-remove', - // data: this.state.modalData - // }); - // } + AppDispatcher.dispatch({ + type: 'users/start-remove', + data: this.state.modalData, + token: this.state.token + }); + } - // closeEditModal(data) { - // this.setState({ editModal: false }); + closeEditModal(data) { + this.setState({ editModal: false }); - // if (data) { - // AppDispatcher.dispatch({ - // type: 'users/start-edit', - // data: data, - // token: this.state.token - // }); - // } - // } - - // 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; - // } + if (data) { + AppDispatcher.dispatch({ + type: 'users/start-edit', + data: data, + token: this.state.token + }); + } + } render() { @@ -120,22 +110,22 @@ class Users extends Component { this.closeNewModal(data)} /> - {/* this.closeEditModal(data)} project={this.state.modalData} />*/} + this.closeEditModal(data)} user={this.state.modalData} /> - {/* + - Delete Project + Delete user - Are you sure you want to delete the project '{this.state.modalData.name}'? + Are you sure you want to delete the user '{this.state.modalData.username}'? - */} +
    ); } diff --git a/src/data-managers/rest-data-manager.js b/src/data-managers/rest-data-manager.js index 99dbd04..2ae0a46 100644 --- a/src/data-managers/rest-data-manager.js +++ b/src/data-managers/rest-data-manager.js @@ -123,7 +123,7 @@ class RestDataManager { var obj = {}; obj[this.type] = this.filterKeys(object); - RestAPI.put(this.makeURL(this.url + '/' + object._id, token), obj).then(response => { + RestAPI.put(this.makeURL(this.url + '/' + object._id), obj, token).then(response => { AppDispatcher.dispatch({ type: this.type + 's/edited', data: response[this.type] From 703fa92d2fb9142599f5952db999e90cdd339949 Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Wed, 3 May 2017 11:26:11 +0200 Subject: [PATCH 14/19] include user e-mail --- src/components/dialog/edit-user.js | 9 ++++++++- src/components/dialog/new-user.js | 9 ++++++++- src/containers/users.js | 3 ++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/components/dialog/edit-user.js b/src/components/dialog/edit-user.js index 9485c29..a779725 100644 --- a/src/components/dialog/edit-user.js +++ b/src/components/dialog/edit-user.js @@ -38,6 +38,7 @@ class EditUserDialog extends Component { this.state = { username: '', + mail: '', role: '', _id: '' } @@ -58,6 +59,7 @@ class EditUserDialog extends Component { resetState() { this.setState({ username: this.props.user.username, + mail: this.props.user.mail, role: this.props.user.role, _id: this.props.user._id }); @@ -88,8 +90,13 @@ class EditUserDialog extends Component { this.handleChange(e)} /> + + E-mail + this.handleChange(e)} /> + + - Simulation + Role this.handleChange(e)}> diff --git a/src/components/dialog/new-user.js b/src/components/dialog/new-user.js index 60f845d..690f17d 100644 --- a/src/components/dialog/new-user.js +++ b/src/components/dialog/new-user.js @@ -37,6 +37,7 @@ class NewUserDialog extends Component { this.state = { username: '', + mail: '', role: 'admin', password: '1234' }; @@ -57,6 +58,7 @@ class NewUserDialog extends Component { resetState() { this.setState({ username: '', + mail: '', role: 'admin', password: '1234' }); @@ -85,8 +87,13 @@ class NewUserDialog extends Component { this.handleChange(e)} /> + + E-mail + this.handleChange(e)} /> + + - Simulation + Role this.handleChange(e)}> diff --git a/src/containers/users.js b/src/containers/users.js index b1f573f..91f720c 100644 --- a/src/containers/users.js +++ b/src/containers/users.js @@ -101,7 +101,8 @@ class Users extends Component {

    Users

    - + + this.setState({ editModal: true, modalData: this.state.users[index] })} onDelete={index => this.setState({ deleteModal: true, modalData: this.state.users[index] })} />
    From 8fb0166059b61eeb1fe52d8b16352224409f8876 Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Wed, 3 May 2017 11:44:08 +0200 Subject: [PATCH 15/19] human names and consisting roles with back-end --- src/components/dialog/edit-user.js | 5 +++-- src/components/dialog/new-user.js | 5 +++-- src/containers/users.js | 8 +++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/dialog/edit-user.js b/src/components/dialog/edit-user.js index a779725..c8bb203 100644 --- a/src/components/dialog/edit-user.js +++ b/src/components/dialog/edit-user.js @@ -98,8 +98,9 @@ class EditUserDialog extends Component { Role this.handleChange(e)}> - - + + + diff --git a/src/components/dialog/new-user.js b/src/components/dialog/new-user.js index 690f17d..f666e31 100644 --- a/src/components/dialog/new-user.js +++ b/src/components/dialog/new-user.js @@ -95,8 +95,9 @@ class NewUserDialog extends Component { Role this.handleChange(e)}> - - + + + diff --git a/src/containers/users.js b/src/containers/users.js index 91f720c..288c4f4 100644 --- a/src/containers/users.js +++ b/src/containers/users.js @@ -94,6 +94,12 @@ class Users extends Component { } } + getHumanRoleName(role_key) { + const HUMAN_ROLE_NAMES = {admin: 'Administrator', user: 'User', guest: 'Guest'}; + + return HUMAN_ROLE_NAMES.hasOwnProperty(role_key)? HUMAN_ROLE_NAMES[role_key] : ''; + } + render() { return ( @@ -103,7 +109,7 @@ class Users extends Component { - + this.getHumanRoleName(role)} /> this.setState({ editModal: true, modalData: this.state.users[index] })} onDelete={index => this.setState({ deleteModal: true, modalData: this.state.users[index] })} />
    From d218c1854d466bcd8db80de9c0ee42f4dd5daba8 Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Wed, 3 May 2017 12:46:53 +0200 Subject: [PATCH 16/19] hide user management section for non-admins --- src/components/menu-sidebar.js | 4 +++- src/containers/app.js | 6 ++++-- src/data-managers/users-data-manager.js | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/menu-sidebar.js b/src/components/menu-sidebar.js index 1782304..882ab56 100644 --- a/src/components/menu-sidebar.js +++ b/src/components/menu-sidebar.js @@ -33,7 +33,9 @@ class SidebarMenu extends Component {
  • Projects
  • Simulations
  • Simulators
  • -
  • User Management
  • + { this.props.currentRole === 'admin' ? +
  • User Management
  • : '' + }
  • Logout
  • diff --git a/src/containers/app.js b/src/containers/app.js index 4e90442..9e6eaf0 100644 --- a/src/containers/app.js +++ b/src/containers/app.js @@ -76,9 +76,11 @@ class App extends Component { } } + let currentUser = UserStore.getState().currentUser; + return { simulations: SimulationStore.getState(), - currentUser: UserStore.getState().currentUser, + currentRole: currentUser? currentUser.role : '', token: UserStore.getState().token, runningSimulators: simulators @@ -183,7 +185,7 @@ class App extends Component {
    - +
    {children} diff --git a/src/data-managers/users-data-manager.js b/src/data-managers/users-data-manager.js index bd60534..f5db8f8 100644 --- a/src/data-managers/users-data-manager.js +++ b/src/data-managers/users-data-manager.js @@ -46,7 +46,7 @@ class UsersDataManager extends RestDataManager { RestAPI.get(this.makeURL('/users/me'), token).then(response => { AppDispatcher.dispatch({ type: 'users/current-user', - user: response + user: response.user }); }).catch(error => { AppDispatcher.dispatch({ From 39c8a2518a14f659fabafaa6e3563ceefa45880e Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Wed, 3 May 2017 15:58:58 +0200 Subject: [PATCH 17/19] sidebar menu items no wrap and grow --- src/components/menu-sidebar.js | 12 ++++----- src/containers/app.js | 10 ++++--- src/styles/app.css | 49 +++++++++++++++++++++++++--------- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/components/menu-sidebar.js b/src/components/menu-sidebar.js index 882ab56..152644a 100644 --- a/src/components/menu-sidebar.js +++ b/src/components/menu-sidebar.js @@ -29,14 +29,14 @@ class SidebarMenu extends Component {

    Menu

      -
    • Home
    • -
    • Projects
    • -
    • Simulations
    • -
    • Simulators
    • +
    • Home
    • +
    • Projects
    • +
    • Simulations
    • +
    • Simulators
    • { this.props.currentRole === 'admin' ? -
    • User Management
    • : '' +
    • User Management
    • : '' } -
    • Logout
    • +
    • Logout
    ); diff --git a/src/containers/app.js b/src/containers/app.js index 9e6eaf0..2af248e 100644 --- a/src/containers/app.js +++ b/src/containers/app.js @@ -77,7 +77,7 @@ class App extends Component { } let currentUser = UserStore.getState().currentUser; - + return { simulations: SimulationStore.getState(), currentRole: currentUser? currentUser.role : '', @@ -185,10 +185,12 @@ class App extends Component {
    - -
    - {children} +
    + +
    + {children} +