diff --git a/src/app.js b/src/app.js index 0a895b7..fe71133 100644 --- a/src/app.js +++ b/src/app.js @@ -46,6 +46,11 @@ class App extends React.Component { constructor(props) { super(props); + + AppDispatcher.dispatch({ + type: 'config/load', + }); + this.state = { showSidebarMenu: false, } diff --git a/src/common/data-managers/notifications-factory.js b/src/common/data-managers/notifications-factory.js index ece1e8b..a032db0 100644 --- a/src/common/data-managers/notifications-factory.js +++ b/src/common/data-managers/notifications-factory.js @@ -138,6 +138,14 @@ class NotificationsFactory { }; } + static ACTION_INFO() { + return { + title: 'Action successfully requested', + level: 'info' + }; + } + + } export default NotificationsFactory; diff --git a/src/common/menu-sidebar.js b/src/common/menu-sidebar.js index 1b7208e..55f1580 100644 --- a/src/common/menu-sidebar.js +++ b/src/common/menu-sidebar.js @@ -18,20 +18,58 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; import Branding from '../branding/branding'; +import { Container } from 'flux/utils'; +import LoginStore from '../user/login-store'; +import AppDispatcher from './app-dispatcher'; class SidebarMenu extends React.Component { + constructor(props) { + super(props); + + this.state = { + externalAuth: false, + logoutLink: "", + } + } + + static getStores() { + return [LoginStore] + } + + static calculateState(prevState, props) { + let config = LoginStore.getState().config; + let logout_url = _.get(config, ['authentication', 'logout_url']); + + if (logout_url) { + return { + externalAuth: true, + logoutLink: logout_url, + } + } + + return { + externalAuth: false, + logoutLink: "/logout", + } + } + + logout() { + AppDispatcher.dispatch({ + type: 'users/logout' + }); + // The Login Store is deleted automatically + + // discard login token and current User + localStorage.setItem('token', ''); + localStorage.setItem('currentUser', ''); + } + render() { const brand = Branding.instance.brand; console.log(brand.links) let links = [] - /*++++ - for (var key of Object.keys(brand.links) ) { - console.log(`${key}: ${brand.links[key]}`); - links.push(
  • {key}
  • ); - }*/ - if (brand.links) { Object.keys(brand.links).forEach(key => { console.log(`${key}: ${brand.links[key]}`); @@ -39,6 +77,33 @@ class SidebarMenu extends React.Component { }) } + if (this.state.externalAuth) { + return ( +
    +

    Menu

    + + { + links.length > 0 ? +
    +

    +

    Links

    + +
    + : '' + } +
    + ); + } return (
    @@ -52,7 +117,7 @@ class SidebarMenu extends React.Component {
  • User Management
  • : '' } -
  • Logout
  • +
  • Logout
  • { @@ -69,4 +134,5 @@ class SidebarMenu extends React.Component { } } -export default SidebarMenu; +let fluxContainerConverter = require('../common/FluxContainerConverter'); +export default Container.create(fluxContainerConverter.convert(SidebarMenu)); \ No newline at end of file diff --git a/src/common/table-column.js b/src/common/table-column.js index 9d77606..460eb4b 100644 --- a/src/common/table-column.js +++ b/src/common/table-column.js @@ -23,7 +23,9 @@ class TableColumn extends Component { modifier: null, width: null, editButton: false, + showEditButton: null, deleteButton: false, + showDeleteButton: null, exportButton: false, duplicateButton: false, link: '/', diff --git a/src/common/table.js b/src/common/table.js index 8774d10..f54335e 100644 --- a/src/common/table.js +++ b/src/common/table.js @@ -79,9 +79,19 @@ class CustomTable extends Component { cell.push(); } else if (linkKey === 'filebuttons') { content.forEach((contentvalue, contentkey) => { - cell.push(Download {contentvalue}} > - ); + cell.push( + Download {contentvalue}} > + + ); }); } else { cell.push(content); @@ -114,17 +124,34 @@ class CustomTable extends Component { } // add buttons - if (child.props.editButton) { - let disable = (typeof data.managedexternally !== "undefined" && data.managedexternally); - cell.push({disable ? "Externally managed ICs cannot be edited" : "edit"} } > - ); + let showEditButton = true + if (child.props.showEditButton !== null) + { + showEditButton = child.props.showEditButton(index) } + if(showEditButton){ + if (child.props.editButton) { + cell.push( + Edit }> + + ); + } + } + if (child.props.checkbox) { const checkboxKey = child.props.checkboxKey; let isDisabled = false; if (child.props.checkboxDisabled != null){ - isDisabled = !child.props.checkboxDisabled(index) + isDisabled = child.props.checkboxDisabled(index) } cell.push( Export } > - ); + cell.push( + Export } > + + ); } if (child.props.duplicateButton) { - cell.push( Duplicate } > - ); + cell.push( + Duplicate } > + + ); } if (child.props.addRemoveFilesButton) { - cell.push(Add/remove File(s)} > - ); + cell.push( + Add/remove File(s)} > + + ); } if (child.props.downloadAllButton) { - cell.push(Download All Files} > - ); + cell.push( + Download All Files} > + + ); } - if (child.props.deleteButton) { - cell.push( Delete } > - ); + let showDeleteButton = true; + if (child.props.showDeleteButton !== null){ + showDeleteButton = child.props.showDeleteButton(index) } + if (showDeleteButton){ + if (child.props.deleteButton) { + cell.push( + Delete } > + + ); + } + } + + + return cell; } // addCell @@ -244,9 +330,19 @@ class CustomTable extends Component { onCellBlur: () => { } }; - return ( + return ( {(this.state.editCell[0] === cellIndex && this.state.editCell[1] === rowIndex) ? ( - children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} ref={ref => { this.activeInput = ref; }} /> + children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} + ref={ref => { this.activeInput = ref; }} /> ) : ( {cell.map((element, elementIndex) => ( diff --git a/src/config-reader.js b/src/config-reader.js new file mode 100644 index 0000000..1893910 --- /dev/null +++ b/src/config-reader.js @@ -0,0 +1,43 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import RestDataManager from './common/data-managers/rest-data-manager'; +import RestAPI from './common/api/rest-api'; +import AppDispatcher from './common/app-dispatcher'; + + +class ConfigReader extends RestDataManager { + constructor() { + super('config', '/config'); + } + + loadConfig() { + RestAPI.get(this.makeURL('/config'), null).then(response => { + AppDispatcher.dispatch({ + type: 'config/loaded', + data: response, + }); + }).catch(error => { + AppDispatcher.dispatch({ + type: 'config/load-error', + error: error, + }); + }); + } +}; + +export default new ConfigReader(); \ No newline at end of file diff --git a/src/config.js b/src/config.js index 6ae36be..e49e4c5 100644 --- a/src/config.js +++ b/src/config.js @@ -23,4 +23,4 @@ const config = { branding: 'villasweb', } -export default config +export default config \ No newline at end of file diff --git a/src/ic/ic-action.js b/src/ic/ic-action.js index b0333b8..2d08af4 100644 --- a/src/ic/ic-action.js +++ b/src/ic/ic-action.js @@ -17,6 +17,9 @@ import React from 'react'; import { Button, DropdownButton, Dropdown, InputGroup, FormControl } from 'react-bootstrap'; +import AppDispatcher from "../common/app-dispatcher"; +import NotificationsFactory from "../common/data-managers/notifications-factory"; +import NotificationsDataManager from "../common/data-managers/notifications-data-manager"; class ICAction extends React.Component { constructor(props) { @@ -47,9 +50,187 @@ class ICAction extends React.Component { }; } } + return null } + runAction(action, when) { + + if (action.data.action === 'none') { + console.warn("No command selected. Nothing was sent."); + return; + } + + if (!this.props.hasConfigs){ + let newAction = {}; + newAction["action"] = action.data.action + newAction["when"] = when + + for (let index of this.props.selectedICs) { + let ic = this.props.ics[index]; + let icID = ic.id; + + /* VILLAScontroller protocol + see: https://villas.fein-aachen.org/doc/controller-protocol.html + + RESET SHUTDOWN + { + "action": "reset/shutdown/stop/pause/resume" + "when": "1234567" + } + + DELETE + { + "action": "delete" + "parameters":{ + "uuid": "uuid-of-the-manager-for-this-IC" + } + "when": "1234567" + } + + CREATE is not possible within ICAction (see add IC) + */ + + if (newAction.action === "delete"){ + // prepare parameters for delete incl. correct IC id + newAction["parameters"] = {}; + newAction.parameters["uuid"] = ic.uuid; + // get the ID of the manager IC + let managerIC = null; + for (let i of this.props.ics){ + if (i.uuid === ic.manager){ + managerIC = i; + } + } + if (managerIC == null){ + NotificationsDataManager.addNotification(NotificationsFactory.DELETE_ERROR("Could not find manager IC with UUID " + ic.manager)); + continue; + } + + icID = managerIC.id; // send delete action to manager of IC + } + + AppDispatcher.dispatch({ + type: 'ics/start-action', + icid: icID, + action: newAction, + result: null, + token: this.props.token + }); + + } // end for loop over selected ICs + } else { + + /*VILLAScontoller protocol + see: https://villas.fein-aachen.org/doc/controller-protocol.html + * + * STOP PAUSE RESUME + { + "action": "reset/shutdown/stop/pause/resume" + "when": "1234567" + } + * + * START + { + "action": "start" + "when": 1234567 + "parameters": { + Start parameters for this IC as configured in the component config + } + "model": { + "type": "url" + "url": "https://villas.k8s.eonerc.rwth-aachen.de/api/v2/files/{fileID}" where fileID is the model file configured in the component config + "token": "asessiontoken" + } + "results":{ + "type": "url" + "url" : "https://villas.k8s.eonerc.rwth-aachen.de/api/v2/results/{resultID}/file" where resultID is the ID of the result created for this run + "token": "asessiontoken" + } + } + * + * + * */ + + + let newActions = []; + for (let config of this.props.selectedConfigs) { + let newAction = {} + newAction["action"] = action.data.action + newAction["when"] = when + + // get IC for component config + let ic = null; + for (let component of this.props.ics) { + if (component.id === config.icID) { + ic = component; + } + } + + if (ic == null) { + continue; + } + + // the following is not required by the protocol; it is an internal help + newAction["icid"] = ic.id + + if (newAction.action === 'start') { + newAction["parameters"] = config.startParameters; + + + if (config.fileIDs.length > 0){ + newAction["model"] = {} + newAction.model["type"] = "url" + newAction.model["token"] = this.props.token + + let fileURLs = [] + for (let fileID of config.fileIDs){ + fileURLs.push("/files/" + fileID.toString()) + } + newAction.model["url"] = fileURLs + } + + newAction["results"] = {} + newAction.results["type"] = "url" + newAction.results["token"] = this.props.token + newAction.results["url"] = "/results/RESULTID/file" // RESULTID serves as placeholder and is replaced later + + } + + // add the new action + newActions.push(newAction); + + } // end for loop over selected configs + + + let newResult = {} + newResult["result"] = {} + if (action.data.action === 'start') { + + let configSnapshots = []; + // create config snapshots in case action is start + for (let config of this.props.selectedConfigs) { + let index = this.props.configs.indexOf(config) + configSnapshots.push(this.props.snapshotConfig(index)); + } + + // create new result for new run + newResult.result["description"] = "Start at " + when; + newResult.result["scenarioID"] = this.props.selectedConfigs[0].scenarioID + newResult.result["configSnapshots"] = configSnapshots + } + + + console.log("Dispatching actions for configs", newActions, newResult) + AppDispatcher.dispatch({ + type: 'ics/start-action', + action: newActions, + result: newResult, + token: this.props.token + }); + } + } + setAction = id => { // search action for (let action of this.props.actions) { @@ -65,7 +246,13 @@ class ICAction extends React.Component { render() { - let sendCommandDisabled = this.props.runDisabled || this.state.selectedAction == null || this.state.selectedAction.id === "-1" + let sendCommandDisabled = false; + if (!this.props.hasConfigs && this.props.selectedICs.length === 0 || this.state.selectedAction == null || this.state.selectedAction.id === "-1"){ + sendCommandDisabled = true; + } + if (this.props.hasConfigs && this.props.selectedConfigs.length === 0|| this.state.selectedAction == null || this.state.selectedAction.id === "-1"){ + sendCommandDisabled = true; + } let time = this.state.time.getFullYear().pad(4) + '-' + this.state.time.getMonth().pad(2) + '-' + @@ -98,7 +285,7 @@ class ICAction extends React.Component { + onClick={() => this.runAction(this.state.selectedAction, this.state.time)}>Run Select time for synced command execution
    ; diff --git a/src/ic/ic-store.js b/src/ic/ic-store.js index 7cf314a..b852b94 100644 --- a/src/ic/ic-store.js +++ b/src/ic/ic-store.js @@ -66,16 +66,48 @@ class InfrastructureComponentStore extends ArrayStore { return state; case 'ics/start-action': - if (!Array.isArray(action.data)) - action.data = [ action.data ] + if (!Array.isArray(action.action)) + action.action = [ action.action ] - ICsDataManager.doActions(action.ic, action.data, action.token); + ICsDataManager.doActions(action.icid, action.action, action.token, action.result); + return state; + + case 'ics/action-started': + NotificationsDataManager.addNotification(NotificationsFactory.ACTION_INFO()); return state; case 'ics/action-error': console.log(action.error); return state; + case 'ics/action-result-added': + + for (let a of action.actions){ + + if (a.results !== undefined && a.results != null){ + // adapt URL for newly created result ID + a.results.url = a.results.url.replace("RESULTID", action.data.result.id); + a.results.url = ICsDataManager.makeURL(a.results.url); + a.results.url = window.location.host + a.results.url; + } + if (a.model !== undefined && a.model != null && JSON.stringify(a.model) !== JSON.stringify({})) { + // adapt URL(s) for model file + let modelURLs = [] + for (let url of a.model.url){ + let modifiedURL = ICsDataManager.makeURL(url); + modifiedURL = window.location.host + modifiedURL; + modelURLs.push(modifiedURL) + } + a.model.url = modelURLs + } + ICsDataManager.doActions(a.icid, [a], action.token) + } + return state; + + case 'ics/action-result-add-error': + console.log(action.error); + return state + case 'ics/get-status': ICsDataManager.getStatus(action.url, action.token, action.ic); return super.reduce(state, action); @@ -85,7 +117,12 @@ class InfrastructureComponentStore extends ArrayStore { if(!tempIC.managedexternally){ tempIC.state = action.data.state; tempIC.uptime = action.data.time_now - action.data.time_started; - tempIC.statusupdateraw = action.data; + if (tempIC.statusupdateraw === null || tempIC.statusupdateraw === undefined){ + tempIC.statusupdateraw = {}; + tempIC.statusupdateraw["status"] = action.data; + } else { + tempIC.statusupdateraw["status"] = action.data; + } AppDispatcher.dispatch({ type: 'ics/start-edit', data: tempIC, @@ -98,6 +135,27 @@ class InfrastructureComponentStore extends ArrayStore { console.log("status error:", action.error); return super.reduce(state, action); + case 'ics/nodestats-received': + let tempIC2 = action.ic; + if(!tempIC2.managedexternally){ + if (tempIC2.statusupdateraw === null || tempIC2.statusupdateraw === undefined){ + tempIC2.statusupdateraw = {}; + tempIC2.statusupdateraw["statistics"] = action.data; + } else { + tempIC2.statusupdateraw["statistics"] = action.data; + } + AppDispatcher.dispatch({ + type: 'ics/start-edit', + data: tempIC2, + token: action.token, + }); + } + return super.reduce(state, action); + + case 'ics/nodestats-error': + console.log("nodestats error:", action.error); + return super.reduce(state, action); + case 'ics/restart': ICsDataManager.restart(action.url, action.token); return super.reduce(state, action); diff --git a/src/ic/ics-data-manager.js b/src/ic/ics-data-manager.js index 02d480f..74b4080 100644 --- a/src/ic/ics-data-manager.js +++ b/src/ic/ics-data-manager.js @@ -24,28 +24,85 @@ class IcsDataManager extends RestDataManager { super('ic', '/ic'); } - doActions(ic, actions, token = null) { - for (let action of actions) { - if (action.when) - // Send timestamp as Unix Timestamp - action.when = Math.round(action.when.getTime() / 1000); - } + doActions(icid, actions, token = null, result=null) { - RestAPI.post(this.makeURL(this.url + '/' + ic.id + '/action'), actions, token).then(response => { + + if (icid !== undefined && icid != null && JSON.stringify(icid) !== JSON.stringify({})) { + + for (let action of actions) { + if (action.when) { + // Send timestamp as Unix Timestamp + action.when = Math.round(action.when.getTime() / 1000); + } + } + // sending action to a specific IC via IC list + + RestAPI.post(this.makeURL(this.url + '/' + icid + '/action'), actions, token).then(response => { AppDispatcher.dispatch({ - type: 'ics/action-started', - data: response + type: 'ics/action-started', + data: response }); - }).catch(error => { + }).catch(error => { AppDispatcher.dispatch({ - type: 'ics/action-error', + type: 'ics/action-error', + error + }); + }); + } else { + // sending the same action to multiple ICs via scenario controls + + // distinguish between "start" action and any other + + if (actions[0].action !== "start"){ + for (let a of actions){ + + // sending action to a specific IC via IC list + if (a.when) { + // Send timestamp as Unix Timestamp + a.when = Math.round(a.when.getTime() / 1000); + } + + RestAPI.post(this.makeURL(this.url + '/' + a.icid + '/action'), [a], token).then(response => { + AppDispatcher.dispatch({ + type: 'ics/action-started', + data: response + }); + }).catch(error => { + AppDispatcher.dispatch({ + type: 'ics/action-error', + error + }); + }); + + } + } else{ + // for start actions procedure is different + // first a result needs to be created, then the start actions can be sent + + RestAPI.post(this.makeURL( '/results'), result, token).then(response => { + AppDispatcher.dispatch({ + type: 'ics/action-result-added', + data: response, + actions: actions, + token: token, + }); + + AppDispatcher.dispatch({ + type: "results/added", + data: response.result, + }); + }).catch(error => { + AppDispatcher.dispatch({ + type: 'ics/action-result-add-error', error + }); }); - }); + } + } } getStatus(url,token,ic){ - RestAPI.get(url, null).then(response => { + RestAPI.get(url + "/status", null).then(response => { AppDispatcher.dispatch({ type: 'ics/status-received', data: response, @@ -58,6 +115,25 @@ class IcsDataManager extends RestDataManager { error: error }) }) + + // get name of websocket + /*let ws_api = ic.websocketurl.split("/") + let ws_name = ws_api[ws_api.length-1] // websocket name is the last element in the websocket url + + RestAPI.get(url + "/node/" + ws_name + "/stats", null).then(response => { + AppDispatcher.dispatch({ + type: 'ics/nodestats-received', + data: response, + token: token, + ic: ic + }); + }).catch(error => { + AppDispatcher.dispatch({ + type: 'ics/nodestats-error', + error: error + }) + })*/ + } restart(url,token){ diff --git a/src/ic/ics.js b/src/ic/ics.js index de10aaa..6152c17 100644 --- a/src/ic/ics.js +++ b/src/ic/ics.js @@ -36,6 +36,8 @@ import ICDialog from './ic-dialog'; import ICAction from './ic-action'; import DeleteDialog from '../common/dialogs/delete-dialog'; +import NotificationsDataManager from "../common/data-managers/notifications-data-manager"; +import NotificationsFactory from "../common/data-managers/notifications-factory"; class InfrastructureComponents extends Component { static getStores() { @@ -135,7 +137,7 @@ class InfrastructureComponents extends Component { && ic.apiurl !== '' && ic.apiurl !== undefined && ic.apiurl !== null && !ic.managedexternally) { AppDispatcher.dispatch({ type: 'ics/get-status', - url: ic.apiurl + "/status", + url: ic.apiurl, token: this.state.sessionToken, ic: ic }); @@ -149,11 +151,35 @@ class InfrastructureComponents extends Component { this.setState({ newModal : false }); if (data) { - AppDispatcher.dispatch({ - type: 'ics/start-add', - data, - token: this.state.sessionToken, - }); + if (!data.managedexternally) { + AppDispatcher.dispatch({ + type: 'ics/start-add', + data, + token: this.state.sessionToken, + }); + } else { + // externally managed IC: dispatch create action to selected manager + let newAction = {}; + newAction["action"] = "create"; + newAction["parameters"] = data; + newAction["when"] = new Date() + + // find the manager IC + let managerIC = this.state.ics.find(ic => ic.uuid === data.manager) + if (managerIC === null || managerIC === undefined){ + NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Could not find manager IC with UUID " + data.manager)); + return; + } + + AppDispatcher.dispatch({ + type: 'ics/start-action', + icid: managerIC.id, + action: newAction, + result: null, + token: this.state.sessionToken + }); + + } } } @@ -240,18 +266,7 @@ class InfrastructureComponents extends Component { this.setState({ selectedICs: selectedICs }); } - runAction(action, when) { - for (let index of this.state.selectedICs) { - action.when = when; - AppDispatcher.dispatch({ - type: 'ics/start-action', - ic: this.state.ics[index], - data: action.data, - token: this.state.sessionToken, - }); - } - } static isICOutdated(component) { if (!component.stateUpdateAt) @@ -378,9 +393,9 @@ class InfrastructureComponents extends Component { } } - isExternalIC(index){ - let ic = this.state.ics[index] - return ic.managedexternally + isLocalIC(index, ics){ + let ic = ics[index] + return !ic.managedexternally } getICCategoryTable(ics, editable, title){ @@ -390,7 +405,7 @@ class InfrastructureComponents extends Component { this.isExternalIC(index)} + checkboxDisabled={(index) => this.isLocalIC(index, ics) === true} onChecked={(ic, event) => this.onICChecked(ic, event)} width='30' /> @@ -420,19 +435,21 @@ class InfrastructureComponents extends Component { modifier={(stateUpdateAt, component) => this.stateUpdateModifier(stateUpdateAt, component)} /> - {this.state.currentUser.role === "Admin" && editable ? + {this.state.currentUser.role === "Admin" ? this.isLocalIC(index, ics)} exportButton deleteButton + showDeleteButton = {(index) => this.isLocalIC(index, ics)} onEdit={index => this.setState({editModal: true, modalIC: ics[index], modalIndex: index})} onExport={index => this.exportIC(index)} onDelete={index => this.setState({deleteModal: true, modalIC: ics[index], modalIndex: index})} /> : this.exportIC(index)} /> @@ -496,12 +513,15 @@ class InfrastructureComponents extends Component { {this.state.currentUser.role === "Admin" && this.state.numberOfExternalICs > 0 ?
    this.runAction(action, when)} + hasConfigs = {false} + ics={this.state.ics} + selectedICs={this.state.selectedICs} + token={this.state.sessionToken} actions={[ {id: '-1', title: 'Action', data: {action: 'none'}}, {id: '0', title: 'Reset', data: {action: 'reset'}}, {id: '1', title: 'Shutdown', data: {action: 'shutdown'}}, + {id: '2', title: 'Delete', data: {action: 'delete'}} ]} />
    diff --git a/src/ic/new-ic.js b/src/ic/new-ic.js index 2e81887..39aeebd 100644 --- a/src/ic/new-ic.js +++ b/src/ic/new-ic.js @@ -170,7 +170,7 @@ class NewICDialog extends React.Component { {this.props.managers.length > 0 ? <> - An externally managed component is created and managed by an IC manager via AMQP} > + An externally managed component is created and managed by an IC manager via AMQP} > this.handleChange(e)}> diff --git a/src/img/dog-waiting-bw.jpg b/src/img/dog-waiting-bw.jpg new file mode 100644 index 0000000..31c17ff Binary files /dev/null and b/src/img/dog-waiting-bw.jpg differ diff --git a/src/router.js b/src/router.js index 6b9321d..7dcc072 100644 --- a/src/router.js +++ b/src/router.js @@ -28,6 +28,8 @@ import Dashboard from './dashboard/dashboard' import InfrastructureComponents from './ic/ics'; import Users from './user/users'; import User from "./user/user"; +import LoginComplete from './user/login-complete' + class Root extends React.Component { @@ -35,6 +37,7 @@ class Root extends React.Component { return ( + diff --git a/src/scenario/scenario.js b/src/scenario/scenario.js index 791e79f..1f8dee2 100644 --- a/src/scenario/scenario.js +++ b/src/scenario/scenario.js @@ -394,60 +394,6 @@ class Scenario extends React.Component { } - runAction(action, when) { - if (action.data.action === 'none') { - console.warn("No command selected. Nothing was sent."); - return; - } - - let configs = []; - for (let index of this.state.selectedConfigs) { - // get IC for component config - let ic = null; - for (let component of this.state.ics) { - if (component.id === this.state.configs[index].icID) { - ic = component; - } - } - - if (ic == null) { - continue; - } - - if (action.data.action === 'start') { - configs.push(this.copyConfig(index)); - action.data.parameters = this.state.configs[index].startParameters; - } - - action.data.when = when; - - console.log("Sending action: ", action.data) - - AppDispatcher.dispatch({ - type: 'ics/start-action', - ic: ic, - data: action.data, - token: this.state.sessionToken - }); - } - - if (configs.length !== 0) { //create result (only if command was 'start') - let componentConfigs = {}; - componentConfigs["configs"] = configs; - let data = {}; - data["Description"] = "Run " + this.state.scenario.name; // default description, to be change by user later - data["ResultFileIDs"] = []; - data["scenarioID"] = this.state.scenario.id; - data["ConfigSnapshots"] = JSON.stringify(componentConfigs, null, 2); - AppDispatcher.dispatch({ - type: 'results/start-add', - data, - token: this.state.sessionToken, - }) - } - - }; - getICName(icID) { for (let ic of this.state.ics) { if (ic.id === icID) { @@ -710,7 +656,7 @@ class Scenario extends React.Component { } openResultConfigSnaphots(result) { - if (!result.configSnapshots || result.configSnapshots == "") { + if (result.configSnapshots === null || result.configSnapshots === undefined) { this.setState({ modalResultConfigs: {"configs": []}, modalResultConfigsIndex: result.id, @@ -718,7 +664,7 @@ class Scenario extends React.Component { }); } else { this.setState({ - modalResultConfigs: JSON.parse(result.configSnapshots), + modalResultConfigs: result.configSnapshots, modalResultConfigsIndex: result.id, resultConfigsModal: true }); @@ -928,8 +874,12 @@ class Scenario extends React.Component { {this.state.ExternalICInUse ? (
    this.runAction(action, when)} + hasConfigs={true} + ics={this.state.ics} + configs={this.state.configs} + selectedConfigs = {this.state.selectedConfigs} + snapshotConfig = {(index) => this.copyConfig(index)} + token = {this.state.sessionToken} actions={[ { id: '-1', title: 'Action', data: { action: 'none' } }, { id: '0', title: 'Start', data: { action: 'start' } }, diff --git a/src/signal/edit-signal-mapping.js b/src/signal/edit-signal-mapping.js index b1df22c..3b75386 100644 --- a/src/signal/edit-signal-mapping.js +++ b/src/signal/edit-signal-mapping.js @@ -97,23 +97,24 @@ class EditSignalMapping extends React.Component { let signals = this.state.signals; let modifiedSignals = this.state.modifiedSignalIDs; - - if (column === 1) { // Name change + console.log("HandleMappingChange", row, column) + if (column === 2) { // Name change signals[row].name = event.target.value; if (modifiedSignals.find(id => id === signals[row].id) === undefined){ modifiedSignals.push(signals[row].id); } - } else if (column === 2) { // unit change + } else if (column === 3) { // unit change signals[row].unit = event.target.value; if (modifiedSignals.find(id => id === signals[row].id) === undefined){ modifiedSignals.push(signals[row].id); } - } else if (column === 3) { // scaling factor change + } else if (column === 4) { // scaling factor change signals[row].scalingFactor = parseFloat(event.target.value); if (modifiedSignals.find(id => id === signals[row].id) === undefined){ modifiedSignals.push(signals[row].id); } - } else if (column === 0) { //index change + } else if (column === 1) { //index change + console.log("Index change") signals[row].index =parseInt(event.target.value, 10); if (modifiedSignals.find(id => id === signals[row].id) === undefined){ modifiedSignals.push(signals[row].id); diff --git a/src/styles/app.css b/src/styles/app.css index 7640536..4d588c2 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -112,6 +112,13 @@ body { } } +.verticalhorizontal { + display: flex; + justify-content: center; + align-items: center; + height: 800px; +} + /** * Menus */ @@ -219,6 +226,26 @@ body { background-color: white; } +/** + * Login select + */ + .login-select { + position: sticky; + width: 300px; + height: 150px; + top: 50%; + left: 50%; + margin-top: 50px; + margin-bottom: 100px; + transform: translate(-50%); + + + padding: 20px 20px; + + background-color: #a8c7cf; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 9px 18px 0 rgba(0, 0, 0, 0.1); +} /** * Login form @@ -234,6 +261,13 @@ body { 0 9px 18px 0 rgba(0, 0, 0, 0.1); } +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border:0; + border-top: 1px solid rgba(0, 0, 0, 0.1); +} + /** * Tables */ diff --git a/src/user/login-complete.js b/src/user/login-complete.js new file mode 100644 index 0000000..4fd4922 --- /dev/null +++ b/src/user/login-complete.js @@ -0,0 +1,107 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import { Redirect } from 'react-router-dom'; +import AppDispatcher from '../common/app-dispatcher'; +import LoginStore from './login-store' +import { Container } from 'flux/utils'; + + +class LoginComplete extends React.Component { + constructor(props) { + console.log("LoginComplete constructor"); + super(props); + + AppDispatcher.dispatch({ + type: 'users/extlogin', + }); + + this.state = { + loginMessage: '', + token: '', + currentUser: '', + secondsToWait: 99, + } + + this.timer = 0; + this.startTimer = this.startTimer.bind(this); + this.countDown = this.countDown.bind(this); + this.stopTimer = this.stopTimer.bind(this); + + + } + + componentDidMount() { + this.startTimer(); + this.setState({secondsToWait: 5}); + } + + + static getStores(){ + return [LoginStore] + } + + static calculateState(prevState, props) { + // We need to work with the login store here to trigger the re-render upon state change after login + // Upon successful login, the token and currentUser are stored in the local storage as strings + return { + loginMessage: LoginStore.getState().loginMessage, + token: LoginStore.getState().token, + currentUser: LoginStore.getState().currentUser, + } + } + + stopTimer() { + console.log("stop timer"); + clearInterval(this.timer); + } + + startTimer() { + if (this.timer == 0 && this.state.secondsToWait > 0) { + // call function 'countDown' every 1000ms + this.timer = setInterval(this.countDown, 1000); + } + } + + countDown() { + let seconds = this.state.secondsToWait - 1; + this.setState({secondsToWait: seconds}); + + // waiting time over, stop counting down + if (seconds == 0) { + clearInterval(this.timer); + } + } + + render() { + if (this.state.currentUser && this.state.currentUser !== "") { + this.stopTimer(); + return (); + } + else if (this.state.secondsToWait == 0) { + this.stopTimer(); + return (); + } else { + return (
    + Waiting Dog
    ); + } + } +} + +let fluxContainerConverter = require('../common/FluxContainerConverter'); +export default Container.create(fluxContainerConverter.convert(LoginComplete)); \ No newline at end of file diff --git a/src/user/login-form.js b/src/user/login-form.js index 8c09152..fedfa26 100644 --- a/src/user/login-form.js +++ b/src/user/login-form.js @@ -19,6 +19,8 @@ import React, { Component } from 'react'; import { Form, Button, FormGroup, FormControl, FormLabel, Col } from 'react-bootstrap'; import RecoverPassword from './recover-password' import AppDispatcher from '../common/app-dispatcher'; +import _ from 'lodash'; + class LoginForm extends Component { constructor(props) { @@ -56,17 +58,17 @@ class LoginForm extends Component { this.setState({ [event.target.id]: event.target.value, disableLogin }); } - openRecoverPassword(){ - this.setState({forgottenPassword: true}); + openRecoverPassword() { + this.setState({ forgottenPassword: true }); } - closeRecoverPassword(){ - this.setState({forgottenPassword: false}); + closeRecoverPassword() { + this.setState({ forgottenPassword: false }); } - render() { + villaslogin() { return ( -
    + Username
    @@ -89,7 +91,7 @@ class LoginForm extends Component { } - + @@ -101,9 +103,28 @@ class LoginForm extends Component { this.closeRecoverPassword()} sessionToken={this.props.sessionToken} /> - ); } + + render() { + let villasLogin = this.villaslogin(); + + if (this.props.config) { + let externalLogin = _.get(this.props.config, ['authentication', 'external', 'enabled']) + let provider = _.get(this.props.config, ['authentication', 'external', 'provider_name']) + let url = _.get(this.props.config, ['authentication', 'external', 'authorize_url']) + "?rd=/login/complete" + + if (externalLogin && provider && url) { + return [ + villasLogin, +
    , + + ]; + } + } + + return villasLogin; + } } export default LoginForm; diff --git a/src/user/login-store.js b/src/user/login-store.js index aa69290..3e0eba6 100644 --- a/src/user/login-store.js +++ b/src/user/login-store.js @@ -20,6 +20,7 @@ import { ReduceStore } from 'flux/utils'; import AppDispatcher from '../common/app-dispatcher'; import UsersDataManager from './users-data-manager'; import ICDataDataManager from '../ic/ic-data-data-manager'; +import ConfigReader from '../config-reader'; class LoginStore extends ReduceStore { constructor() { @@ -31,15 +32,30 @@ class LoginStore extends ReduceStore { currentUser: null, token: null, loginMessage: null, + config: null, }; } reduce(state, action) { switch (action.type) { + case 'config/load': + ConfigReader.loadConfig(); + return state; + + case 'config/loaded': + return Object.assign({}, state, { config: action.data }); + + case 'config/load-error': + return Object.assign({}, state, { config: null}); + case 'users/login': UsersDataManager.login(action.username, action.password); return Object.assign({}, state, { loginMessage: null }); + case 'users/extlogin': + UsersDataManager.login(); + return Object.assign({}, state, { loginMessage: null }); + case 'users/logout': // disconnect from all infrastructure components ICDataDataManager.closeAll(); diff --git a/src/user/login.js b/src/user/login.js index 0a6fac8..ea38f89 100644 --- a/src/user/login.js +++ b/src/user/login.js @@ -26,10 +26,20 @@ import Header from '../common/header'; import Footer from '../common/footer'; import NotificationsDataManager from '../common/data-managers/notifications-data-manager'; import LoginStore from './login-store' +import AppDispatcher from '../common/app-dispatcher'; class Login extends Component { + constructor(props) { + super(props); - static getStores(){ + // load config in case the user goes directly to /login + // otherwise it will be loaded in app constructor + AppDispatcher.dispatch({ + type: 'config/load', + }); + } + + static getStores() { return [LoginStore] } @@ -40,6 +50,7 @@ class Login extends Component { loginMessage: LoginStore.getState().loginMessage, token: LoginStore.getState().token, currentUser: LoginStore.getState().currentUser, + config: LoginStore.getState().config, } } @@ -62,7 +73,7 @@ class Login extends Component {
    Login - +