diff --git a/package-lock.json b/package-lock.json index 68bdec9..fca3d27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3910,6 +3910,11 @@ "isarray": "^1.0.0" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -5735,6 +5740,14 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -8141,11 +8154,6 @@ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" }, - "immer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==" - }, "immutable": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", @@ -10571,6 +10579,30 @@ "universalify": "^2.0.0" } }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -10602,6 +10634,25 @@ "set-immediate-shim": "~1.0.1" } }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -10754,11 +10805,46 @@ "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -13664,9 +13750,9 @@ } }, "react-dev-utils": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.3.tgz", - "integrity": "sha512-4lEA5gF4OHrcJLMUV1t+4XbNDiJbsAWCH5Z2uqlTqW6dD7Cf5nEASkeXrCI/Mz83sI2o527oBIFKVMXtRf1Vtg==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz", + "integrity": "sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A==", "requires": { "@babel/code-frame": "7.10.4", "address": "1.1.2", @@ -13728,11 +13814,21 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" }, + "immer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", + "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==" + }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, + "react-error-overlay": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", + "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13827,11 +13923,6 @@ "prop-types": "^15.6.0" } }, - "react-error-overlay": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", - "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==" - }, "react-fullscreenable": { "version": "2.5.1-0", "resolved": "https://registry.npmjs.org/react-fullscreenable/-/react-fullscreenable-2.5.1-0.tgz", @@ -13887,6 +13978,39 @@ "react-base16-styling": "^0.6.0", "react-lifecycles-compat": "^3.0.4", "react-textarea-autosize": "^8.3.2" + }, + "dependencies": { + "fbemitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz", + "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==", + "requires": { + "fbjs": "^3.0.0" + } + }, + "fbjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.0.tgz", + "integrity": "sha512-dJd4PiDOFuhe7vk4F80Mba83Vr2QuK86FoxtgPmzBqEJahncp+13YCmfoa53KHCo6OnlXLG7eeMWPfB5CrpVKg==", + "requires": { + "cross-fetch": "^3.0.4", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + } + }, + "flux": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.1.tgz", + "integrity": "sha512-emk4RCvJ8RzNP2lNpphKnG7r18q8elDYNAPx7xn+bDeOIo9FFfxEfIQ2y6YbQNmnsGD3nH1noxtLE64Puz1bRQ==", + "requires": { + "fbemitter": "^3.0.0", + "fbjs": "^3.0.0" + } + } } }, "react-lifecycles-compat": { diff --git a/package.json b/package.json index f62b007..6954861 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "handlebars": "^4.7.7", "jquery": "^3.6.0", "jszip": "^3.6.0", + "jsonwebtoken": "^8.5.1", "libcimsvg": "git+https://git.rwth-aachen.de/acs/public/cim/pintura-npm-package.git", "lodash": "^4.17.21", "moment": "^2.29.1", @@ -60,7 +61,7 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, - "proxy": "http://localhost:4000", + "proxy": "https://villas.k8s.eonerc.rwth-aachen.de", "browserslist": { "production": [ ">0.2%", diff --git a/src/app.js b/src/app.js index 3149a4b..cf4ce3b 100644 --- a/src/app.js +++ b/src/app.js @@ -20,7 +20,7 @@ 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 { Hidden } from 'react-grid-system' +import jwt from 'jsonwebtoken' import AppDispatcher from './common/app-dispatcher'; import NotificationsDataManager from './common/data-managers/notifications-data-manager'; @@ -39,6 +39,7 @@ import User from './user/user'; import APIBrowser from './common/api-browser'; import LoginStore from './user/login-store' + import './styles/app.css'; class App extends React.Component { @@ -63,22 +64,37 @@ class App extends React.Component { // 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("Logged-in as user ", currentUser.username) - AppDispatcher.dispatch({ - type: 'users/logged-in', - token: token, - currentUser: currentUser - }); + + let isExpired = this.tokenIsExpired(token); + if (isExpired) { + console.log("Token expired") + AppDispatcher.dispatch({ + type: 'users/logout' + }); + } else { + let currentUser = JSON.parse(localStorage.getItem("currentUser")); + console.log("Logged-in as user ", currentUser.username) + AppDispatcher.dispatch({ + type: 'users/logged-in', + token: token, + currentUser: currentUser + }); + } } } + tokenIsExpired(token){ + let decodedToken = jwt.decode(token); + let timeNow = (new Date().getTime() + 1) / 1000; + return decodedToken.exp < timeNow; + } + render() { let token = localStorage.getItem("token"); let currentUserRaw = localStorage.getItem("currentUser"); - if (token == null || token === "" || currentUserRaw == null || currentUserRaw === "") { + if ((token == null || token === "" || currentUserRaw == null || currentUserRaw === "") || this.tokenIsExpired(token)) { console.log("APP redirecting to logout/ login") return (); } diff --git a/src/componentconfig/config-table.js b/src/componentconfig/config-table.js new file mode 100644 index 0000000..1452949 --- /dev/null +++ b/src/componentconfig/config-table.js @@ -0,0 +1,434 @@ +/** + * 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 FileSaver from 'file-saver'; +import IconButton from "../common/icon-button"; +import Table from "../common/table"; +import TableColumn from "../common/table-column"; +import DeleteDialog from "../common/dialogs/delete-dialog"; +import AppDispatcher from "../common/app-dispatcher"; +import NotificationsDataManager from "../common/data-managers/notifications-data-manager"; +import ICAction from "../ic/ic-action"; +import EditConfigDialog from "./edit-config"; +import ImportConfigDialog from "./import-config"; +import EditSignalMappingDialog from "../signal/edit-signal-mapping"; + +class ConfigTable extends Component { + + constructor() { + super(); + + this.state = { + editConfigModal: false, + modalConfigData: {}, + modalConfigIndex: 0, + deleteConfigModal: false, + importConfigModal: false, + newConfig: false, + selectedConfigs: [], + ExternalICInUse: false, + editOutputSignalsModal: false, + editInputSignalsModal: false, + } + } + + addConfig() { + const config = { + scenarioID: this.props.scenario.id, + name: 'New Component Configuration', + icID: this.props.ics.length > 0 ? this.props.ics[0].id : null, + startParameters: {}, + }; + + AppDispatcher.dispatch({ + type: 'configs/start-add', + data: config, + token: this.props.sessionToken + }); + + this.setState({ newConfig: true }); + + } + + closeEditConfigModal(data) { + this.setState({ editConfigModal: false, newConfig: false }); + + if (data) { + AppDispatcher.dispatch({ + type: 'configs/start-edit', + data: data, + token: this.props.sessionToken, + }); + } + } + + closeDeleteConfigModal(confirmDelete) { + this.setState({ deleteConfigModal: false }); + + if (confirmDelete === false) { + return; + } + + AppDispatcher.dispatch({ + type: 'configs/start-remove', + data: this.state.modalConfigData, + token: this.props.sessionToken + }); + } + + importConfig(data) { + this.setState({ importConfigModal: false }); + + if (data == null) { + return; + } + + let newConfig = JSON.parse(JSON.stringify(data.config)) + + newConfig["scenarioID"] = this.props.scenario.id; + newConfig.name = data.name; + + AppDispatcher.dispatch({ + type: 'configs/start-add', + data: newConfig, + token: this.props.sessionToken + }); + } + + copyConfig(index) { + let config = JSON.parse(JSON.stringify(this.props.configs[index])); + + let signals = JSON.parse(JSON.stringify(this.props.signals.filter(s => s.configID === parseInt(config.id, 10)))); + signals.forEach((signal) => { + delete signal.configID; + delete signal.id; + }) + + // two separate lists for inputMapping and outputMapping + let inputSignals = signals.filter(s => s.direction === 'in'); + let outputSignals = signals.filter(s => s.direction === 'out'); + + // add signal mappings to config + config["inputMapping"] = inputSignals; + config["outputMapping"] = outputSignals; + + delete config.id; + delete config.scenarioID; + delete config.inputLength; + delete config.outputLength; + + return config; + } + + exportConfig(index) { + let config = this.copyConfig(index); + + // show save dialog + const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); + FileSaver.saveAs(blob, 'config-' + config.name + '.json'); + } + + duplicateConfig(index) { + let newConfig = this.copyConfig(index); + newConfig["scenarioID"] = this.props.scenario.id; + newConfig.name = newConfig.name + '_copy'; + + AppDispatcher.dispatch({ + type: 'configs/start-add', + data: newConfig, + token: this.props.sessionToken + }); + } + + onConfigChecked(index, event) { + const selectedConfigs = Object.assign([], this.state.selectedConfigs); + for (let key in selectedConfigs) { + if (selectedConfigs[key] === index) { + // update existing entry + if (event.target.checked) { + return; + } + + selectedConfigs.splice(key, 1); + + this.setState({ selectedConfigs: selectedConfigs }); + return; + } + } + + // add new entry + if (event.target.checked === false) { + return; + } + + selectedConfigs.push(index); + this.setState({ selectedConfigs: selectedConfigs }); + } + + usesExternalIC(index) { + let icID = this.props.configs[index].icID; + + let ic = null; + for (let component of this.props.ics) { + if (component.id === icID) { + ic = component; + } + } + + if (ic == null) { + return false; + } + + if (ic.managedexternally === true) { + this.setState({ ExternalICInUse: true }) + return true + } + + return false + } + + getICName(icID) { + for (let ic of this.props.ics) { + if (ic.id === icID) { + return ic.name || ic.uuid; + } + } + } + + /* ############################################## + * Signal modification methods + ############################################## */ + + closeEditSignalsModal(direction) { + + // reload the config + AppDispatcher.dispatch({ + type: 'configs/start-load', + data: this.state.modalConfigData.id, + token: this.props.sessionToken + }); + + if (direction === "in") { + this.setState({ editInputSignalsModal: false }); + } else if (direction === "out") { + this.setState({ editOutputSignalsModal: false }); + } + } + + signalsAutoConf(index) { + let componentConfig = this.props.configs[index]; + // determine apiurl of infrastructure component + let ic = this.props.ics.find(ic => ic.id === componentConfig.icID) + if (!ic.type.includes("villas-node")) { + let message = "Cannot autoconfigure signals for IC type " + ic.type + " of category " + ic.category + ". This is only possible for gateway ICs of type 'VILLASnode'." + console.warn(message); + + const SIGNAL_AUTOCONF_WARN_NOTIFICATION = { + title: 'Failed to load signal config for IC ' + ic.name, + message: message, + level: 'warning' + }; + NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_WARN_NOTIFICATION); + return; + } + + let splitWebsocketURL = ic.websocketurl.split("/") + + AppDispatcher.dispatch({ + type: 'signals/start-autoconfig', + url: ic.apiurl + "/nodes", + socketname: splitWebsocketURL[splitWebsocketURL.length - 1], + token: this.props.sessionToken, + configID: componentConfig.id + }); + + } + + startPintura(configIndex) { + let config = this.props.configs[configIndex]; + + // get xml / CIM file + let files = [] + for (let id of config.fileIDs) { + for (let file of this.props.files) { + if (file.id === id && ['xml'].some(e => file.type.includes(e))) { + files.push(file); + } + } + } + + if (files.length > 1) { + // more than one CIM file... + console.warn("There is more than one CIM file selected in this component configuration. I will open them all in a separate tab.") + } + + let baseURL = 'aaa.bbb.ccc.ddd/api/v2/files/' + for (let file of files) { + // endpoint param serves for download and upload of CIM file, token is required for authentication + let params = { + token: this.props.sessionToken, + endpoint: baseURL + String(file.id), + } + + // TODO start Pintura for editing CIM/ XML file from here + console.warn("Starting Pintura... and nothing happens so far :-) ", params) + } + } + + render() { + return ( +
+ {/*Component Configurations table*/} +

