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:
commit
18966f86e0
36 changed files with 1434 additions and 400 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -17,3 +17,4 @@ yarn-debug.log*
|
|||
yarn-error.log*
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
package-lock.json
|
||||
|
|
|
@ -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
630
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||
|
|
|
@ -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
82
src/common/api-browser.js
Normal 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;
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -35,6 +35,7 @@ class TableColumn extends Component {
|
|||
labelKey: null,
|
||||
checkbox: false,
|
||||
checkboxKey: '',
|
||||
checkboxDisabled: null,
|
||||
labelStyle: null,
|
||||
labelModifier: null
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
220
src/ic/ics.js
220
src/ic/ics.js
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
|
@ -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)} />
|
||||
|
|
|
@ -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}>
|
||||
|
|
3
src/styles/swagger-ui.css
Normal file
3
src/styles/swagger-ui.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.swagger-ui div.scheme-container {
|
||||
display: none
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Reference in a new issue