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

Merge branch 'master' into feature-external-auth

This commit is contained in:
irismarie 2021-02-18 15:11:56 +01:00
commit 18966f86e0
36 changed files with 1434 additions and 400 deletions

1
.gitignore vendored
View file

@ -17,3 +17,4 @@ yarn-debug.log*
yarn-error.log*
.vscode/
*.code-workspace
package-lock.json

View file

@ -2,6 +2,7 @@ variables:
GIT_SUBMODULE_STRATEGY: normal
DOCKER_TAG: ${CI_COMMIT_BRANCH}
DOCKER_IMAGE: ${CI_REGISTRY_IMAGE}
FF_GITLAB_REGISTRY_HELPER_IMAGE: 1
cache:
untracked: true

630
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -29,6 +29,7 @@
"libcimsvg": "git+https://git.rwth-aachen.de/acs/public/cim/pintura-npm-package.git",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"moment-duration-format": "^2.3.2",
"multiselect-react-dropdown": "^1.6.2",
"node-sass": "^4.14.1",
"popper.js": "^1.16.1",
@ -56,6 +57,7 @@
"react-trafficlight": "^5.2.1",
"sass": "^1.29.0",
"superagent": "^6.1.0",
"swagger-ui-react": "^3.42.0",
"ts-node": "^9.0.0",
"type-fest": "^0.13.1",
"typescript": "^4.1.2",

View file