Component Configurations + + this.addConfig()} + icon='plus' + /> + this.setState({ importConfigModal: true })} + icon='upload' + /> + +

+ + !this.usesExternalIC(index)} + onChecked={(index, event) => this.onConfigChecked(index, event)} + width={20} + /> + {this.props.currentUser.role === "Admin" ? + + : <> + } + + this.setState({ editOutputSignalsModal: true, modalConfigData: this.props.configs[index], modalConfigIndex: index })} + width={150} + /> + this.setState({ editInputSignalsModal: true, modalConfigData: this.props.configs[index], modalConfigIndex: index })} + width={150} + /> + this.signalsAutoConf(index)} + width={150} + /> + this.getICName(icID)} + width={200} + /> + this.setState({ editConfigModal: true, modalConfigData: this.props.configs[index], modalConfigIndex: index })} + onDelete={(index) => this.setState({ deleteConfigModal: true, modalConfigData: this.props.configs[index], modalConfigIndex: index })} + onExport={index => this.exportConfig(index)} + onDuplicate={index => this.duplicateConfig(index)} + /> +
+ + {this.state.ExternalICInUse ? +
+ this.copyConfig(index)} + token = {this.props.sessionToken} + 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' } } + ]} /> +
+ :
+ } + +
+ + this.closeEditConfigModal(data)} + config={this.state.modalConfigData} + ics={this.props.ics} + files={this.props.files} + sessionToken={this.props.sessionToken} + /> + this.importConfig(data)} + ics={this.props.ics} + /> + this.closeDeleteConfigModal(c)} + /> + this.closeEditSignalsModal(direction)} + direction="Output" + signals={this.props.signals} + configID={this.state.modalConfigData.id} + sessionToken={this.props.sessionToken} + /> + this.closeEditSignalsModal(direction)} + direction="Input" + signals={this.props.signals} + configID={this.state.modalConfigData.id} + sessionToken={this.props.sessionToken} + /> +
+ ) + } +} + +export default ConfigTable; diff --git a/src/dashboard/dashboard-table.js b/src/dashboard/dashboard-table.js new file mode 100644 index 0000000..89f5bf0 --- /dev/null +++ b/src/dashboard/dashboard-table.js @@ -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 . + ******************************************************************************/ + +import React, {Component} from "react"; +import FileSaver from 'file-saver'; +import IconButton from "../common/icon-button"; +import Table from "../common/table"; +import TableColumn from "../common/table-column"; +import NewDashboardDialog from "./new-dashboard"; +import EditDashboardDialog from "./edit-dashboard"; +import ImportDashboardDialog from "./import-dashboard"; +import DeleteDialog from "../common/dialogs/delete-dialog"; +import AppDispatcher from "../common/app-dispatcher"; + +class DashboardTable extends Component { + + constructor() { + super(); + + this.state = { + newDashboardModal: false, + deleteDashboardModal: false, + importDashboardModal: false, + modalDashboardData: {}, + dashboardEditModal: false, + } + } + + closeNewDashboardModal(data) { + this.setState({ newDashboardModal: false }); + if (data) { + // TODO: 'newDashboard' not used, check this + let newDashboard = data; + // add default grid value and scenarioID + newDashboard["grid"] = 15; + newDashboard["scenarioID"] = this.props.scenario.id; + + AppDispatcher.dispatch({ + type: 'dashboards/start-add', + data, + token: this.props.sessionToken, + }); + } + } + + closeEditDashboardModal(data) { + this.setState({ dashboardEditModal: false }); + + let editDashboard = this.state.modalDashboardData; + + if (data != null) { + editDashboard.name = data.name; + AppDispatcher.dispatch({ + type: 'dashboards/start-edit', + data: editDashboard, + token: this.props.sessionToken + }); + } + } + + closeDeleteDashboardModal(confirmDelete) { + this.setState({ deleteDashboardModal: false }); + + if (confirmDelete === false) { + return; + } + + AppDispatcher.dispatch({ + type: 'dashboards/start-remove', + data: this.state.modalDashboardData, + token: this.props.sessionToken, + }); + } + + closeImportDashboardModal(data) { + this.setState({ importDashboardModal: false }); + + if (data) { + let newDashboard = JSON.parse(JSON.stringify(data)); + newDashboard["scenarioID"] = this.props.scenario.id; + + AppDispatcher.dispatch({ + type: 'dashboards/start-add', + data: newDashboard, + token: this.props.sessionToken, + }); + } + } + + copyDashboard(index) { + let dashboard = JSON.parse(JSON.stringify(this.props.dashboards[index])); + + let widgets = JSON.parse(JSON.stringify(this.props.widgets.filter(w => w.dashboardID === parseInt(dashboard.id, 10)))); + widgets.forEach((widget) => { + delete widget.dashboardID; + delete widget.id; + }) + dashboard["widgets"] = widgets; + delete dashboard.scenarioID; + delete dashboard.id; + + return dashboard; + } + + exportDashboard(index) { + let dashboard = this.copyDashboard(index); + + // show save dialog + const blob = new Blob([JSON.stringify(dashboard, null, 2)], { type: 'application/json' }); + FileSaver.saveAs(blob, 'dashboard - ' + dashboard.name + '.json'); + } + + duplicateDashboard(index) { + let newDashboard = this.copyDashboard(index); + newDashboard.scenarioID = this.props.scenario.id; + newDashboard.name = newDashboard.name + '_copy'; + + AppDispatcher.dispatch({ + type: 'dashboards/start-add', + data: newDashboard, + token: this.props.sessionToken, + }); + } + + render() { + + return ( +
+ {/*Dashboard table*/} +

Dashboards + + this.setState({newDashboardModal: true})} + icon='plus' + /> + this.setState({importDashboardModal: true})} + icon='upload' + /> + +

+ + {this.props.currentUser.role === "Admin" ? + + : <> + } + + + + this.setState({ + dashboardEditModal: true, + modalDashboardData: this.props.dashboards[index] + })} + onDelete={(index) => this.setState({ + deleteDashboardModal: true, + modalDashboardData: this.props.dashboards[index], + modalDashboardIndex: index + })} + onExport={index => this.exportDashboard(index)} + onDuplicate={index => this.duplicateDashboard(index)} + /> +
+ + this.closeNewDashboardModal(data)} + /> + this.closeEditDashboardModal(data)} + /> + this.closeImportDashboardModal(data)} + /> + this.closeDeleteDashboardModal(e)} + /> + +
+ ) + } +} + +export default DashboardTable; diff --git a/src/dashboard/dashboard.js b/src/dashboard/dashboard.js index ab6721f..8ed06ac 100644 --- a/src/dashboard/dashboard.js +++ b/src/dashboard/dashboard.js @@ -21,7 +21,7 @@ import Fullscreenable from 'react-fullscreenable'; import classNames from 'classnames'; import EditWidget from '../widget/edit-widget/edit-widget'; -import EditFiles from '../file/edit-files'; +import EditFilesDialog from '../file/edit-files'; import EditSignalMappingDialog from "../signal/edit-signal-mapping"; import WidgetContextMenu from '../widget/widget-context-menu'; import WidgetToolbox from '../widget/widget-toolbox'; @@ -560,7 +560,7 @@ class Dashboard extends Component { ics={this.state.ics} /> - - - this.selectUploadFile(event)} - /> - - - +
+
Add file
+ + + this.selectUploadFile(event)} /> + + + - + + +
+ +
diff --git a/src/ic/ic-action.js b/src/ic/ic-action.js index df4c17f..a33f73f 100644 --- a/src/ic/ic-action.js +++ b/src/ic/ic-action.js @@ -74,14 +74,14 @@ class ICAction extends React.Component { * see: https://villas.fein-aachen.org/doc/controller-protocol.html */ - if (newAction.action == "create" || newAction.action === "delete") { + if (newAction.action === "create" || newAction.action === "delete") { // prepare parameters for delete incl. correct IC id newAction["parameters"] = {}; - if (newAction.action == "delete") { + if (newAction.action === "delete") { newAction.parameters["uuid"] = ic.uuid; } - else if (newAction.action == "create") { + else if (newAction.action === "create") { newAction.parameters = ic.statusupdateraw.properties; } diff --git a/src/ic/ic-store.js b/src/ic/ic-store.js index d892c7d..bc9aa18 100644 --- a/src/ic/ic-store.js +++ b/src/ic/ic-store.js @@ -88,14 +88,14 @@ class InfrastructureComponentStore extends ArrayStore { // adapt URL for newly created result ID a.results.url = a.results.url.replace("RESULTID", action.data.result.id); a.results.url = ICsDataManager.makeURL(a.results.url); - a.results.url = window.location.host + a.results.url; + a.results.url = 'https://' + window.location.host + a.results.url; } if (a.model !== undefined && a.model != null && JSON.stringify(a.model) !== JSON.stringify({})) { // adapt URL(s) for model file let modelURLs = [] for (let url of a.model.url){ let modifiedURL = ICsDataManager.makeURL(url); - modifiedURL = window.location.host + modifiedURL; + modifiedURL = 'https://' + window.location.host + modifiedURL; modelURLs.push(modifiedURL) } a.model.url = modelURLs diff --git a/src/result/edit-result.js b/src/result/edit-result.js index 2b8b13f..aba2117 100644 --- a/src/result/edit-result.js +++ b/src/result/edit-result.js @@ -154,11 +154,13 @@ class EditResultDialog extends React.Component { + +
@@ -194,11 +196,14 @@ class EditResultDialog extends React.Component { this.selectUploadFile(event)} /> + +
diff --git a/src/result/result-table.js b/src/result/result-table.js new file mode 100644 index 0000000..cac8ab5 --- /dev/null +++ b/src/result/result-table.js @@ -0,0 +1,244 @@ +/** + * 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 JSZip from 'jszip'; +import {Button} from "react-bootstrap"; +import FileSaver from 'file-saver'; +import AppDispatcher from "../common/app-dispatcher"; +import IconButton from "../common/icon-button"; +import Table from "../common/table"; +import TableColumn from "../common/table-column"; +import DeleteDialog from "../common/dialogs/delete-dialog"; +import EditResultDialog from "./edit-result"; +import ResultConfigDialog from "./result-configs-dialog"; +import NewResultDialog from "./new-result"; + +class ResultTable extends Component { + + constructor() { + super(); + + this.state = { + editResultsModal: false, + modalResultsData: {}, + modalResultsIndex: 0, + newResultModal: false, + filesToDownload: [], + zipfiles: false, + resultNodl: 0, + resultConfigsModal: false, + modalResultConfigs: {}, + modalResultConfigsIndex: 0, + } + } + + componentDidUpdate(prevProps, prevState) { + // check whether file data has been loaded + if (this.state.filesToDownload && this.state.filesToDownload.length > 0) { + if (this.props.files !== prevProps.files) { + if (!this.state.zipfiles) { + let fileToDownload = this.props.files.filter(file => file.id === this.state.filesToDownload[0]) + if (fileToDownload.length === 1 && fileToDownload[0].data) { + const blob = new Blob([fileToDownload[0].data], { type: fileToDownload[0].type }); + FileSaver.saveAs(blob, fileToDownload[0].name); + this.setState({ filesToDownload: [] }); + } + } else { // zip and save one or more files (download all button) + let filesToDownload = this.props.files.filter(file => this.state.filesToDownload.includes(file.id) && file.data); + if (filesToDownload.length === this.state.filesToDownload.length) { // all requested files have been loaded + var zip = new JSZip(); + filesToDownload.forEach(file => { + zip.file(file.name, file.data); + }); + let zipname = "result_" + this.state.resultNodl + "_" + (new Date()).toISOString(); + zip.generateAsync({ type: "blob" }).then(function (content) { + saveAs(content, zipname); + }); + this.setState({ filesToDownload: [] }); + } + } + } + } + } + + closeNewResultModal(data) { + this.setState({ newResultModal: false }); + if (data) { + data["scenarioID"] = this.props.scenario.id; + AppDispatcher.dispatch({ + type: 'results/start-add', + data, + token: this.props.sessionToken, + }); + } + } + + closeEditResultsModal() { + this.setState({ editResultsModal: false }); + } + + downloadResultData(param) { + let toDownload = []; + let zip = false; + + if (typeof (param) === 'object') { // download all files + toDownload = param.resultFileIDs; + zip = true; + this.setState({ filesToDownload: toDownload, zipfiles: zip, resultNodl: param.id }); + } else { // download one file + toDownload.push(param); + this.setState({ filesToDownload: toDownload, zipfiles: zip }); + } + + toDownload.forEach(fileid => { + AppDispatcher.dispatch({ + type: 'files/start-download', + data: fileid, + token: this.props.sessionToken + }); + }); + } + + closeDeleteResultsModal(confirmDelete) { + this.setState({ deleteResultsModal: false }); + + if (confirmDelete === false) { + return; + } + + AppDispatcher.dispatch({ + type: 'results/start-remove', + data: this.state.modalResultsData, + token: this.props.sessionToken, + }); + } + + + openResultConfigSnapshots(result) { + if (result.configSnapshots === null || result.configSnapshots === undefined) { + this.setState({ + modalResultConfigs: {"configs": []}, + modalResultConfigsIndex: result.id, + resultConfigsModal: true + }); + } else { + this.setState({ + modalResultConfigs: result.configSnapshots, + modalResultConfigsIndex: result.id, + resultConfigsModal: true + }); + } + } + + closeResultConfigSnapshots() { + this.setState({ resultConfigsModal: false }); + } + + modifyResultNoColumn(id, result) { + return + } + + render() { + + return ( +
+ {/*Result table*/} +

