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

manual merge with master

This commit is contained in:
irismarie 2021-03-03 17:13:23 +01:00
commit 3665146ca7
23 changed files with 881 additions and 161 deletions

View file

@ -46,6 +46,11 @@ class App extends React.Component {
constructor(props) {
super(props);
AppDispatcher.dispatch({
type: 'config/load',
});
this.state = {
showSidebarMenu: false,
}

View file

@ -138,6 +138,14 @@ class NotificationsFactory {
};
}
static ACTION_INFO() {
return {
title: 'Action successfully requested',
level: 'info'
};
}
}
export default NotificationsFactory;

View file

@ -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(<li><a href={brand.links[key]} title={key}>{key}</a></li>);
}*/
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 (
<div className="menu-sidebar">
<h2>Menu</h2>
<ul>
<li hidden={!brand.pages.home}><NavLink to="/home" activeClassName="active" title="Home">Home</NavLink></li>
<li hidden={!brand.pages.scenarios}><NavLink to="/scenarios" activeClassName="active" title="Scenarios">Scenarios</NavLink></li>
<li hidden={!brand.pages.infrastructure}><NavLink to="/infrastructure" activeClassName="active" title="Infrastructure Components">Infrastructure Components</NavLink></li>
{ this.props.currentRole === 'Admin' ?
<li><NavLink to="/users" activeClassName="active" title="User Management">User Management</NavLink></li> : ''
}
<li hidden={!brand.pages.account}><NavLink to="/account" title="Account">Account</NavLink></li>
<a onClick={this.logout.bind(this)} href={this.state.logoutLink}>Logout</a>
<li hidden={!brand.pages.api}><NavLink to="/api" title="API Browser">API Browser</NavLink></li>
</ul>
{
links.length > 0 ?
<div>
<br></br>
<h4> Links</h4>
<ul> {links} </ul>
</div>
: ''
}
</div>
);
}
return (
<div className="menu-sidebar">
@ -52,7 +117,7 @@ class SidebarMenu extends React.Component {
<li><NavLink to="/users" activeClassName="active" title="User Management">User Management</NavLink></li> : ''
}
<li hidden={!brand.pages.account}><NavLink to="/account" title="Account">Account</NavLink></li>
<li><NavLink to="/logout" title="Logout">Logout</NavLink></li>
<li><NavLink to={this.state.logoutLink} title="Logout">Logout</NavLink></li>
<li hidden={!brand.pages.api}><NavLink to="/api" title="API Browser">API Browser</NavLink></li>
</ul>
{
@ -69,4 +134,5 @@ class SidebarMenu extends React.Component {
}
}
export default SidebarMenu;
let fluxContainerConverter = require('../common/FluxContainerConverter');
export default Container.create(fluxContainerConverter.convert(SidebarMenu));

View file

@ -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: '/',

View file

@ -79,9 +79,19 @@ class CustomTable extends Component {
cell.push(<Button variant="link" onClick={() => child.props.onClick(index)}>{content}</Button>);
} else if (linkKey === 'filebuttons') {
content.forEach((contentvalue, contentkey) => {
cell.push(<OverlayTrigger key={contentkey} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"export"}`}>Download {contentvalue}</Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onDownload(contentkey)} disabled={child.props.onDownload == null}>{contentkey + ' ' }
<Icon icon='file-download' /></Button></OverlayTrigger>);
cell.push(
<OverlayTrigger
key={contentkey}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"export"}`}>Download {contentvalue}</Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onDownload(contentkey)}
disabled={child.props.onDownload == null}>
{contentkey + ' ' }
<Icon icon='file-download' />
</Button>
</OverlayTrigger>);
});
} 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(<OverlayTrigger key={0} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"edit"}`}>{disable ? "Externally managed ICs cannot be edited" : "edit"} </Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onEdit(index)} disabled={disable || child.props.onEdit == null}><Icon icon='edit' /></Button></OverlayTrigger>);
let showEditButton = true
if (child.props.showEditButton !== null)
{
showEditButton = child.props.showEditButton(index)
}
if(showEditButton){
if (child.props.editButton) {
cell.push(
<OverlayTrigger
key={0}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"edit"}`}> Edit </Tooltip>}>
<Button
variant='table-control-button'
onClick={() => child.props.onEdit(index)}
disabled={child.props.onEdit == null} >
<Icon icon='edit' />
</Button>
</OverlayTrigger>);
}
}
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(
<FormCheck
@ -137,30 +164,89 @@ class CustomTable extends Component {
}
if (child.props.exportButton) {
cell.push(<OverlayTrigger key={1} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"export"}`}> Export </Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onExport(index)} disabled={child.props.onExport == null}><Icon icon='download' /></Button></OverlayTrigger>);
cell.push(
<OverlayTrigger
key={1}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"export"}`}> Export </Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onExport(index)}
disabled={child.props.onExport == null}>
<Icon icon='download' />
</Button>
</OverlayTrigger>);
}
if (child.props.duplicateButton) {
cell.push(<OverlayTrigger key={2} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"duplicate"}`}> Duplicate </Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onDuplicate(index)} disabled={child.props.onDuplicate == null}><Icon icon='copy' /></Button></OverlayTrigger>);
cell.push(
<OverlayTrigger
key={2}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"duplicate"}`}> Duplicate </Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onDuplicate(index)}
disabled={child.props.onDuplicate == null}>
<Icon icon='copy' />
</Button>
</OverlayTrigger>);
}
if (child.props.addRemoveFilesButton) {
cell.push(<OverlayTrigger key={3} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"export"}`}>Add/remove File(s)</Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onAddRemove(index)} disabled={child.props.onAddRemove == null}><Icon icon='file' /></Button></OverlayTrigger>);
cell.push(
<OverlayTrigger
key={3}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"export"}`}>Add/remove File(s)</Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onAddRemove(index)}
disabled={child.props.onAddRemove == null}>
<Icon icon='file' />
</Button>
</OverlayTrigger>);
}
if (child.props.downloadAllButton) {
cell.push(<OverlayTrigger key={4} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"export"}`}>Download All Files</Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onDownloadAll(index)} disabled={child.props.onDownloadAll == null}><Icon icon='file-download' /></Button></OverlayTrigger>);
cell.push(
<OverlayTrigger
key={4}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"export"}`}>Download All Files</Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onDownloadAll(index)}
disabled={child.props.onDownloadAll == null}>
<Icon icon='file-download' />
</Button>
</OverlayTrigger>);
}
if (child.props.deleteButton) {
cell.push(<OverlayTrigger key={5} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"delete"}`}> Delete </Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onDelete(index)} disabled={child.props.onDelete == null}><Icon icon='trash' /></Button></OverlayTrigger>);
let showDeleteButton = true;
if (child.props.showDeleteButton !== null){
showDeleteButton = child.props.showDeleteButton(index)
}
if (showDeleteButton){
if (child.props.deleteButton) {
cell.push(
<OverlayTrigger
key={5}
placement={'bottom'}
overlay={<Tooltip id={`tooltip-${"delete"}`}> Delete </Tooltip>} >
<Button
variant='table-control-button'
onClick={() => child.props.onDelete(index)}
disabled={child.props.onDelete == null}>
<Icon icon='trash' />
</Button>
</OverlayTrigger>);
}
}
return cell;
} // addCell
@ -244,9 +330,19 @@ class CustomTable extends Component {
onCellBlur: () => { }
};
return (<td key={cellIndex} tabIndex={tabIndex} onClick={evtHdls.onCellClick} onFocus={evtHdls.onCellFocus} onBlur={evtHdls.onCellBlur}>
return (<td
key={cellIndex}
tabIndex={tabIndex}
onClick={evtHdls.onCellClick}
onFocus={evtHdls.onCellFocus}
onBlur={evtHdls.onCellBlur}>
{(this.state.editCell[0] === cellIndex && this.state.editCell[1] === rowIndex) ? (
<FormControl as='input' type={children[cellIndex].props.inputType} value={cell} onChange={(event) => children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} ref={ref => { this.activeInput = ref; }} />
<FormControl
as='input'
type={children[cellIndex].props.inputType}
value={cell}
onChange={(event) => children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)}
ref={ref => { this.activeInput = ref; }} />
) : (
<span>
{cell.map((element, elementIndex) => (

43
src/config-reader.js Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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();

View file

@ -23,4 +23,4 @@ const config = {
branding: 'villasweb',
}
export default config
export default config

View file

@ -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 {
<Button
variant="secondary"
disabled={sendCommandDisabled}
onClick={() => this.props.runAction(this.state.selectedAction, this.state.time)}>Run</Button>
onClick={() => this.runAction(this.state.selectedAction, this.state.time)}>Run</Button>
</InputGroup>
<small className="text-muted">Select time for synced command execution</small>
</div>;

View file

@ -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);

View file

@ -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){

View file

@ -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 {
<Table data={ics}>
<TableColumn
checkbox
checkboxDisabled={(index) => 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" ?
<TableColumn
width='200'
width='150'
editButton
showEditButton ={(index) => 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})}
/>
:
<TableColumn
width='100'
width='50'
exportButton
onExport={index => this.exportIC(index)}
/>
@ -496,12 +513,15 @@ class InfrastructureComponents extends Component {
{this.state.currentUser.role === "Admin" && this.state.numberOfExternalICs > 0 ?
<div style={{float: 'left'}}>
<ICAction
runDisabled={this.state.selectedICs.length === 0}
runAction={(action, when) => 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'}}
]}
/>
</div>

View file

@ -170,7 +170,7 @@ class NewICDialog extends React.Component {
{this.props.managers.length > 0 ?
<>
<FormGroup controlId="managedexternally">
<OverlayTrigger key="3" placement={'left'} overlay={<Tooltip id={`tooltip-${"me"}`}>An externally managed component is created and managed by an IC manager via AMQP</Tooltip>} >
<OverlayTrigger key="-1" placement={'left'} overlay={<Tooltip id={`tooltip-${"me"}`}>An externally managed component is created and managed by an IC manager via AMQP</Tooltip>} >
<FormCheck type={"checkbox"} label={"Managed externally"} defaultChecked={this.state.managedexternally} onChange={e => this.handleChange(e)}>
</FormCheck>
</OverlayTrigger>

BIN
src/img/dog-waiting-bw.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -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 (
<BrowserRouter>
<Switch>
<Route path='/login/complete' component={LoginComplete} />
<Route path='/login' component={Login} />
<Route path='/logout' component={Logout} />
<Route path='/' component={App} />

View file

@ -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 ? (
<div style={{ float: 'left' }}>
<ICAction
runDisabled={this.state.selectedConfigs.length === 0}
runAction={(action, when) => 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' } },

View file

@ -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);

View file

@ -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
*/

107
src/user/login-complete.js Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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 (<Redirect to="/home" />);
}
else if (this.state.secondsToWait == 0) {
this.stopTimer();
return (<Redirect to="/login" />);
} else {
return (<div class="verticalhorizontal">
<img style={{height: 300}}src={require('../img/dog-waiting-bw.jpg').default} alt="Waiting Dog" /></div>);
}
}
}
let fluxContainerConverter = require('../common/FluxContainerConverter');
export default Container.create(fluxContainerConverter.convert(LoginComplete));

View file

@ -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 (
<Form>
<Form key="login_a">
<FormGroup controlId="username">
<FormLabel column={true}>Username</FormLabel>
<Col>
@ -89,7 +91,7 @@ class LoginForm extends Component {
</div>
}
<FormGroup style={{paddingTop: 15, paddingBottom: 5}}>
<FormGroup style={{ paddingTop: 15, paddingBottom: 5 }}>
<Col>
<span className='solid-button'>
<Button variant='secondary' style={{width: 90}} type="submit" disabled={this.state.disableLogin} onClick={e => this.login(e)}>Login</Button>
@ -101,9 +103,28 @@ class LoginForm extends Component {
<RecoverPassword show={this.state.forgottenPassword} onClose={() => this.closeRecoverPassword()} sessionToken={this.props.sessionToken} />
</Form>
);
}
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,
<hr key="login_b"/>,
<Button key="login_c" onClick={e => window.location = url } block>Sign in with {provider}</Button>
];
}
}
return villasLogin;
}
}
export default LoginForm;

View file

@ -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();

View file

@ -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 {
<div className="login-container">
<NavbarBrand>Login</NavbarBrand>
<LoginForm loginMessage={this.state.loginMessage} />
<LoginForm loginMessage={this.state.loginMessage} config={this.state.config}/>
</div>
<Footer />

View file

@ -17,10 +17,10 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import AppDispatcher from '../common/app-dispatcher';
class Logout extends React.Component {
componentDidMount() {
AppDispatcher.dispatch({
type: 'users/logout'
@ -40,4 +40,4 @@ class Logout extends React.Component {
}
}
export default Logout;
export default Logout;

View file

@ -25,18 +25,34 @@ class UsersDataManager extends RestDataManager {
}
login(username, password) {
RestAPI.post(this.makeURL('/authenticate'), { username: username, password: password }).then(response => {
AppDispatcher.dispatch({
type: 'users/logged-in',
token: response.token,
currentUser: response.user,
if (username && password) {
RestAPI.post(this.makeURL('/authenticate/internal'), { username: username, password: password }).then(response => {
AppDispatcher.dispatch({
type: 'users/logged-in',
token: response.token,
currentUser: response.user,
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'users/login-error',
error: error
});
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'users/login-error',
error: error
} else { // external authentication
RestAPI.post(this.makeURL('/authenticate/external'),).then(response => {
console.log(response);
AppDispatcher.dispatch({
type: 'users/logged-in',
token: response.token,
currentUser: response.user,
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'users/login-error',
error: error
});
});
});
}
}
}