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/ic/ic-action.js b/src/ic/ic-action.js index b0333b8..02d3a63 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,183 @@ 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 + // TODO do not default to the first file of the config + newAction.model["url"] = "/files/" + config.fileIDs[0].toString() + } + + 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 +242,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 +281,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..39b1c8c 100644 --- a/src/ic/ic-store.js +++ b/src/ic/ic-store.js @@ -66,16 +66,43 @@ 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 for model file + a.model.url = ICsDataManager.makeURL(a.model.url); + a.model.url = window.location.host + a.model.url; + } + 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); diff --git a/src/ic/ics-data-manager.js b/src/ic/ics-data-manager.js index 02d480f..a2382fc 100644 --- a/src/ic/ics-data-manager.js +++ b/src/ic/ics-data-manager.js @@ -24,24 +24,86 @@ 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){ diff --git a/src/ic/ics.js b/src/ic/ics.js index de10aaa..95d224f 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() { @@ -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) @@ -420,19 +435,19 @@ class InfrastructureComponents extends Component { modifier={(stateUpdateAt, component) => this.stateUpdateModifier(stateUpdateAt, component)} /> - {this.state.currentUser.role === "Admin" && editable ? + {this.state.currentUser.role === "Admin" ? !this.isExternalIC(index)} exportButton - deleteButton + deleteButton = {(index) => !this.isExternalIC(index)} 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 +511,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/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' } },