Results + + this.setState({ newResultModal: true })} + icon='plus' + /> + +

+ + + this.modifyResultNoColumn(id, result)} + width={70} + /> + + + + this.downloadResultData(index)} + width={300} + /> + this.setState({ editResultsModal: true, modalResultsIndex: index })} + onDownloadAll={(index) => this.downloadResultData(this.props.results[index])} + onDelete={(index) => this.setState({ deleteResultsModal: true, modalResultsData: this.props.results[index], modalResultsIndex: index })} + /> +
+ + + this.closeDeleteResultsModal(e)} + /> + + this.closeNewResultModal(data)} + /> +
+ ) + } +} + +export default ResultTable; diff --git a/src/scenario/scenario-users-table.js b/src/scenario/scenario-users-table.js new file mode 100644 index 0000000..c93b356 --- /dev/null +++ b/src/scenario/scenario-users-table.js @@ -0,0 +1,169 @@ +/** + * 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 {Button, Form, InputGroup} from "react-bootstrap"; +import {Redirect} from "react-router-dom"; +import Table from "../common/table"; +import TableColumn from "../common/table-column"; +import Icon from "../common/icon"; +import DeleteDialog from "../common/dialogs/delete-dialog"; +import AppDispatcher from "../common/app-dispatcher"; + +class ScenarioUsersTable extends Component { + + constructor(props) { + super(props); + + this.state = { + userToAdd: '', + deleteUserName: '', + deleteUserModal: false, + goToScenarios: false + } + } + + onUserInputChange(e) { + this.setState({ userToAdd: e.target.value }); + } + + addUser() { + AppDispatcher.dispatch({ + type: 'scenarios/add-user', + data: this.props.scenario.id, + username: this.state.userToAdd, + token: this.props.sessionToken + }); + + this.setState({ userToAdd: '' }); + } + + closeDeleteUserModal() { + let scenarioID = this.props.scenario.id; + if (this.state.deleteUserName === this.props.currentUser.username) { + AppDispatcher.dispatch({ + type: 'scenarios/remove-user', + data: scenarioID, + username: this.state.deleteUserName, + token: this.props.sessionToken, + ownuser: true + }); + this.setState({ goToScenarios: true }); + } else { + AppDispatcher.dispatch({ + type: 'scenarios/remove-user', + data: scenarioID, + username: this.state.deleteUserName, + token: this.props.sessionToken, + ownuser: false + }); + } + this.setState({ deleteUserModal: false }); + } + + + render() { + if (this.state.goToScenarios) { + console.log("redirect to scenario overview") + return (); + } + + const altButtonStyle = { + marginLeft: '10px', + } + + const iconStyle = { + height: '30px', + width: '30px' + } + + return ( +
+ {/*Scenario Users table*/} +