@ -38,6 +38,7 @@ import Scenarios from './scenario/scenarios';
import Scenario from './scenario/scenario';
import Users from './user/users';
import User from './user/user';
import APIBrowser from './common/api-browser';
import './styles/app.css';
@ -122,6 +123,7 @@ class App extends React.Component {
<Route path="/infrastructure" component={InfrastructureComponents} />
<Route path="/account" component={User} />
<Route path="/users" component={Users} />
<Route path="/api" component={APIBrowser} />
</div>
</div>

82
src/common/api-browser.js Normal file
View file

@ -0,0 +1,82 @@
/**
* 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 SwaggerUI from 'swagger-ui-react'
import 'swagger-ui-react/swagger-ui.css'
import '../styles/swagger-ui.css';
import RestAPI from './api/rest-api';
import RestDataManager from './data-managers/rest-data-manager';
class APIBrowser extends React.Component {
constructor(props) {
super(props);
this.state = {
'spec': null
};
}
mangleSpec(spec) {
spec.host = window.location.host;
return spec;
}
componentWillMount() {
this._asyncRequest = RestAPI.get('/api/v2/openapi')
.then((spec) => {
this._asyncRequest = null;
this.setState({
'spec': this.mangleSpec(spec)
});
});
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
requestInterceptor(req) {
var token = localStorage.getItem('token');
if (token)
req.headers.Authorization = 'Bearer ' + token;
return req
}
render() {
return (
<div>
{ this.state.spec &&
<SwaggerUI
spec={this.state.spec}
tryItOutEnabled={true}
requestInterceptor={this.requestInterceptor}
/> }
</div>
);
}
}
export default APIBrowser;

View file

@ -26,10 +26,13 @@ import NotificationsFactory from "../data-managers/notifications-factory";
function isNetworkError(err, url) {
let result = false;
// If not status nor response fields, it is a network error. TODO: Handle timeouts
if (err.status == null || err.status === 500 || err.response == null) {
result = true;
if (err.status === 500 && err.response != null){
let notification = NotificationsFactory.INTERNAL_SERVER_ERROR(err.response)
NotificationsDataManager.addNotification(notification);
} else if (err.status == null || err.status === 500 || err.response == null) {
// If not status nor response fields, it is a network error. TODO: Handle timeouts
result = true;
let notification = err.timeout? NotificationsFactory.REQUEST_TIMEOUT : NotificationsFactory.SERVER_NOT_REACHABLE(url);
NotificationsDataManager.addNotification(notification);
}

View file

@ -42,6 +42,14 @@ class NotificationsFactory {
};
}
static INTERNAL_SERVER_ERROR(response) {
return {
title: 'Internal server error',
message: response.message,
level: 'error'
};
}
static ADD_ERROR(message) {
return {
title: "Add Error",

View file

@ -37,7 +37,7 @@ class DeleteDialog extends React.Component {
<Modal.Body>
Are you sure you want to delete the {this.props.title} <strong>'{this.props.name}'</strong>?
<Collapse isOpened={this.props.managedexternally} >
<FormLabel size="sm">The IC will be deleted if the respective VILLAScontroller sends "gone" state and no component config is using the IC anymore</FormLabel>
<FormLabel size="sm">The IC will be deleted if the respective manager sends "gone" state and no component config is using the IC anymore</FormLabel>
</Collapse>
</Modal.Body>

View file

@ -18,28 +18,10 @@
import React from 'react';
import config from '../config';
import {Redirect} from "react-router-dom";
import { Redirect } from "react-router-dom";
import { NavLink } from 'react-router-dom';
class Home extends React.Component {
constructor(props) {
super(props);
// create url for API documentation, distinguish between localhost and production deployment
let docs_url = "";
let docs_location = "/swagger/index.html";
let base_url = window.location.origin;
if (base_url.search("localhost") === -1){
docs_url = base_url + docs_location;
} else {
// useful for local testing, replace port 3000 with port 4000 (port of backend)
docs_url = base_url.replace("3000", "4000") + docs_location;
}
this.state = {
docs_url: docs_url
};
}
getCounts(type) {
if (this.state.hasOwnProperty('counts'))
@ -68,10 +50,9 @@ class Home extends React.Component {
</p>
<p>
An interactive documentation of the VILLASweb API is available <a href={this.state.docs_url} target="_blank" rel="noopener noreferrer">here</a>.
An interactive documentation of the VILLASweb API is available <NavLink to="/api">here</NavLink>.
</p>
<h3>Data Model</h3>
<img height={400} src={require('../img/datamodel.png').default} alt="Datamodel VILLASweb" />
@ -103,8 +84,6 @@ class Home extends React.Component {
<li>Users can have access to multiple scenarios</li>
</ul>
<h3>Credits</h3>
<p>VILLASweb is developed by the <a href="http://acs.eonerc.rwth-aachen.de">Institute for Automation of Complex Power Systems</a> at the <a href="https;//www.rwth-aachen.de">RWTH Aachen University</a>.</p>
<ul>

View file

@ -33,6 +33,7 @@ class SidebarMenu extends React.Component {
}
<li><NavLink to="/account" title="Account">Account</NavLink></li>
<li><NavLink to="/logout" title="Logout">Logout</NavLink></li>
<li><NavLink to="/api" title="API Browser">API Browser</NavLink></li>
</ul>
</div>
);

View file

@ -35,6 +35,7 @@ class TableColumn extends Component {
labelKey: null,
checkbox: false,
checkboxKey: '',
checkboxDisabled: null,
labelStyle: null,
labelModifier: null

View file

@ -52,29 +52,34 @@ class CustomTable extends Component {
break;
}
}
} else if ('data' in child.props && 'dataKey' in child.props) {
content = new Map();
let keys = _.get(data, child.props.dataKey);
let filteredData = child.props.data.filter(data => keys.includes(data.id))
filteredData.forEach(file => {
content.set(_.get(file, 'id'), _.get(file, 'name'));
})
} else if ('dataKey' in child.props) {
content = _.get(data, child.props.dataKey);
}
const modifier = child.props.modifier;
if (modifier && content != null) {
content = modifier(content);
content = modifier(content, data);
}
let cell = [];
if (content != null) {
//content = content.toString();
// check if cell should be a link
const linkKey = child.props.linkKey;
if (linkKey && data[linkKey] != null) {
cell.push(<Link to={child.props.link + data[linkKey]}>{content}</Link>);
} else if (child.props.clickable) {
cell.push(<Button variant="link" onClick={() => child.props.onClick(index)}>{content}</Button>);
} else if (linkKey == 'filebuttons') {
content.forEach(element => {
cell.push(<OverlayTrigger key={element} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"export"}`}>Download {element}</Tooltip>} >
<Button variant='table-control-button' onClick={() => child.props.onDownload(element)} disabled={child.props.onDownload == null}>{element + ' ' }
} 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>);
});
} else {
@ -116,8 +121,18 @@ class CustomTable extends Component {
if (child.props.checkbox) {
const checkboxKey = child.props.checkboxKey;
cell.push(<FormCheck className="table-control-checkbox" inline checked={checkboxKey ? data[checkboxKey] : null} onChange={e => child.props.onChecked(index, e)} />);
let isDisabled = false;
if (child.props.checkboxDisabled != null){
isDisabled = !child.props.checkboxDisabled(index)
}
cell.push(
<FormCheck
className="table-control-checkbox"
inline
disabled = {isDisabled}
checked={checkboxKey ? data[checkboxKey] : null}
onChange={e => child.props.onChecked(data, e)}
/>);
}
if (child.props.exportButton) {

View file

@ -122,8 +122,8 @@ class EditICDialog extends React.Component {
case "simulator":
typeOptions = ["dummy","generic","dpsim","rtlab","rscad", "opalrt"];
break;
case "controller":
typeOptions = ["kubernetes","villas-controller"];
case "manager":
typeOptions = ["villas-node","villas-relay","generic"];
break;
case "gateway":
typeOptions = ["villas-node","villas-relay"];
@ -158,10 +158,10 @@ class EditICDialog extends React.Component {
<FormLabel column={false}>Category</FormLabel>
<FormControl as="select" value={this.state.category} onChange={(e) => this.handleChange(e)}>
<option>simulator</option>
<option>controller</option>
<option>service</option>
<option>gateway</option>
<option>equipment</option>
<option>manager</option>
</FormControl>
</FormGroup>
<FormGroup controlId="type">

View file

@ -16,75 +16,93 @@
******************************************************************************/
import React from 'react';
import { Button, ButtonToolbar, DropdownButton, Dropdown } from 'react-bootstrap';
import TimePicker from 'react-bootstrap-time-picker'
import { Button, ButtonToolbar, DropdownButton, Dropdown, InputGroup, FormControl } from 'react-bootstrap';
class ICAction extends React.Component {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.state = {
selectedAction: null,
selectedDelay: 0
};
let t = new Date()
Number.prototype.pad = function(size) {
var s = String(this);
while (s.length < (size || 2)) {s = "0" + s;}
return s;
}
static getDerivedStateFromProps(props, state){
if (state.selectedAction == null) {
if (props.actions != null && props.actions.length > 0) {
return{
selectedAction: props.actions[0]
};
}
}
return null
}
let time = new Date();
time.setMinutes(5 * Math.round(time.getMinutes() / 5 + 1))
setAction = id => {
// search action
for (let action of this.props.actions) {
if (action.id === id) {
this.setState({ selectedAction: action });
}
}
this.state = {
selectedAction: null,
time: time
};
}
setDelayForAction = time => {
// time in int format: (hours * 3600 + minutes * 60 + seconds)
this.setState({selectedDelay: time})
static getDerivedStateFromProps(props, state) {
if (state.selectedAction == null) {
if (props.actions != null && props.actions.length > 0) {
return {
selectedAction: props.actions[0]
};
}
}
return null
}
render() {
let sendCommandDisabled = this.props.runDisabled || this.state.selectedAction == null || this.state.selectedAction.id === "-1"
const actionList = this.props.actions.map(action => (
<Dropdown.Item key={action.id} eventKey={action.id} active={this.state.selectedAction === action.id}>
{action.title}
</Dropdown.Item>
));
return <div>
{"Select delay for command execution (Format hh:mm, max 1h):"}
<TimePicker
format={24}
initialValue={this.state.selectedDelay}
value={this.state.selectedDelay}
start={"00:00"}
end={"01:00"}
step={1}
onChange={this.setDelayForAction}
/>
<ButtonToolbar>
<DropdownButton title={this.state.selectedAction != null ? this.state.selectedAction.title : ''} id="action-dropdown" onSelect={this.setAction}>
{actionList}
</DropdownButton>
<Button style={{ marginLeft: '5px' }} disabled={sendCommandDisabled} onClick={() => this.props.runAction(this.state.selectedAction, this.state.selectedDelay)}>Send command</Button>
</ButtonToolbar>
</div>;
setAction = id => {
// search action
for (let action of this.props.actions) {
if (action.id === id) {
this.setState({ selectedAction: action });
}
}
};
setTimeForAction = (time) => {
this.setState({ time: new Date(time) })
}
render() {
let sendCommandDisabled = this.props.runDisabled || this.state.selectedAction == null || this.state.selectedAction.id === "-1"
let time = this.state.time.getFullYear().pad(4) + '-' +
this.state.time.getMonth().pad(2) + '-' +
this.state.time.getDay().pad(2) + 'T' +
this.state.time.getHours().pad(2) + ':' +
this.state.time.getMinutes().pad(2);
const actionList = this.props.actions.map(action => (
<Dropdown.Item key={action.id} eventKey={action.id} active={this.state.selectedAction === action.id}>
{action.title}
</Dropdown.Item>
));
return <div>
<InputGroup>
<InputGroup.Prepend>
<DropdownButton
variant="outline-secondary"
title={this.state.selectedAction != null ? this.state.selectedAction.title : ''}
id="action-dropdown"
onSelect={this.setAction}>
{actionList}
</DropdownButton>
<FormControl
type="datetime-local"
variant="outline-secondary"
value={time}
onChange={this.setTimeForAction} />
</InputGroup.Prepend>
<Button
variant="outline-secondary"
disabled={sendCommandDisabled}
onClick={() => this.props.runAction(this.state.selectedAction, this.state.time)}>Run</Button>
</InputGroup>
<small className="text-muted">Select time for synced command execution</small>
</div>;
}
}
export default ICAction;

View file

@ -68,8 +68,28 @@ class ICDialog extends React.Component {
<form>
<Row>
<Col>
<h5>Status:</h5>
<h5>State: {this.props.ic.state}</h5>
<h5>Category: {this.props.ic.category}</h5>
<h5>Type: {this.props.ic.type}</h5>
<h5>Uptime: {this.props.ic.uptime}</h5>
<h5>Location: {this.props.ic.location}</h5>
<h5>Description: {this.props.ic.description}</h5>
<h5>Websocket URL: {this.props.ic.websocketurl}</h5>
<h5>API URL: {this.props.ic.apiurl}</h5>
<h5>Start parameter scheme:</h5>
<ReactJson
src={this.props.ic.startParameterScheme}
name={false}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard={false}
collapsed={0}
/>
</Col>
<Col>
<h5>Raw Status:</h5>
<ReactJson
src={this.props.ic.statusupdateraw}
name={false}
@ -79,37 +99,36 @@ class ICDialog extends React.Component {
collapsed={1}
/>
</Col>
{this.props.ic.type === "villas-node" || this.props.ic.type === "villas-relay" ? (
<>
<div className='section-buttons-group-right'>
<Button style={{margin: '5px'}} size='sm' onClick={() => this.downloadGraph(graphURL)}><Icon
icon="download"/></Button>
</div>
<h5>Graph:</h5>
<div>
<img alt={"Graph image download failed and/or incorrect image API URL"} src={graphURL}/>
</div>
{this.props.ic.type === "villas-node" || this.props.ic.type === "villas-relay" ? (
<Col>
<div className='section-buttons-group-right'>
<Button style={{margin: '5px'}} size='sm' onClick={() => this.downloadGraph(graphURL)}><Icon
icon="download"/></Button>
</div>
<h5>Graph:</h5>
<div>
<img alt={"Graph image download failed and/or incorrect image URL"} src={graphURL}/>
</div>
{this.props.userRole === "Admin" ? (
<div>
<h5>Controls:</h5>
{this.props.userRole === "Admin" ? (
<div>
<Button style={{margin: '5px'}} size='lg'
onClick={() => this.setState({confirmCommand: true, command: 'restart'})}>Restart</Button>
<Button style={{margin: '5px'}} size='lg' onClick={() => this.setState({
confirmCommand: true,
command: 'shutdown'
})}>Shutdown</Button>
</div>
</div>)
: (<div/>)}
<h5>Controls:</h5>
<div>
<Button style={{margin: '5px'}} size='lg'
onClick={() => this.setState({confirmCommand: true, command: 'restart'})}>Restart</Button>
<Button style={{margin: '5px'}} size='lg' onClick={() => this.setState({
confirmCommand: true,
command: 'shutdown'
})}>Shutdown</Button>
</div>
</div>)
: (<div/>)}
<ConfirmCommand show={this.state.confirmCommand} command={this.state.command} name={this.props.ic.name}
onClose={c => this.closeConfirmModal(c)}/>
</Col>
): (<div/>)}
<ConfirmCommand show={this.state.confirmCommand} command={this.state.command} name={this.props.ic.name}
onClose={c => this.closeConfirmModal(c)}/>
</>) : (<div/>)}
</Col>
</Row>
</form>
</Dialog>

View file

@ -84,6 +84,7 @@ class InfrastructureComponentStore extends ArrayStore {
let tempIC = action.ic;
if(!tempIC.managedexternally){
tempIC.state = action.data.state;
tempIC.uptime = action.data.time_now - action.data.time_started;
tempIC.statusupdateraw = action.data;
AppDispatcher.dispatch({
type: 'ics/start-edit',

View file

@ -24,8 +24,14 @@ class IcsDataManager extends RestDataManager {
super('ic', '/ic');
}
doActions(ic, action, token = null) {
RestAPI.post(this.makeURL(this.url + '/' + ic.id + '/action'), action, token).then(response => {
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);
}
RestAPI.post(this.makeURL(this.url + '/' + ic.id + '/action'), actions, token).then(response => {
AppDispatcher.dispatch({
type: 'ics/action-started',
data: response

View file

@ -17,11 +17,12 @@
import React, { Component } from 'react';
import { Container } from 'flux/utils';
import { Button, Badge } from 'react-bootstrap';
import {Button, Badge, Tooltip, OverlayTrigger} from 'react-bootstrap';
import FileSaver from 'file-saver';
import _ from 'lodash';
import moment from 'moment'
import AppDispatcher from '../common/app-dispatcher';
import InfrastructureComponentStore from './ic-store';
@ -74,9 +75,27 @@ class InfrastructureComponents extends Component {
}
});
// collect number of external ICs
let externalICs = ics.filter(ic => ic.managedexternally === true)
let numberOfExternalICs = externalICs.length;
// collect all IC categories
let managers = ics.filter(ic => ic.category === "manager")
let gateways = ics.filter(ic => ic.category === "gateway")
let simulators = ics.filter(ic => ic.category === "simulator")
let services = ics.filter(ic => ic.category === "service")
let equipment = ics.filter(ic => ic.category === "equipment")
return {
sessionToken: localStorage.getItem("token"),
ics: ics,
managers: managers,
gateways: gateways,
simulators: simulators,
services: services,
equipment: equipment,
numberOfExternalICs,
modalIC: {},
deleteModal: false,
icModal: false,
@ -126,7 +145,6 @@ class InfrastructureComponents extends Component {
}
}
closeNewModal(data) {
this.setState({ newModal : false });
@ -195,7 +213,9 @@ class InfrastructureComponents extends Component {
}
}
onICChecked(index, event) {
onICChecked(ic, event) {
let index = this.state.ics.indexOf(ic);
const selectedICs = Object.assign([], this.state.selectedICs);
for (let key in selectedICs) {
if (selectedICs[key] === index) {
@ -220,8 +240,10 @@ class InfrastructureComponents extends Component {
this.setState({ selectedICs: selectedICs });
}
runAction(action) {
runAction(action, when) {
for (let index of this.state.selectedICs) {
action.when = when;
AppDispatcher.dispatch({
type: 'ics/start-action',
ic: this.state.ics[index],
@ -241,7 +263,6 @@ class InfrastructureComponents extends Component {
}
stateLabelStyle(state, component){
var style = [ 'badge' ];
if (InfrastructureComponents.isICOutdated(component) && state !== 'shutdown') {
@ -286,7 +307,6 @@ class InfrastructureComponents extends Component {
default:
style.push('badge-default');
/* Possible states of ICs
* 'error': ['resetting', 'error'],
'idle': ['resetting', 'error', 'idle', 'starting', 'shuttingdown'],
@ -305,50 +325,44 @@ class InfrastructureComponents extends Component {
return style.join(' ')
}
stateUpdateModifier(updatedAt) {
stateUpdateModifier(updatedAt, component) {
let dateFormat = 'ddd, DD MMM YYYY HH:mm:ss ZZ';
let dateTime = moment(updatedAt, dateFormat);
return dateTime.fromNow()
}
modifyManagedExternallyColumn(managedExternally){
modifyManagedExternallyColumn(managedExternally, component){
if(managedExternally){
return <Icon icon='check' />
} else {
return ""
}
}
modifyUptimeColumn(uptime){
modifyUptimeColumn(uptime, component){
if(uptime >= 0){
return <span>{uptime + "s"}</span>
let momentDurationFormatSetup = require("moment-duration-format");
momentDurationFormatSetup(moment)
let timeString = moment.duration(uptime, "seconds").humanize();
return <span>{timeString}</span>
}
else{
return <Badge variant="secondary">Unknown</Badge>
}
}
modifyNameColumn(name){
let ic = this.state.ics.find(ic => ic.name === name);
if(ic.type === "villas-node" || ic.type === "villas-relay" || ic.managedexternally){
return <Button variant="link" onClick={() => this.openICStatus(ic)}>{name}</Button> }
else{
return <span>{name}</span>
}
modifyNameColumn(name, component){
let index = this.state.ics.indexOf(component);
return <Button variant="link" style={{color: '#047cab'}} onClick={() => this.openICStatus(component)}>{name}</Button>
}
openICStatus(ic){
let index = this.state.ics.indexOf(ic);
this.setState({ icModal: true, modalIC: ic, modalIndex: index })
}
sendControlCommand(command,ic){
if(command === "restart"){
AppDispatcher.dispatch({
type: 'ics/restart',
@ -362,79 +376,140 @@ class InfrastructureComponents extends Component {
token: this.state.sessionToken,
});
}
}
isExternalIC(index){
let ic = this.state.ics[index]
return ic.managedexternally
}
getICCategoryTable(ics, editable, title){
if (ics && ics.length > 0) {
return (<div>
<h2>{title}</h2>
<Table data={ics}>
<TableColumn
checkbox
checkboxDisabled={(index) => this.isExternalIC(index)}
onChecked={(ic, event) => this.onICChecked(ic, event)}
width='30'
/>
<TableColumn
title='Name'
dataKeys={['name']}
modifier={(name, component) => this.modifyNameColumn(name, component)}
/>
<TableColumn
title='State'
labelKey='state'
tooltipKey='error'
labelStyle={(state, component) => this.stateLabelStyle(state, component)}
/>
<TableColumn
title='Type'
dataKeys={['type']}
/>
<TableColumn
title='Uptime'
dataKey='uptime'
modifier={(uptime, component) => this.modifyUptimeColumn(uptime, component)}
/>
<TableColumn
title='Last Update'
dataKey='stateUpdateAt'
modifier={(stateUpdateAt, component) => this.stateUpdateModifier(stateUpdateAt, component)}
/>
{this.state.currentUser.role === "Admin" && editable ?
<TableColumn
width='200'
editButton
exportButton
deleteButton
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'
exportButton
onExport={index => this.exportIC(index)}
/>
}
</Table>
</div>);
} else {
return <div/>
}
}
render() {
const buttonStyle = {
marginLeft: '10px'
};
let managerTable = this.getICCategoryTable(this.state.managers, false, "IC Managers")
let simulatorTable = this.getICCategoryTable(this.state.simulators, true, "Simulators")
let gatewayTable = this.getICCategoryTable(this.state.gateways, true, "Gateways")
let serviceTable = this.getICCategoryTable(this.state.services, true, "Services")
let equipmentTable = this.getICCategoryTable(this.state.equipment, true, "Equipment")
return (
<div className='section'>
<h1>Infrastructure Components</h1>
<Table data={this.state.ics}>
<TableColumn checkbox onChecked={(index, event) => this.onICChecked(index, event)} width='30' />
<TableColumn title='Name' dataKeys={['name', 'rawProperties.name']} modifier={(name) => this.modifyNameColumn(name)}/>
<TableColumn title='State' labelKey='state' tooltipKey='error' labelStyle={(state, component) => this.stateLabelStyle(state, component)} />
<TableColumn title='Category' dataKeys={['category', 'rawProperties.category']} />
<TableColumn title='Type' dataKeys={['type', 'rawProperties.type']} />
<TableColumn title='Managed externally' dataKey='managedexternally' modifier={(managedexternally) => this.modifyManagedExternallyColumn(managedexternally)} width='105' />
<TableColumn title='Uptime' dataKey='uptime' modifier={(uptime) => this.modifyUptimeColumn(uptime)}/>
<TableColumn title='Location' dataKey='location' />
{/* <TableColumn title='Realm' dataKeys={['properties.realm', 'rawProperties.realm']} /> */}
<TableColumn title='WebSocket URL' dataKey='websocketurl' />
<TableColumn title='API URL' dataKey='apiurl' />
<TableColumn title='Last Update' dataKey='stateUpdateAt' modifier={(stateUpdateAt) => this.stateUpdateModifier(stateUpdateAt)} />
<h1>Infrastructure Components
{this.state.currentUser.role === "Admin" ?
<TableColumn
width='200'
editButton
exportButton
deleteButton
onEdit={index => this.setState({ editModal: true, modalIC: this.state.ics[index], modalIndex: index })}
onExport={index => this.exportIC(index)}
onDelete={index => this.setState({ deleteModal: true, modalIC: this.state.ics[index], modalIndex: index })}
/>
:
<TableColumn
width='100'
exportButton
onExport={index => this.exportIC(index)}
/>
(<span>
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add Infrastructure Component </Tooltip>} >
<Button onClick={() => this.setState({newModal: true})} style={buttonStyle}><Icon icon="plus"
/></Button>
</OverlayTrigger>
<OverlayTrigger
key={2}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"import"}`}> Import Infrastructure Component </Tooltip>} >
<Button onClick={() => this.setState({importModal: true})} style={buttonStyle}><Icon icon="upload"
/></Button>
</OverlayTrigger>
</span>)
:
(<span> </span>)
}
</Table>
{this.state.currentUser.role === "Admin" ?
<div style={{ float: 'left' }}>
</h1>
{managerTable}
{simulatorTable}
{gatewayTable}
{serviceTable}
{equipmentTable}
{this.state.currentUser.role === "Admin" && this.state.numberOfExternalICs > 0 ?
<div style={{float: 'left'}}>
<ICAction
runDisabled={this.state.selectedICs.length === 0}
runAction={action => this.runAction(action)}
runAction={(action, when) => this.runAction(action, when)}
actions={[
{ id: '-1', title: 'Select command', data: { action: 'none' } },
{ id: '0', title: 'Reset', data: { action: 'reset' } },
{ id: '1', title: 'Shutdown', data: { action: 'shutdown' } },
]}
{id: '-1', title: 'Action', data: {action: 'none'}},
{id: '0', title: 'Reset', data: {action: 'reset'}},
{id: '1', title: 'Shutdown', data: {action: 'shutdown'}},
]}
/>
</div>
:
<div> </div>
}
{this.state.currentUser.role === "Admin" ?
<div style={{ float: 'right' }}>
<Button onClick={() => this.setState({ newModal: true })} style={buttonStyle}><Icon icon="plus" /> Infrastructure Component</Button>
<Button onClick={() => this.setState({ importModal: true })} style={buttonStyle}><Icon icon="upload" /> Import</Button>
</div>
:
<div> </div>
<div/>
}
<div style={{ clear: 'both' }} />
<NewICDialog show={this.state.newModal} onClose={data => this.closeNewModal(data)} />
<NewICDialog show={this.state.newModal} onClose={data => this.closeNewModal(data)} managers={this.state.managers} />
<EditICDialog show={this.state.editModal} onClose={data => this.closeEditModal(data)} ic={this.state.modalIC} />
<ImportICDialog show={this.state.importModal} onClose={data => this.closeImportModal(data)} />
<DeleteDialog title="infrastructure-component" name={this.state.modalIC.name || 'Unknown'} show={this.state.deleteModal} onClose={(e) => this.closeDeleteModal(e)} />
<ICDialog
show={this.state.icModal}
onClose={data => this.closeICModal(data)}
@ -443,7 +518,6 @@ class InfrastructureComponents extends Component {
userRole={this.state.currentUser.role}
sendControlCommand={(command, ic) => this.sendControlCommand(command, ic)}/>
<DeleteDialog title="infrastructure-component" name={this.state.modalIC.name || 'Unknown'} show={this.state.deleteModal} onClose={(e) => this.closeDeleteModal(e)} />
</div>
);
}

View file

@ -34,7 +34,8 @@ class NewICDialog extends React.Component {
category: '',
managedexternally: false,
description: '',
location: ''
location: '',
manager: ''
};
}
@ -48,7 +49,8 @@ class NewICDialog extends React.Component {
uuid: this.state.uuid,
managedexternally: this.state.managedexternally,
location: this.state.location,
description: this.state.description
description: this.state.description,
manager: this.state.manager
};
if (this.state.websocketurl != null && this.state.websocketurl !== "" && this.state.websocketurl !== 'http://') {
@ -88,6 +90,7 @@ class NewICDialog extends React.Component {
let websocketurl = true;
let type = true;
let category = true;
let manager = true;
if (this.state.name === '') {
name = false;
@ -97,6 +100,10 @@ class NewICDialog extends React.Component {
uuid = false;
}
if(this.state.managedexternally && manager === ''){
manager = false;
}
if (this.state.type === '') {
type = false;
}
@ -105,7 +112,7 @@ class NewICDialog extends React.Component {
category = false;
}
this.valid = name && uuid && websocketurl && type && category;
this.valid = name && uuid && websocketurl && type && category && manager;
// return state to control
if (target === 'name') return name ? "success" : "error";
@ -113,6 +120,7 @@ class NewICDialog extends React.Component {
if (target === 'websocketurl') return websocketurl ? "success" : "error";
if (target === 'type') return type ? "success" : "error";
if (target === 'category') return category ? "success" : "error";
if (target === 'manager') return manager ? "success" : "error";
return this.valid;
}
@ -131,8 +139,8 @@ class NewICDialog extends React.Component {
case "simulator":
typeOptions = ["dummy","generic","dpsim","rtlab","rscad","opalrt"];
break;
case "controller":
typeOptions = ["kubernetes","villas-controller"];
case "manager":
typeOptions = ["villas-node","villas-relay","generic"];
break;
case "gateway":
typeOptions = ["villas-node","villas-relay"];
@ -146,33 +154,60 @@ class NewICDialog extends React.Component {
default:
typeOptions =[];
}
let managerOptions = [];
managerOptions.push(<option default>Select manager</option>);
for (let m of this.props.managers) {
managerOptions.push (
<option key={m.id} value={m.uuid}>{m.name}</option>
);
}
return (
<Dialog show={this.props.show} title="New Infrastructure Component" buttonTitle="Add" onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={this.validateForm()}>
<form>
<FormGroup controlId="managedexternally">
<OverlayTrigger key="3" placement={'left'} overlay={<Tooltip id={`tooltip-${"me"}`}>An externally managed component will show up in the list only after a VILLAScontroller for the component type has created the component and cannot be edited by users</Tooltip>} >
<FormCheck type={"checkbox"} label={"Managed externally"} defaultChecked={this.state.managedexternally} onChange={e => this.handleChange(e)}>
</FormCheck>
</OverlayTrigger>
</FormGroup>
{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>} >
<FormCheck type={"checkbox"} label={"Managed externally"} defaultChecked={this.state.managedexternally} onChange={e => this.handleChange(e)}>
</FormCheck>
</OverlayTrigger>
</FormGroup>
{this.state.managedexternally === true ?
<FormGroup controlId="manager" valid={this.validateForm('manager')}>
<OverlayTrigger key="0" placement={'right'} overlay={<Tooltip id={`tooltip-${"required"}`}> Required field </Tooltip>} >
<FormLabel>Manager to create new IC *</FormLabel>
</OverlayTrigger>
<FormControl as="select" value={this.state.manager} onChange={(e) => this.handleChange(e)}>
{managerOptions}
</FormControl>
</FormGroup>
: <div/>
}
</>
: <div/>
}
<FormGroup controlId="name" valid={this.validateForm('name')}>
<OverlayTrigger key="0" placement={'right'} overlay={<Tooltip id={`tooltip-${"required"}`}> Required field </Tooltip>} >
<OverlayTrigger key="1" placement={'right'} overlay={<Tooltip id={`tooltip-${"required"}`}> Required field </Tooltip>} >
<FormLabel>Name *</FormLabel>
</OverlayTrigger>
<FormControl type="text" placeholder="Enter name" value={this.state.name} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="category" valid={this.validateForm('category')}>
<OverlayTrigger key="1" placement={'right'} overlay={<Tooltip id={`tooltip-${"required"}`}> Required field </Tooltip>} >
<OverlayTrigger key="2" placement={'right'} overlay={<Tooltip id={`tooltip-${"required"}`}> Required field </Tooltip>} >
<FormLabel>Category of component *</FormLabel>
</OverlayTrigger>
<FormControl as="select" value={this.state.category} onChange={(e) => this.handleChange(e)}>
<option default>Select category</option>
<option>simulator</option>
<option>controller</option>
<option>service</option>
<option>gateway</option>
<option>equipment</option>
<option>manager</option>
</FormControl>
</FormGroup>
<FormGroup controlId="type" valid={this.validateForm('type')}>
@ -206,11 +241,15 @@ class NewICDialog extends React.Component {
<FormControl type="text" placeholder="Enter Description" value={this.state.description} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
<FormGroup controlId="uuid" valid={this.validateForm('uuid')}>
<FormLabel>UUID</FormLabel>
<FormControl type="text" placeholder="Enter uuid" value={this.state.uuid} onChange={(e) => this.handleChange(e)} />
<FormControl.Feedback />
</FormGroup>
{this.state.managedexternally === false ?
<FormGroup controlId="uuid" valid={this.validateForm('uuid')}>
<FormLabel>UUID</FormLabel>
<FormControl type="text" placeholder="Enter uuid" value={this.state.uuid}
onChange={(e) => this.handleChange(e)}/>
<FormControl.Feedback/>
</FormGroup>
: <div/>
}
</form>
</Dialog>
);

View file

@ -16,7 +16,7 @@
******************************************************************************/
import React from 'react';
import {FormGroup, FormControl, FormLabel, Col, Button, ProgressBar} from 'react-bootstrap';
import { FormGroup, FormControl, FormLabel, Col, Row, Button, ProgressBar } from 'react-bootstrap';
import AppDispatcher from "../common/app-dispatcher";
import FileStore from "../file/file-store"
@ -38,8 +38,6 @@ class EditResultDialog extends React.Component {
uploadFile: null,
uploadProgress: 0,
files: null,
result: null,
resultExists: false,
};
}
@ -53,26 +51,38 @@ class EditResultDialog extends React.Component {
this.setState({ [event.target.id]: event.target.value });
};
isEmpty(val) {
return (val === undefined || val == null || val.length <= 0);
};
componentDidUpdate(prevProps, prevState) {
if (this.state.resultExists && this.props.files != prevProps.files) {
this.setState({files: FileStore.getState().filter(file => this.state.result.resultFileIDs.includes(file.id))});
if (this.props.resultId != prevProps.resultId || this.props.results != prevProps.results) {
let result = this.props.results[this.props.resultId];
if (result && Object.keys(result).length != 0) {
let hasFiles = !this.isEmpty(result.resultFileIDs);
if (hasFiles) {
this.setState({
id: result.id,
description: result.description,
files: FileStore.getState().filter(file => result.resultFileIDs.includes(file.id)),
})
} else {
this.setState({
id: result.id,
description: result.description,
files: null,
})
}
}
}
if (this.props.result != prevProps.result && Object.keys(this.props.result).length != 0) {
this.setState({
id: this.props.result.id,
description: this.props.result.description,
result: this.props.result,
resultExists: true,
files: FileStore.getState().filter(file => this.props.result.resultFileIDs.includes(file.id)),
})
}
}
};
selectUploadFile(event) {
this.setState({ uploadFile: event.target.files[0] });
};
startFileUpload(){
startFileUpload() {
const formData = new FormData();
formData.append("file", this.state.uploadFile);
@ -97,68 +107,99 @@ class EditResultDialog extends React.Component {
this.setState({ uploadProgress: parseInt(event.percent.toFixed(), 10) });
};
deleteFile(index){
deleteFile(index) {
let file = this.state.files[index];
AppDispatcher.dispatch({
type: 'files/start-remove',
data: file,
type: 'resultfiles/start-remove',
resultID: this.state.id,
fileID: file.id,
token: this.props.sessionToken
});
}
submitDescription() {
let result = this.props.results[this.props.resultId];
if (!this.isEmpty(result)) {
result.description = this.state.description;
AppDispatcher.dispatch({
type: 'results/start-edit',
data: result,
token: this.props.sessionToken
});
}
}
render() {
return <Dialog show={this.props.show}
title={'Edit Result No. '+this.state.id}
buttonTitle='Close'
onClose={() => this.onClose()}
blendOutCancel = {true}
valid={true}
size = 'lg'>
title={'Edit Result No. ' + this.state.id}
buttonTitle='Close'
onClose={() => this.onClose()}
blendOutCancel={true}
valid={true}
size='lg'>
<div>
<FormGroup as={Col} controlId='description'>
<FormLabel column={false}>Description</FormLabel>
<FormControl type='text' placeholder='Enter description' value={this.state.description} onChange={this.handleChange} />
<FormControl.Feedback />
<Row style={{ float: 'center' }} >
<Col xs lg="2">
<FormLabel>Description</FormLabel>
</Col>
<Col xs lg="4">
<FormControl type='text' placeholder={this.state.description} value={this.state.description} onChange={this.handleChange} />
<FormControl.Feedback />
</Col>
<Col xs lg="2">
<Button
type="submit"
onClick={() => this.submitDescription()}>
Save
</Button>
</Col>
</Row>
</FormGroup>
<Table data={this.state.files}>
<TableColumn title='ID' dataKey='id'/>
<TableColumn title='Name' dataKey='name'/>
<TableColumn title='Size (bytes)' dataKey='size'/>
<TableColumn title='Type' dataKey='type'/>
<TableColumn
title=''
deleteButton
onDelete={(index) => this.deleteFile(index)}
/>
<TableColumn title='ID' dataKey='id' />
<TableColumn title='Name' dataKey='name' />
<TableColumn title='Size (bytes)' dataKey='size' />
<TableColumn title='Type' dataKey='type' />
<TableColumn
title=''
deleteButton
onDelete={(index) => this.deleteFile(index)}
/>
</Table>
<div style={{ float: 'center' }}>
<h5>Add result file</h5>
<Row>
<Col xs lg="4">
<FormControl type='file' onChange={(event) => this.selectUploadFile(event)} />
</Col>
<Col xs lg="2">
<Button
disabled={this.state.uploadFile === null}
onClick={() => this.startFileUpload()}>
Upload
</Button>
</Col>
</Row>
</div>
<FormGroup controlId='resultfile'>
<FormLabel>Add Result File</FormLabel>
<FormControl type='file' onChange={(event) => this.selectUploadFile(event)} />
<br></br>
<FormGroup as={Col} >
<ProgressBar
striped={true}
animated={true}
now={this.state.uploadProgress}
label={this.state.uploadProgress + '%'}
/>
</FormGroup>
<FormGroup as={Col} >
<Button
disabled={this.state.uploadFile === null}
onClick={() => this.startFileUpload()}>
Upload
</Button>
</FormGroup>
<FormGroup as={Col} >
<ProgressBar
striped={true}
animated={true}
now={this.state.uploadProgress}
label={this.state.uploadProgress + '%'}
/>
</FormGroup>
</div>
</Dialog>;
}

View file

@ -15,40 +15,15 @@
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import ArrayStore from '../common/array-store';
import ResultsDataManager from './results-data-manager';
import FilesDataManager from '../file/files-data-manager'
import AppDispatcher from '../common/app-dispatcher';
class ResultStore extends ArrayStore {
constructor() {
super('results', ResultsDataManager);
}
saveFile(state, action){
let fileID = parseInt(action.id)
state.forEach((element, index, array) => {
if (element.id === fileID) {
// save blob object
array[index]["data"] = new Blob([action.data.data], {type: action.data.type});
// update file type
array[index]["type"] = action.data.type;
if (array[index]["objectURL"] !== ''){
// free memory of previously generated object URL
URL.revokeObjectURL(array[index]["objectURL"]);
}
// create an object URL for the file
array[index]["objectURL"] = URL.createObjectURL(array[index]["data"])
}
});
// announce change to listeners
this.__emitChange();
return state
}
simplify(timestamp) {
let parts = timestamp.split("T");
let datestr = parts[0];
@ -67,17 +42,39 @@ class ResultStore extends ArrayStore {
reduce(state, action) {
switch (action.type) {
case 'results/loaded':
this.simplifyTimestamps(action.data);
if (Array.isArray(action.data)) {
this.simplifyTimestamps(action.data);
} else {
this.simplifyTimestamps([action.data]);
}
return super.reduce(state, action);
case 'results/added':
this.simplifyTimestamps([action.data]);
return super.reduce(state, action);
case 'results/edited':
this.simplifyTimestamps([action.data]);
return super.reduce(state, action);
case 'results/removed':
// Remove files from filestore
action.data.resultFileIDs.forEach(fileid => {
AppDispatcher.dispatch({
type: 'files/removed',
data: fileid
});
});
return super.reduce(state, action);
case 'resultfiles/start-upload':
ResultsDataManager.uploadFile(action.data, action.resultID, action.token, action.progressCallback, action.finishedCallback, action.scenarioID);
return state;
case 'resultfiles/start-remove':
ResultsDataManager.removeFile(action.resultID, action.fileID, action.token);
return state;
default:
return super.reduce(state, action);
}

View file

@ -19,24 +19,24 @@ import RestDataManager from '../common/data-managers/rest-data-manager';
import RestAPI from '../common/api/rest-api';
import AppDispatcher from '../common/app-dispatcher';
class ResultsDataManager extends RestDataManager{
class ResultsDataManager extends RestDataManager {
constructor() {
super('result', '/results');
}
uploadFile(file, resultID, token = null, progressCallback = null, finishedCallback = null, scenarioID) {
RestAPI.upload(this.makeURL(this.url + '/' + resultID + '/file') , file, token, progressCallback, scenarioID).then(response => {
RestAPI.upload(this.makeURL(this.url + '/' + resultID + '/file'), file, token, progressCallback, scenarioID).then(response => {
AppDispatcher.dispatch({
type: 'files/uploaded',
});
// Trigger a results reload
// Trigger a result reload
AppDispatcher.dispatch({
type: 'results/start-load',
param: '?scenarioID=' + scenarioID,
token: token
data: resultID,
token: token,
});
// Trigger a files reload
@ -57,6 +57,23 @@ class ResultsDataManager extends RestDataManager{
});
}
removeFile(resultID, fileID, token) {
RestAPI.delete(this.makeURL(this.url + '/' + resultID + '/file/' + fileID), token).then(response => {
// reload result
AppDispatcher.dispatch({
type: 'results/start-load',
data: resultID,
token: token,
});
// update files
AppDispatcher.dispatch({
type: 'files/removed',
data: fileID,
token: token,
});
});
}
}
export default new ResultsDataManager();

View file

@ -103,6 +103,7 @@ class Scenario extends React.Component {
files: FileStore.getState().filter(file => file.scenarioID === parseInt(props.match.params.scenario, 10)),
ics: ICStore.getState(),
ExternalICInUse: false,
deleteConfigModal: false,
importConfigModal: false,
@ -113,9 +114,11 @@ class Scenario extends React.Component {
editResultsModal: prevState.editResultsModal || false,
modalResultsData: {},
modalResultsIndex: 0,
modalResultsIndex: prevState.modalResultsIndex,
newResultModal: false,
filesToDownload: [],
filesToDownload: prevState.filesToDownload,
zipfiles: prevState.zipfiles || false,
resultNodl: prevState.resultNodl,
editOutputSignalsModal: prevState.editOutputSignalsModal || false,
editInputSignalsModal: prevState.editInputSignalsModal || false,
@ -158,25 +161,28 @@ class Scenario extends React.Component {
componentDidUpdate(prevProps, prevState) {
// check whether file data has been loaded
if (this.state.filesToDownload.length > 0 ) {
if (this.state.filesToDownload.length === 1) {
let fileToDownload = FileStore.getState().filter(file => file.id === this.state.filesToDownload[0])
if (fileToDownload.length === 1 && fileToDownload[0].data) {
const blob = new Blob([fileToDownload[0].data], {type: fileToDownload[0].type});
FileSaver.saveAs(blob, fileToDownload[0].name);
this.setState({ filesToDownload: [] });
}
} else { // zip and save several files
let filesToDownload = FileStore.getState().filter(file => this.state.filesToDownload.includes(file.id) && file.data);
if (filesToDownload.length === this.state.filesToDownload.length) { // all requested files have been loaded
var zip = new JSZip();
filesToDownload.forEach(file => {
zip.file(file.name, file.data);
});
zip.generateAsync({type: "blob"}).then(function(content) {
saveAs(content, "results.zip");
});
this.setState({ filesToDownload: [] });
if (this.state.filesToDownload && this.state.filesToDownload.length > 0 ) {
if (this.state.files != prevState.files) {
if (!this.state.zipfiles) {
let fileToDownload = FileStore.getState().filter(file => file.id === this.state.filesToDownload[0])
if (fileToDownload.length === 1 && fileToDownload[0].data) {
const blob = new Blob([fileToDownload[0].data], {type: fileToDownload[0].type});
FileSaver.saveAs(blob, fileToDownload[0].name);
this.setState({ filesToDownload: [] });
}
} else { // zip and save one or more files (download all button)
let filesToDownload = FileStore.getState().filter(file => this.state.filesToDownload.includes(file.id) && file.data);
if (filesToDownload.length === this.state.filesToDownload.length) { // all requested files have been loaded
var zip = new JSZip();
filesToDownload.forEach(file => {
zip.file(file.name, file.data);
});
let zipname = "result_" + this.state.resultNodl + "_" + (new Date()).toISOString();
zip.generateAsync({type: "blob"}).then(function(content) {
saveAs(content, zipname);
});
this.setState({ filesToDownload: [] });
}
}
}
}
@ -361,9 +367,30 @@ class Scenario extends React.Component {
this.setState({ selectedConfigs: selectedConfigs });
}
runAction(action, delay) {
// delay in seconds
usesExternalIC(index){
let icID = this.state.configs[index].icID;
let ic = null;
for (let component of this.state.ics) {
if (component.id === this.state.configs[index].icID) {
ic = component;
}
}
if (ic == null) {
return false;
}
if (ic.managedexternally === true){
this.setState({ExternalICInUse: true})
return true
}
return false
}
runAction(action, when) {
if (action.data.action === 'none') {
console.warn("No command selected. Nothing was sent.");
return;
@ -386,8 +413,7 @@ class Scenario extends React.Component {
action.data.parameters = this.state.configs[index].startParameters;
}
// Unix time stamp + delay
action.data.when = Math.round(Date.now() / 1000.0 + delay)
action.data.when = when;
console.log("Sending action: ", action.data)
@ -613,21 +639,19 @@ class Scenario extends React.Component {
closeEditResultsModal() {
this.setState({ editResultsModal: false });
AppDispatcher.dispatch({
type: 'results/start-load',
token: this.state.sessionToken,
param: '?scenarioID=' + this.state.scenario.id
})
}
downloadResultData(param) {
let toDownload = [];
let zip = false;
if (typeof(param) === 'object') {
if (typeof(param) === 'object') { // download all files
toDownload = param.resultFileIDs;
} else {
zip = true;
this.setState({ filesToDownload: toDownload, zipfiles: zip, resultNodl: param.id });
} else { // download one file
toDownload.push(param);
this.setState({ filesToDownload: toDownload, zipfiles: zip});
}
toDownload.forEach(fileid => {
@ -637,8 +661,6 @@ class Scenario extends React.Component {
token: this.state.sessionToken
});
});
this.setState({ filesToDownload: toDownload });
}
closeDeleteResultsModal(confirmDelete) {
@ -727,6 +749,7 @@ class Scenario extends React.Component {
title='Files/Data'
dataKey='resultFileIDs'
linkKey='filebuttons'
data={this.state.files}
width='300'
onDownload={(index) => this.downloadResultData(index)}
/>
@ -736,7 +759,7 @@ class Scenario extends React.Component {
editButton
downloadAllButton
deleteButton
onEdit={index => this.setState({ editResultsModal: true, modalResultsData: this.state.results[index], modalResultsIndex: index })}
onEdit={index => this.setState({ editResultsModal: true, modalResultsIndex: index })}
onDownloadAll={(index) => this.downloadResultData(this.state.results[index])}
onDelete={(index) => this.setState({ deleteResultsModal: true, modalResultsData: this.state.results[index], modalResultsIndex: index })}
/>
@ -746,7 +769,8 @@ class Scenario extends React.Component {
sessionToken={this.state.sessionToken}
show={this.state.editResultsModal}
files={this.state.files}
result={this.state.modalResultsData}
results={this.state.results}
resultId={this.state.modalResultsIndex}
scenarioID={this.state.scenario.id}
onClose={this.closeEditResultsModal.bind(this)} />
<DeleteDialog title="result" name={this.state.modalResultsData.id} show={this.state.deleteResultsModal} onClose={(e) => this.closeDeleteResultsModal(e)} />
@ -772,15 +796,27 @@ class Scenario extends React.Component {
scenarioID={this.state.scenario.id}
/>
{/*Component Configurations table*/}
<h2 style={tableHeadingStyle}>Component Configurations
<Button onClick={() => this.addConfig()} style={buttonStyle}><Icon icon="plus" /></Button>
<Button onClick={() => this.setState({ importConfigModal: true })} style={buttonStyle}><Icon icon="upload" /></Button>
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add Component Configuration </Tooltip>} >
<Button onClick={() => this.addConfig()} style={buttonStyle}><Icon icon="plus" /></Button>
</OverlayTrigger>
<OverlayTrigger
key={2}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"import"}`}> Import Component Configuration </Tooltip>} >
<Button onClick={() => this.setState({ importConfigModal: true })} style={buttonStyle}><Icon icon="upload" /></Button>
</OverlayTrigger>
</h2>
<Table data={this.state.configs}>
<TableColumn checkbox onChecked={(index, event) => this.onConfigChecked(index, event)} width='30' />
<TableColumn
checkbox
checkboxDisabled={(index) => this.usesExternalIC(index)}
onChecked={(index, event) => this.onConfigChecked(index, event)}
width='30' />
<TableColumn title='Name' dataKey='name' />
<TableColumn title='Configuration file(s)' dataKey='fileIDs' modifier={(fileIDs) => this.getListOfFiles(fileIDs, ['json', 'JSON'])} />
<TableColumn
@ -822,19 +858,21 @@ class Scenario extends React.Component {
/>
</Table>
<div style={{ float: 'left' }}>
<ICAction
runDisabled={this.state.selectedConfigs.length === 0}
runAction={(action, delay) => this.runAction(action, delay)}
actions={[
{ id: '-1', title: 'Select command', data: { action: 'none' } },
{ id: '0', title: 'Start', data: { action: 'start' } },
{ id: '1', title: 'Stop', data: { action: 'stop' } },
{ id: '2', title: 'Pause', data: { action: 'pause' } },
{ id: '3', title: 'Resume', data: { action: 'resume' } }
]} />
</div>
{ this.state.ExternalICInUse ? (
<div style={{float: 'left'}}>
<ICAction
runDisabled={this.state.selectedConfigs.length === 0}
runAction={(action, when) => this.runAction(action, when)}
actions={[
{id: '-1', title: 'Action', data: {action: 'none'}},
{id: '0', title: 'Start', data: {action: 'start'}},
{id: '1', title: 'Stop', data: {action: 'stop'}},
{id: '2', title: 'Pause', data: {action: 'pause'}},
{id: '3', title: 'Resume', data: {action: 'resume'}}
]}/>
</div>
) : (<div/>)
}
<div style={{ clear: 'both' }} />
<EditConfigDialog
@ -868,8 +906,18 @@ class Scenario extends React.Component {
{/*Dashboard table*/}
<h2 style={tableHeadingStyle}>Dashboards
<Button onClick={() => this.setState({ newDashboardModal: true })} style={buttonStyle}><Icon icon="plus" /></Button>
<Button onClick={() => this.setState({ importDashboardModal: true })} style={buttonStyle}><Icon icon="upload" /></Button>
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add Dashboard </Tooltip>} >
<Button onClick={() => this.setState({ newDashboardModal: true })} style={buttonStyle}><Icon icon="plus" /></Button>
</OverlayTrigger>
<OverlayTrigger
key={2}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"import"}`}> Import Dashboard </Tooltip>} >
<Button onClick={() => this.setState({ importDashboardModal: true })} style={buttonStyle}><Icon icon="upload" /></Button>
</OverlayTrigger>
</h2>
<Table data={this.state.dashboards}>
<TableColumn title='Name' dataKey='name' link='/dashboards/' linkKey='id' />
@ -896,7 +944,12 @@ class Scenario extends React.Component {
{/*Result table*/}
<h2 style={tableHeadingStyle}>Results
<Button onClick={() => this.setState({ newResultModal: true })} style={buttonStyle}><Icon icon="plus" /></Button>
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add Result </Tooltip>} >
<Button onClick={() => this.setState({ newResultModal: true })} style={buttonStyle}><Icon icon="plus" /></Button>
</OverlayTrigger>
</h2>
{resulttable}
<NewResultDialog show={this.state.newResultModal} onClose={data => this.closeNewResultModal(data)} />

View file

@ -17,7 +17,7 @@
import React, { Component } from 'react';
import { Container } from 'flux/utils';
import { Button } from 'react-bootstrap';
import {Button, OverlayTrigger, Tooltip} from 'react-bootstrap';
import FileSaver from 'file-saver';
import AppDispatcher from '../common/app-dispatcher';
@ -242,8 +242,18 @@ class Scenarios extends Component {
return (
<div className='section'>
<h1>Scenarios
<Button onClick={() => this.setState({ newModal: true })} style={buttonStyle}><Icon icon="plus" /></Button>
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add Scenario </Tooltip>} >
<Button onClick={() => this.setState({ newModal: true })} style={buttonStyle}><Icon icon="plus" /></Button>
</OverlayTrigger>
<OverlayTrigger
key={2}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"import"}`}> Import Scenario </Tooltip>} >
<Button onClick={() => this.setState({ importModal: true })} style={buttonStyle}><Icon icon="upload" /></Button>
</OverlayTrigger>
</h1>
<Table data={this.state.scenarios}>

View file

@ -0,0 +1,3 @@
.swagger-ui div.scheme-container {
display: none
}

View file

@ -398,6 +398,8 @@ div[class*="-widget"] label {
/* Begin time offset widget */
.time-offset {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-around;

View file

@ -72,14 +72,14 @@ class LoginForm extends Component {
<FormGroup controlId="username">
<FormLabel column={true}>Username</FormLabel>
<Col>
<FormControl type="text" placeholder="Username" onChange={(e) => this.handleChange(e)} />
<FormControl type="text" placeholder="Username" autoComplete="username" onChange={(e) => this.handleChange(e)} />
</Col>
</FormGroup>
<FormGroup controlId="password">
<FormLabel column={true}>Password</FormLabel>
<Col >
<FormControl type="password" placeholder="Password" onChange={(e) => this.handleChange(e)} />
<FormControl type="password" placeholder="Password" autoComplete="current-password" onChange={(e) => this.handleChange(e)} />
</Col>
</FormGroup>

View file

@ -17,7 +17,7 @@
import React from 'react';
import Dialog from '../common/dialogs/dialog';
import Config from '../config.js';
import Config from '../config';
class RecoverPassword extends React.Component {
@ -29,13 +29,10 @@ class RecoverPassword extends React.Component {
}
}
onClose() {
this.props.onClose();
}
render() {
return (
<Dialog show={this.props.show} title="Recover password" buttonTitle="Close" onClose={(c) => this.onClose(c)} blendOutCancel = {true} valid={true} size = 'lg'>
@ -43,7 +40,7 @@ class RecoverPassword extends React.Component {
<div>Please contact:</div>
<div>{this.state.admin.name}</div>
<div>E-Mail:</div>
<a href={`mailto:${this.state.admin.mail}`}>{this.state.admin.mail}</a>
<a href={`mailto:${this.state.admin.mail}`}>{this.state.admin.mail}</a>
</div>
</Dialog>
);

View file

@ -17,7 +17,7 @@
import React, { Component } from 'react';
import { Container } from 'flux/utils';
import { Button } from 'react-bootstrap';
import {Button, OverlayTrigger, Tooltip} from 'react-bootstrap';
import AppDispatcher from '../common/app-dispatcher';
import UsersStore from './users-store';
@ -130,9 +130,22 @@ class Users extends Component {
render() {
const buttonStyle = {
marginLeft: '10px'
};
return (
<div>
<h1>Users</h1>
<h1>Users
<OverlayTrigger
key={1}
placement={'top'}
overlay={<Tooltip id={`tooltip-${"add"}`}> Add User </Tooltip>} >
<Button style={buttonStyle} onClick={() => this.setState({ newModal: true })}><Icon icon='plus' /> </Button>
</OverlayTrigger>
</h1>
<Table data={this.state.users}>
<TableColumn title='Username' width='150' dataKey='username' />
@ -143,7 +156,7 @@ class Users extends Component {
<TableColumn width='200' editButton deleteButton onEdit={index => this.setState({ editModal: true, modalData: this.state.users[index] })} onDelete={index => this.setState({ deleteModal: true, modalData: this.state.users[index] })} />
</Table>
<Button onClick={() => this.setState({ newModal: true })}><Icon icon='plus' /> User</Button>
<NewUserDialog show={this.state.newModal} onClose={(data) => this.closeNewModal(data)} />
<EditUserDialog show={this.state.editModal} onClose={(data) => this.closeEditModal(data)} user={this.state.modalData} />

View file

@ -170,7 +170,8 @@ export default function CreateControls(widgetType = null, widget = null, session
<EditWidgetNumberControl key={1} widget={widget} controlId={'customProperties.threshold_yellow'} label={'Threshold yellow'} defaultValue={0} handleChange={(e) => handleChange(e)} />,
<EditWidgetNumberControl key={2} widget={widget} controlId={'customProperties.threshold_red'} label={'Threshold red'} defaultValue={0} handleChange={(e) => handleChange(e)} />,
<EditWidgetCheckboxControl key={3} widget={widget} controlId={'customProperties.horizontal'} input text="Horizontal" handleChange={e => handleChange(e)} />,
<EditWidgetCheckboxControl key={4} widget={widget} controlId={'customProperties.showOffset'} input text="showOffset" handleChange={e => handleChange(e)} />,
<EditWidgetCheckboxControl key={4} widget={widget} controlId={'customProperties.showName'} input text="showName" handleChange={e => handleChange(e)} />,
<EditWidgetCheckboxControl key={5} widget={widget} controlId={'customProperties.showOffset'} input text="showOffset" handleChange={e => handleChange(e)} />,
);
break;

View file

@ -19,6 +19,8 @@ import React, { Component } from 'react';
import { FormGroup, OverlayTrigger, Tooltip , FormLabel, Button } from 'react-bootstrap';
import ColorPicker from './color-picker'
import Icon from "../../common/icon";
import {scaleOrdinal} from "d3-scale";
import {schemeCategory10} from "d3-scale-chromatic";
// schemeCategory20 no longer available in d3
@ -36,8 +38,20 @@ class EditWidgetPlotColorsControl extends Component {
}
static getDerivedStateFromProps(props, state){
let widget = props.widget;
if(widget.customProperties.lineColors === undefined || widget.customProperties.lineColors === null){
// for backwards compatibility with old plots
widget.customProperties.lineColors = []
const newLineColor = scaleOrdinal(schemeCategory10);
for (let signalID of widget.signalIDs){
widget.customProperties.lineColors.push(newLineColor(signalID))
}
}
return {
widget: props.widget
widget: widget
};
}
@ -62,9 +76,9 @@ class EditWidgetPlotColorsControl extends Component {
this.setState({selectedIndex: null});
}
}
render() {
return (
<FormGroup>
<FormLabel>Line Colors</FormLabel>
@ -81,7 +95,7 @@ class EditWidgetPlotColorsControl extends Component {
}
let signal = this.props.signals.find(signal => signal.id === signalID);
return (<OverlayTrigger key={idx} placement={'bottom'} overlay={<Tooltip id={'tooltip-${"signal name"}'}>{signal.name}</Tooltip>}>
<Button
style={style} key={idx} onClick={i => this.editLineColor(signalID)} ><Icon icon="pen" /></Button>

View file

@ -168,6 +168,7 @@ class WidgetFactory {
widget.customProperties.valueMin = 0;
widget.customProperties.valueMax = 1;
widget.customProperties.valueUseMinMax = false;
widget.customProperties.lockAspect = true;
break;
case 'Box':
widget.minWidth = 50;
@ -199,16 +200,17 @@ class WidgetFactory {
break;
case 'TimeOffset':
widget.minWidth = 20;
widget.minHeight = 20;
widget.width = 100;
widget.height = 40;
widget.minWidth = 200;
widget.minHeight = 80;
widget.width = 200;
widget.height = 80;
widget.customProperties.threshold_yellow = 1;
widget.customProperties.threshold_red = 2;
widget.customProperties.icID = -1;
widget.customProperties.horizontal = true;
widget.customProperties.showOffset = true;
widget.customProperties.lockAspect = true;
widget.customProperties.showName = true;
break;
default:

View file

@ -20,7 +20,7 @@ import { scaleOrdinal} from 'd3-scale';
import {schemeCategory10} from 'd3-scale-chromatic'
function Legend(props){
const signal = props.sig;
const hasScalingFactor = (signal.scalingFactor !== 1);
@ -52,10 +52,15 @@ class PlotLegend extends React.Component {
return <div className="plot-legend">
<ul>
{
{ this.props.lineColors !== undefined && this.props.lineColors != null ? (
this.props.signals.map( signal =>
<Legend key={signal.id} sig={signal} lineColor={this.props.lineColors[signal.id]}/>
)}
)) : (
this.props.signals.map( signal =>
<Legend key={signal.id} sig={signal} lineColor={"undefined"}/>
))
}
</ul>
</div>;
}

View file

@ -207,6 +207,11 @@ class Plot extends React.Component {
const lines = this.state.data.map((values, index) => {
let signalID = this.props.signalIDs[index];
if(this.props.lineColors === undefined || this.props.lineColors === null){
this.props.lineColors = [] // for backwards compatibility
}
if (typeof this.props.lineColors[signalID] === "undefined") {
this.props.lineColors[signalID] = newLineColor(signalID);
}

View file

@ -66,19 +66,21 @@ class WidgetTimeOffset extends Component {
} else if (this.props.widget.customProperties.showOffset){
icSelected = this.state.timeOffset + 's';
}
return (
<div className="time-offset">
{this.props.widget.customProperties.icID !== -1 ?
(<span></span>) : (<span>no IC</span>)
}
}
{this.props.widget.customProperties.icID !== -1 && this.props.widget.customProperties.showName ?
(<span>{this.state.icName}</span>) : (<span></span>)
}
<OverlayTrigger key={0} placement={'left'} overlay={<Tooltip id={`tooltip-${"traffic-light"}`}>
{this.props.widget.customProperties.icID !== -1 ?
(<span>{this.state.icName}<br></br>Offset: {this.state.timeOffset + "s"}</span>)
:
(<span>Please select Infrastructure Component</span>)}
</Tooltip>}>
<TrafficLight Horizontal={this.props.widget.customProperties.horizontal} width={this.props.widget.width} height={this.props.widget.height}
<TrafficLight Horizontal={this.props.widget.customProperties.horizontal} width={this.props.widget.width - 40} height={this.props.widget.height - 40}
RedOn={(this.props.widget.customProperties.threshold_red <= this.state.timeOffset) || !this.state.websocketOpen}
YellowOn={(this.props.widget.customProperties.threshold_yellow <= this.state.timeOffset) && (this.state.timeOffset < this.props.widget.customProperties.threshold_red) && this.state.websocketOpen}
GreenOn={(this.state.timeOffset < this.props.widget.customProperties.threshold_yellow) && this.state.websocketOpen}