Users sharing this scenario

+ + {this.props.currentUser.role === "Admin" ? + + : <> + } + + + this.setState({ + deleteUserModal: true, + deleteUserName: this.props.scenario.users[index].username, + modalUserIndex: index + })} + /> +
+ + + this.onUserInputChange(e)} + value={this.state.userToAdd} + type="text" + /> + + + + + + +
+
+ + this.closeDeleteUserModal(c)} + /> +
+ ) + } +} + +export default ScenarioUsersTable; + diff --git a/src/scenario/scenario.js b/src/scenario/scenario.js index 73767c9..01dfd60 100644 --- a/src/scenario/scenario.js +++ b/src/scenario/scenario.js @@ -17,514 +17,86 @@ import React from 'react'; import { Container } from 'flux/utils'; -import { Button, InputGroup, Form } from 'react-bootstrap'; -import FileSaver from 'file-saver'; +import AppDispatcher from '../common/app-dispatcher'; +import IconButton from '../common/icon-button'; import ScenarioStore from './scenario-store'; import ICStore from '../ic/ic-store'; import DashboardStore from '../dashboard/dashboard-store'; import ConfigStore from '../componentconfig/config-store'; import SignalStore from '../signal/signal-store' -import AppDispatcher from '../common/app-dispatcher'; - -import Icon from '../common/icon'; -import Table from '../common/table'; -import TableColumn from '../common/table-column'; -import IconButton from '../common/icon-button'; -import ImportConfigDialog from '../componentconfig/import-config'; -import ImportDashboardDialog from "../dashboard/import-dashboard"; -import NewDashboardDialog from "../dashboard/new-dashboard"; -import EditDashboardDialog from '../dashboard/edit-dashboard'; -import EditFiles from '../file/edit-files' -import NewResultDialog from '../result/new-result'; -import EditResultDialog from '../result/edit-result'; -import ResultConfigDialog from '../result/result-configs-dialog'; - - -import ICAction from '../ic/ic-action'; -import DeleteDialog from '../common/dialogs/delete-dialog'; -import EditConfigDialog from "../componentconfig/edit-config"; -import EditSignalMappingDialog from "../signal/edit-signal-mapping"; import FileStore from "../file/file-store" import WidgetStore from "../widget/widget-store"; import ResultStore from "../result/result-store" -import { Redirect } from 'react-router-dom'; -import NotificationsDataManager from "../common/data-managers/notifications-data-manager"; -var JSZip = require("jszip"); +import DashboardTable from '../dashboard/dashboard-table' +import ResultTable from "../result/result-table"; +import ConfigTable from "../componentconfig/config-table"; +import EditFilesDialog from '../file/edit-files' +import ScenarioUsersTable from "./scenario-users-table"; + class Scenario extends React.Component { + constructor(props) { + super(props); + + this.state = { + filesEditModal: false, + filesEditSaveState: [], + } + } + + static calculateState(prevState, props){ + let scenarioID = parseInt(props.match.params.scenario, 10) + + return{ + scenario: ScenarioStore.getState().find(s => s.id === scenarioID), + results: ResultStore.getState().filter(result => result.scenarioID === scenarioID), + sessionToken: localStorage.getItem("token"), + configs: ConfigStore.getState().filter(config => config.scenarioID === scenarioID), + widgets: WidgetStore.getState(), + dashboards: DashboardStore.getState().filter(dashb => dashb.scenarioID === scenarioID), + signals: SignalStore.getState(), + currentUser: JSON.parse(localStorage.getItem("currentUser")), + files: FileStore.getState().filter(file => file.scenarioID === scenarioID), + ics: ICStore.getState(), + } + } + static getStores() { return [ScenarioStore, ConfigStore, DashboardStore, ICStore, SignalStore, FileStore, WidgetStore, ResultStore]; } - static calculateState(prevState, props) { - if (prevState == null) { - prevState = {}; - } - - // get selected scenario - const sessionToken = localStorage.getItem("token"); - - const scenario = ScenarioStore.getState().find(s => s.id === parseInt(props.match.params.scenario, 10)); - if (scenario === undefined) { - AppDispatcher.dispatch({ - type: 'scenarios/start-load', - data: props.match.params.scenario, - token: sessionToken - }); - } - - // obtain all component configurations of a scenario - let configs = ConfigStore.getState().filter(config => config.scenarioID === parseInt(props.match.params.scenario, 10)); - let editConfigModal = prevState.editConfigModal || false; - let modalConfigData = (prevState.modalConfigData !== {} && prevState.modalConfigData !== undefined) ? prevState.modalConfigData : {}; - let modalConfigIndex = 0; - - if ((typeof prevState.configs !== "undefined") && (prevState.newConfig === true) && (configs.length !== prevState.configs.length)) { - let index = configs.length - 1; - editConfigModal = true; - modalConfigData = configs[index]; - modalConfigIndex = index; - } - - let results = ResultStore.getState().filter(result => result.scenarioID === parseInt(props.match.params.scenario, 10)); - - return { - scenario, - results, - sessionToken, - configs, - editConfigModal, - modalConfigData, - modalConfigIndex, - dashboards: DashboardStore.getState().filter(dashb => dashb.scenarioID === parseInt(props.match.params.scenario, 10)), - signals: SignalStore.getState(), - currentUser: JSON.parse(localStorage.getItem("currentUser")), - files: FileStore.getState().filter(file => file.scenarioID === parseInt(props.match.params.scenario, 10)), - - ics: ICStore.getState(), - ExternalICInUse: false, - - deleteConfigModal: false, - importConfigModal: false, - newConfig: prevState.newConfig || false, - selectedConfigs: prevState.selectedConfigs || [], - filesEditModal: prevState.filesEditModal || false, - filesEditSaveState: prevState.filesEditSaveState || [], - - editResultsModal: prevState.editResultsModal || false, - modalResultsData: {}, - modalResultsIndex: prevState.modalResultsIndex, - newResultModal: false, - filesToDownload: prevState.filesToDownload, - zipfiles: prevState.zipfiles || false, - resultNodl: prevState.resultNodl, - resultConfigsModal: false, - modalResultConfigs: {}, - modalResultConfigsIndex: 0, - - editOutputSignalsModal: prevState.editOutputSignalsModal || false, - editInputSignalsModal: prevState.editInputSignalsModal || false, - - newDashboardModal: false, - dashboardEditModal: prevState.dashboardEditModal || false, - deleteDashboardModal: false, - importDashboardModal: false, - modalDashboardData: {}, - - userToAdd: '', - deleteUserName: '', - deleteUserModal: false, - goToScenarios: false - } - } - componentDidMount() { + let token = localStorage.getItem("token") + let scenarioID = parseInt(this.props.match.params.scenario, 10) //load selected scenario AppDispatcher.dispatch({ type: 'scenarios/start-load', - data: parseInt(this.props.match.params.scenario, 10), - token: this.state.sessionToken + data: scenarioID, + token: token }); - AppDispatcher.dispatch({ type: 'scenarios/start-load-users', - data: parseInt(this.props.match.params.scenario, 10), - token: this.state.sessionToken + data: scenarioID, + token: token }); // load ICs to enable that component configs and dashboards work with them AppDispatcher.dispatch({ type: 'ics/start-load', - token: this.state.sessionToken - }); - } - - componentDidUpdate(prevProps, prevState) { - // check whether file data has been loaded - if (this.state.filesToDownload && this.state.filesToDownload.length > 0) { - if (this.state.files != prevState.files) { - if (!this.state.zipfiles) { - let fileToDownload = FileStore.getState().filter(file => file.id === this.state.filesToDownload[0]) - if (fileToDownload.length === 1 && fileToDownload[0].data) { - const blob = new Blob([fileToDownload[0].data], { type: fileToDownload[0].type }); - FileSaver.saveAs(blob, fileToDownload[0].name); - this.setState({ filesToDownload: [] }); - } - } else { // zip and save one or more files (download all button) - let filesToDownload = FileStore.getState().filter(file => this.state.filesToDownload.includes(file.id) && file.data); - if (filesToDownload.length === this.state.filesToDownload.length) { // all requested files have been loaded - var zip = new JSZip(); - filesToDownload.forEach(file => { - zip.file(file.name, file.data); - }); - let zipname = "result_" + this.state.resultNodl + "_" + (new Date()).toISOString(); - zip.generateAsync({ type: "blob" }).then(function (content) { - saveAs(content, zipname); - }); - this.setState({ filesToDownload: [] }); - } - } - } - } - } - - /* ############################################## - * User modification methods - ############################################## */ - - onUserInputChange(e) { - this.setState({ userToAdd: e.target.value }); - } - - addUser() { - AppDispatcher.dispatch({ - type: 'scenarios/add-user', - data: this.state.scenario.id, - username: this.state.userToAdd, - token: this.state.sessionToken - }); - - this.setState({ userToAdd: '' }); - } - - closeDeleteUserModal() { - let scenarioID = this.state.scenario.id; - if (this.state.deleteUserName === this.state.currentUser.username) { - AppDispatcher.dispatch({ - type: 'scenarios/remove-user', - data: scenarioID, - username: this.state.deleteUserName, - token: this.state.sessionToken, - ownuser: true - }); - this.setState({ goToScenarios: true }); - } else { - AppDispatcher.dispatch({ - type: 'scenarios/remove-user', - data: scenarioID, - username: this.state.deleteUserName, - token: this.state.sessionToken, - ownuser: false - }); - } - this.setState({ deleteUserModal: false }); - } - - /* ############################################## - * Component Configuration modification methods - ############################################## */ - - addConfig() { - const config = { - scenarioID: this.state.scenario.id, - name: 'New Component Configuration', - icID: this.state.ics.length > 0 ? this.state.ics[0].id : null, - startParameters: {}, - }; - - AppDispatcher.dispatch({ - type: 'configs/start-add', - data: config, - token: this.state.sessionToken - }); - - this.setState({ newConfig: true }); - - } - - closeEditConfigModal(data) { - this.setState({ editConfigModal: false, newConfig: false }); - - if (data) { - AppDispatcher.dispatch({ - type: 'configs/start-edit', - data: data, - token: this.state.sessionToken, - }); - } - } - - closeDeleteConfigModal(confirmDelete) { - this.setState({ deleteConfigModal: false }); - - if (confirmDelete === false) { - return; - } - - AppDispatcher.dispatch({ - type: 'configs/start-remove', - data: this.state.modalConfigData, - token: this.state.sessionToken - }); - } - - importConfig(data) { - this.setState({ importConfigModal: false }); - - if (data == null) { - return; - } - - let newConfig = JSON.parse(JSON.stringify(data.config)) - - newConfig["scenarioID"] = this.state.scenario.id; - newConfig.name = data.name; - - AppDispatcher.dispatch({ - type: 'configs/start-add', - data: newConfig, - token: this.state.sessionToken - }); - } - - copyConfig(index) { - let config = JSON.parse(JSON.stringify(this.state.configs[index])); - - let signals = JSON.parse(JSON.stringify(SignalStore.getState().filter(s => s.configID === parseInt(config.id, 10)))); - signals.forEach((signal) => { - delete signal.configID; - delete signal.id; - }) - - // two separate lists for inputMapping and outputMapping - let inputSignals = signals.filter(s => s.direction === 'in'); - let outputSignals = signals.filter(s => s.direction === 'out'); - - // add signal mappings to config - config["inputMapping"] = inputSignals; - config["outputMapping"] = outputSignals; - - delete config.id; - delete config.scenarioID; - delete config.inputLength; - delete config.outputLength; - - return config; - } - - exportConfig(index) { - let config = this.copyConfig(index); - - // show save dialog - const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); - FileSaver.saveAs(blob, 'config-' + config.name + '.json'); - } - - duplicateConfig(index) { - let newConfig = this.copyConfig(index); - newConfig["scenarioID"] = this.state.scenario.id; - newConfig.name = newConfig.name + '_copy'; - - AppDispatcher.dispatch({ - type: 'configs/start-add', - data: newConfig, - token: this.state.sessionToken - }); - } - - onConfigChecked(index, event) { - const selectedConfigs = Object.assign([], this.state.selectedConfigs); - for (let key in selectedConfigs) { - if (selectedConfigs[key] === index) { - // update existing entry - if (event.target.checked) { - return; - } - - selectedConfigs.splice(key, 1); - - this.setState({ selectedConfigs: selectedConfigs }); - return; - } - } - - // add new entry - if (event.target.checked === false) { - return; - } - - selectedConfigs.push(index); - this.setState({ selectedConfigs: selectedConfigs }); - } - - usesExternalIC(index) { - let icID = this.state.configs[index].icID; - - let ic = null; - for (let component of this.state.ics) { - if (component.id === icID) { - ic = component; - } - } - - if (ic == null) { - return false; - } - - if (ic.managedexternally === true) { - this.setState({ ExternalICInUse: true }) - return true - } - - return false - } - - getICName(icID) { - for (let ic of this.state.ics) { - if (ic.id === icID) { - return ic.name || ic.uuid; - } - } - } - - /* ############################################## - * Dashboard modification methods - ############################################## */ - - closeNewDashboardModal(data) { - this.setState({ newDashboardModal: false }); - if (data) { - // TODO: 'newDashboard' not used, check this - let newDashboard = data; - // add default grid value and scenarioID - newDashboard["grid"] = 15; - newDashboard["scenarioID"] = this.state.scenario.id; - - AppDispatcher.dispatch({ - type: 'dashboards/start-add', - data, - token: this.state.sessionToken, - }); - } - } - - closeEditDashboardModal(data) { - this.setState({ dashboardEditModal: false }); - - let editDashboard = this.state.modalDashboardData; - - if (data != null) { - editDashboard.name = data.name; - AppDispatcher.dispatch({ - type: 'dashboards/start-edit', - data: editDashboard, - token: this.state.sessionToken - }); - } - } - - closeDeleteDashboardModal(confirmDelete) { - this.setState({ deleteDashboardModal: false }); - - if (confirmDelete === false) { - return; - } - - AppDispatcher.dispatch({ - type: 'dashboards/start-remove', - data: this.state.modalDashboardData, - token: this.state.sessionToken, - }); - } - - closeImportDashboardModal(data) { - this.setState({ importDashboardModal: false }); - - if (data) { - let newDashboard = JSON.parse(JSON.stringify(data)); - newDashboard["scenarioID"] = this.state.scenario.id; - - AppDispatcher.dispatch({ - type: 'dashboards/start-add', - data: newDashboard, - token: this.state.sessionToken, - }); - } - } - - copyDashboard(index) { - let dashboard = JSON.parse(JSON.stringify(this.state.dashboards[index])); - - let widgets = JSON.parse(JSON.stringify(WidgetStore.getState().filter(w => w.dashboardID === parseInt(dashboard.id, 10)))); - widgets.forEach((widget) => { - delete widget.dashboardID; - delete widget.id; - }) - dashboard["widgets"] = widgets; - delete dashboard.scenarioID; - delete dashboard.id; - - return dashboard; - } - - exportDashboard(index) { - let dashboard = this.copyDashboard(index); - - // show save dialog - const blob = new Blob([JSON.stringify(dashboard, null, 2)], { type: 'application/json' }); - FileSaver.saveAs(blob, 'dashboard - ' + dashboard.name + '.json'); - } - - duplicateDashboard(index) { - let newDashboard = this.copyDashboard(index); - newDashboard.scenarioID = this.state.scenario.id; - newDashboard.name = newDashboard.name + '_copy'; - - AppDispatcher.dispatch({ - type: 'dashboards/start-add', - data: newDashboard, - token: this.state.sessionToken, + token: token }); } /* ############################################## - * Signal modification methods + * File modification methods ############################################## */ - closeEditSignalsModal(direction) { - - // reload the config - AppDispatcher.dispatch({ - type: 'configs/start-load', - data: this.state.modalConfigData.id, - token: this.state.sessionToken - }); - - if (direction === "in") { - this.setState({ editInputSignalsModal: false }); - } else if (direction === "out") { - this.setState({ editOutputSignalsModal: false }); - } - - - } - onEditFiles() { let tempFiles = []; this.state.files.forEach(file => { @@ -538,199 +110,18 @@ class Scenario extends React.Component { closeEditFiles() { this.setState({ filesEditModal: false }); - // TODO do we need this if the dispatches happen in the dialog? } - signalsAutoConf(index) { - let componentConfig = this.state.configs[index]; - // determine apiurl of infrastructure component - let ic = this.state.ics.find(ic => ic.id === componentConfig.icID) - if (!ic.type.includes("villas-node")) { - let message = "Cannot autoconfigure signals for IC type " + ic.type + " of category " + ic.category + ". This is only possible for gateway ICs of type 'VILLASnode'." - console.warn(message); - - const SIGNAL_AUTOCONF_WARN_NOTIFICATION = { - title: 'Failed to load signal config for IC ' + ic.name, - message: message, - level: 'warning' - }; - NotificationsDataManager.addNotification(SIGNAL_AUTOCONF_WARN_NOTIFICATION); - return; - } - - let splitWebsocketURL = ic.websocketurl.split("/") - - AppDispatcher.dispatch({ - type: 'signals/start-autoconfig', - url: ic.apiurl + "/nodes", - socketname: splitWebsocketURL[splitWebsocketURL.length - 1], - token: this.state.sessionToken, - configID: componentConfig.id - }); - - } - - 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); - }); - } - - /* ############################################## - * File modification methods - ############################################## */ - - getListOfFiles(files, fileIDs) { - let fileList = []; - - for (let id of fileIDs) { - for (let file of files) { - if (file.id === id) { - fileList.push(file.name) - } - } - } - - return fileList.join(';'); - } - - /* ############################################## - * Result modification methods - ############################################## */ - - closeNewResultModal(data) { - this.setState({ newResultModal: false }); - if (data) { - data["scenarioID"] = this.state.scenario.id; - AppDispatcher.dispatch({ - type: 'results/start-add', - data, - token: this.state.sessionToken, - }); - } - } - - closeEditResultsModal() { - this.setState({ editResultsModal: false }); - } - - downloadResultData(param) { - let toDownload = []; - let zip = false; - - if (typeof (param) === 'object') { // download all files - toDownload = param.resultFileIDs; - zip = true; - this.setState({ filesToDownload: toDownload, zipfiles: zip, resultNodl: param.id }); - } else { // download one file - toDownload.push(param); - this.setState({ filesToDownload: toDownload, zipfiles: zip }); - } - - toDownload.forEach(fileid => { - AppDispatcher.dispatch({ - type: 'files/start-download', - data: fileid, - token: this.state.sessionToken - }); - }); - } - - closeDeleteResultsModal(confirmDelete) { - this.setState({ deleteResultsModal: false }); - - if (confirmDelete === false) { - return; - } - - AppDispatcher.dispatch({ - type: 'results/start-remove', - data: this.state.modalResultsData, - token: this.state.sessionToken, - }); - } - - openResultConfigSnaphots(result) { - if (result.configSnapshots === null || result.configSnapshots === undefined) { - this.setState({ - modalResultConfigs: {"configs": []}, - modalResultConfigsIndex: result.id, - resultConfigsModal: true - }); - } else { - this.setState({ - modalResultConfigs: result.configSnapshots, - modalResultConfigsIndex: result.id, - resultConfigsModal: true - }); - } - } - - closeResultConfigSnapshots() { - this.setState({ resultConfigsModal: false }); - } - - modifyResultNoColumn(id, result) { - return - } - - startPintura(configIndex) { - let config = this.state.configs[configIndex]; - - // get xml / CIM file - let files = [] - for (let id of config.fileIDs) { - for (let file of this.state.files) { - if (file.id === id && ['xml'].some(e => file.type.includes(e))) { - files.push(file); - } - } - } - - if (files.length > 1) { - // more than one CIM file... - console.warn("There is more than one CIM file selected in this component configuration. I will open them all in a separate tab.") - } - - let baseURL = 'aaa.bbb.ccc.ddd/api/v2/files/' - for (let file of files) { - // endpoint param serves for download and upload of CIM file, token is required for authentication - let params = { - token: this.state.sessionToken, - endpoint: baseURL + String(file.id), - } - - // TODO start Pintura for editing CIM/ XML file from here - console.warn("Starting Pintura... and nothing happens so far :-) ", params) - } - } - - /* ############################################## * Render method ############################################## */ render() { - if (this.state.goToScenarios) { - console.log("redirect to scenario overview") - return (); - } - - const altButtonStyle = { - marginLeft: '10px', - } const tableHeadingStyle = { paddingTop: '30px' } - const iconStyle = { - height: '30px', - width: '30px' - } - if (this.state.scenario === undefined) { return

Loading Scenario...

; } @@ -746,7 +137,7 @@ class Scenario extends React.Component {

{this.state.scenario.name}

- - {/*Component Configurations table*/} -

Component Configurations - - this.addConfig()} - icon='plus' - /> - this.setState({ importConfigModal: true })} - icon='upload' - /> - -

- - !this.usesExternalIC(index)} - onChecked={(index, event) => this.onConfigChecked(index, event)} - width='30' - /> - {this.state.currentUser.role === "Admin" ? - - : <> - } - - this.setState({ editOutputSignalsModal: true, modalConfigData: this.state.configs[index], modalConfigIndex: index })} - /> - this.setState({ editInputSignalsModal: true, modalConfigData: this.state.configs[index], modalConfigIndex: index })} - /> - this.signalsAutoConf(index)} - /> - this.getICName(icID)} - /> - this.setState({ editConfigModal: true, modalConfigData: this.state.configs[index], modalConfigIndex: index })} - onDelete={(index) => this.setState({ deleteConfigModal: true, modalConfigData: this.state.configs[index], modalConfigIndex: index })} - onExport={index => this.exportConfig(index)} - onDuplicate={index => this.duplicateConfig(index)} - /> -
- - {this.state.ExternalICInUse ? -
- this.copyConfig(index)} - token = {this.state.sessionToken} - 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' } } - ]} /> -
- :
- } - -
- - this.closeEditConfigModal(data)} - config={this.state.modalConfigData} - ics={this.state.ics} + - this.importConfig(data)} ics={this.state.ics} - /> - this.closeDeleteConfigModal(c)} - /> - this.closeEditSignalsModal(direction)} - direction="Output" signals={this.state.signals} - configID={this.state.modalConfigData.id} + scenario={this.state.scenario} sessionToken={this.state.sessionToken} + currentUser={this.state.currentUser} + tableHeadingStyle={tableHeadingStyle} /> - this.closeEditSignalsModal(direction)} - direction="Input" - signals={this.state.signals} - configID={this.state.modalConfigData.id} + + - {/*Dashboard table*/} -

Dashboards - - this.setState({ newDashboardModal: true })} - icon='plus' - /> - this.setState({ importDashboardModal: true })} - icon='upload' - /> - -

- - {this.state.currentUser.role === "Admin" ? - - : <> - } - - - this.setState({ dashboardEditModal: true, modalDashboardData: this.state.dashboards[index] })} - onDelete={(index) => this.setState({ deleteDashboardModal: true, modalDashboardData: this.state.dashboards[index], modalDashboardIndex: index })} - onExport={index => this.exportDashboard(index)} - onDuplicate={index => this.duplicateDashboard(index)} - /> -
- - this.closeNewDashboardModal(data)} - /> - this.closeEditDashboardModal(data)} - /> - this.closeImportDashboardModal(data)} - /> - this.closeDeleteDashboardModal(e)} - /> - - {/*Result table*/} -

Results - - this.setState({ newResultModal: true })} - icon='plus' - /> - -

- - - this.modifyResultNoColumn(id, result)} - /> - - - - this.downloadResultData(index)} - /> - this.setState({ editResultsModal: true, modalResultsIndex: index })} - onDownloadAll={(index) => this.downloadResultData(this.state.results[index])} - onDelete={(index) => this.setState({ deleteResultsModal: true, modalResultsData: this.state.results[index], modalResultsIndex: index })} - /> -
- - - this.closeDeleteResultsModal(e)} - /> - - this.closeNewResultModal(data)} + files={this.state.files} + scenario={this.state.scenario} + sessionToken={this.state.sessionToken} + tableHeadingStyle={tableHeadingStyle} /> - {/*Scenario Users table*/} -

Users sharing this scenario

- - {this.state.currentUser.role === "Admin" ? - - : <> - } - - - this.setState({ - deleteUserModal: true, - deleteUserName: this.state.scenario.users[index].username, - modalUserIndex: index - })} - /> -
- - - this.onUserInputChange(e)} - value={this.state.userToAdd} - type="text" - /> - - - - - - -
-
- - this.closeDeleteUserModal(c)} + -
; + +
} } diff --git a/src/user/login-complete.js b/src/user/login-complete.js index 9ed2fc7..7df0d00 100644 --- a/src/user/login-complete.js +++ b/src/user/login-complete.js @@ -69,7 +69,7 @@ class LoginComplete extends React.Component { } startTimer() { - if (this.timer == 0 && this.state.secondsToWait > 0) { + if (this.timer === 0 && this.state.secondsToWait > 0) { // call function 'countDown' every 1000ms this.timer = setInterval(this.countDown, 1000); } @@ -80,7 +80,7 @@ class LoginComplete extends React.Component { this.setState({secondsToWait: seconds}); // waiting time over, stop counting down - if (seconds == 0) { + if (seconds === 0) { clearInterval(this.timer); } } @@ -90,11 +90,11 @@ class LoginComplete extends React.Component { this.stopTimer(); return (); } - else if (this.state.secondsToWait == 0) { + else if (this.state.secondsToWait === 0) { this.stopTimer(); return (); } else { - return
+ return