Skip to content
Snippets Groups Projects
Commit 82f18930 authored by Sandro Weber's avatar Sandro Weber
Browse files

introducing experiment workbench

parent 2cb8477a
No related branches found
No related tags found
No related merge requests found
Showing
with 449 additions and 17 deletions
......@@ -4,7 +4,8 @@ import { BrowserRouter, Switch, Route } from 'react-router-dom';
import EntryPage from './components/entry-page/entry-page';
import ErrorDialog from './components/dialog/error-dialog.js';
import ExperimentOverview from './components/experiment-overview/experiment-overview';
import ExperimentsOverview from './components/experiments-overview/experiments-overview';
import ExperimentWorkbench from './components/experiment-workbench/experiment-workbench';
import SimulationView from './components/simulation-view/simulation-view';
import NotificationDialog from './components/dialog/notification-dialog.js';
//import MqttClientService from './services/nrp-core/mqtt-client-service';
......@@ -22,7 +23,8 @@ class App extends React.Component {
<NotificationDialog/>
<BrowserRouter>
<Switch>
<Route path='/experiments-overview' component={ExperimentOverview} />
<Route path='/experiments-overview' component={ExperimentsOverview} />
<Route path='/experiment/:experimentID' component={ExperimentWorkbench} />
<Route path='/simulation-view/:serverIP/:simulationID' component={SimulationView} />
<Route path='/' component={EntryPage} />
</Switch>
......
......@@ -11,7 +11,7 @@ import PublicExperimentsService from '../../services/experiments/files/public-ex
import ExperimentStorageService from '../../services/experiments/files/experiment-storage-service.js';
import SimulationDetails from './simulation-details';
import ExperimentOverview from '../experiment-overview/experiment-overview.js';
import ExperimentOverview from '../experiments-overview/experiments-overview.js';
import './experiment-list-element.css';
import '../main.css';
......
import NrpCoreDashboard from '../nrp-core-dashboard/nrp-core-dashboard';
let _instance = null;
const SINGLETON_ENFORCER = Symbol();
/**
* Service handling server resources for simulating experiments.
*/
class SimulationToolsService {
constructor(enforcer) {
if (enforcer !== SINGLETON_ENFORCER) {
throw new Error('Use ' + this.constructor.name + '.instance');
}
this.tools = new Map();
for (const toolEntry in SimulationToolsService.TOOLS) {
this.registerToolConfig(SimulationToolsService.TOOLS[toolEntry]);
}
this.registerToolConfig(NrpCoreDashboard.CONSTANTS.TOOL_CONFIG);
}
static get instance() {
if (_instance == null) {
_instance = new SimulationToolsService(SINGLETON_ENFORCER);
}
return _instance;
}
registerToolConfig(toolConfig) {
console.info('registerToolConfig');
console.info(toolConfig);
let id = toolConfig.flexlayoutNode.component;
if (this.tools.has(id)) {
console.warn('SimulationToolsService.registerToolConfig() - tool with ID ' + id + ' already exists');
return;
}
this.tools.set(id, toolConfig);
}
flexlayoutNodeFactory(node) {
var component = node.getComponent();
let toolConfig = this.tools.get(component);
if (toolConfig && toolConfig.flexlayoutFactoryCb) {
return toolConfig.flexlayoutFactoryCb();
}
if (component === 'button') {
return <button>{node.getName()}</button>;
}
else if (component === 'nest_wiki') {
return <iframe src='https://en.wikipedia.org/wiki/NEST_(software)' title='nest_wiki'
className='flexlayout-iframe'></iframe>;
}
}
startToolDrag(flexlayoutNode, layoutReference) {
layoutReference.current.addTabWithDragAndDrop(flexlayoutNode.name, flexlayoutNode);
}
}
SimulationToolsService.TOOLS = Object.freeze({
NEST_DESKTOP: {
singleton: true,
flexlayoutNode: {
'type': 'tab',
'name': 'NEST Desktop',
'component': 'nest-desktop'
},
flexlayoutFactoryCb: () => {
return <iframe src='http://localhost:8000' title='NEST Desktop' />;
},
getIcon: () => {
return <div>
<img src={'https://www.nest-simulator.org/wp-content/uploads/2015/03/nest_logo.png'}
alt="NEST Desktop"
style={{width: 40+ 'px', height: 20 + 'px'}} />
<span>Desktop</span>
</div>;
}
},
TEST_NRP_CORE_DOCU: {
singleton: true,
flexlayoutNode: {
'type': 'tab',
'name': 'NRP-Core Docs',
'component': 'nrp-core-docu'
},
flexlayoutFactoryCb: () => {
return <iframe src='https://hbpneurorobotics.bitbucket.io/index.html'
title='NRP-Core Documentation' />;
},
getIcon: () => {
return <span>NRP-Core Docs</span>;
}
}
});
SimulationToolsService.CONSTANTS = Object.freeze({
CATEGORY: {
EXTERNAL_IFRAME: 'EXTERNAL_IFRAME',
REACT_COMPONENT: 'REACT_COMPONENT'
}
});
export default SimulationToolsService;
.simulation-view-wrapper {
display: grid;
grid-template-rows: auto auto;
grid-template-columns: 68px auto;
grid-template-areas:
"simulation-view-header simulation-view-header"
"simulation-view-sidebar simulation-view-mainview";
}
.simulation-view-header {
grid-area: simulation-view-header;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: yellow;
padding: 3px;
}
.simulation-view-sidebar {
padding: 2px;
grid-area: simulation-view-sidebar;
background-color: green;
}
.simulation-view-mainview {
grid-area: simulation-view-mainview;
background-color: red;
}
.flexlayout__layout {
position: relative;
height: calc(100vh - 63px);
}
.flexlayout__tab {
overflow: hidden;
}
.simulation-view-controls {
display: flex;
flex-direction: row;
align-items: center;
}
.simulation-view-control-buttons {
padding: 0px 10px 0px 10px;
}
.simulation-view-time-info {
display: grid;
gap: 3px;
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
padding: 0px 10px 0px 10px;
font-size: 0.7em;
}
.simulation-view-experiment-title {
padding: 0px 10px 0px 10px;
font-weight: bold;
}
.simulation-tool-button {
width: 60px;
height: 60px;
margin: 2px;
padding: 2px;
font-size: 0.7em;
color: black;
font-weight: bold;
background-color: lightgray;
}
iframe {
width: 100%;
height: 100%;
}
\ No newline at end of file
import React from 'react';
import FlexLayout from 'flexlayout-react';
import { OverlayTrigger, Tooltip, Button } from 'react-bootstrap';
import { RiPlayFill, RiPauseFill, RiLayout6Line } from 'react-icons/ri';
import { GiExitDoor } from 'react-icons/gi';
import { TiMediaRecord } from 'react-icons/ti';
import { VscDebugRestart } from 'react-icons/vsc';
import ExperimentToolsService from './experiment-tools-service';
import ServerResourcesService from '../../services/experiments/execution/server-resources-service.js';
import ExperimentStorageService from '../../services/experiments/files/experiment-storage-service';
import RunningSimulationService from '../../services/experiments/execution/running-simulation-service';
import { EXPERIMENT_STATE } from '../../services/experiments/experiment-constants';
import timeDDHHMMSS from '../../utility/time-filter';
import LeaveWorkbenchDialog from './leave-workbench-dialog';
import '../../../node_modules/flexlayout-react/style/light.css';
import './experiment-workbench.css';
const jsonBaseLayout = {
global: {},
borders: [],
layout:{
'type': 'row',
'weight': 100,
'children': [
{
'type': 'tabset',
'weight': 50,
'selected': 0,
'children': [
{
'type': 'tab',
'name': 'NEST wiki page',
'component':'nest_wiki'
}
]
}
]
}
};
export default class ExperimentWorkbench extends React.Component {
constructor(props) {
super(props);
const {experimentID} = props.match.params;
//console.info('SimulationView ' + serverIP + ' ' + simulationID);
this.experimentID = experimentID;
this.serverURL = 'http://' + this.serverIP + ':8080'; // this should probably be part of some config
this.state = {
modelFlexLayout: FlexLayout.Model.fromJson(jsonBaseLayout),
showLeaveDialog: false
};
this.refLayout = React.createRef();
}
async componentDidMount() {
await this.updateSimulationInfo();
let experiments = await ExperimentStorageService.instance.getExperiments();
this.experimentInfo = experiments.find(experiment => experiment.id === this.experimentID);
console.info('ExperimentWorkbench - experimentInfo');
console.info(this.experimentInfo);
let experimentName = this.experimentInfo.configuration.name;
this.setState({experimentName: experimentName});
let server = this.experimentInfo.joinableServers.find(
server => server.runningSimulation.creationUniqueID === this.state.simulationInfo.creationUniqueID);
this.serverConfig = await ServerResourcesService.instance.getServerConfig(server.server);
console.info('this.serverConfig');
console.info(this.serverConfig);
RunningSimulationService.instance.addRosStatusInfoCallback(
this.serverConfig.rosbridge.websocket,
(data) => {
this.onStatusInfoROS(data);
}
);
}
async updateSimulationInfo() {
let simInfo = await RunningSimulationService.instance.getInfo(this.serverURL, this.simulationID);
this.setState({simulationInfo: simInfo});
console.info('SimulationView.updateSimulationInfo - simulationInfo');
console.info(this.state.simulationInfo);
}
onStatusInfoROS(message) {
this.setState({
timingRealtime: timeDDHHMMSS(message.realTime),
timingSimulationTime: timeDDHHMMSS(message.simulationTime),
timingTimeout: timeDDHHMMSS(message.timeout)
});
}
async onButtonStartPause() {
let newState = this.state.simulationInfo.state === EXPERIMENT_STATE.PAUSED
? EXPERIMENT_STATE.STARTED
: EXPERIMENT_STATE.PAUSED;
await RunningSimulationService.instance.updateState(this.serverURL, this.simulationID, newState);
this.updateSimulationInfo();
}
onButtonLayout() {
console.info(this.state.modelFlexLayout.toJson());
}
showLeaveDialog(show) {
this.setState({showLeaveDialog: show});
}
leaveWorkbench() {
this.props.history.push({
pathname: '/experiments-overview'
});
}
render() {
return (
<div>
<LeaveWorkbenchDialog visible={this.state.showLeaveDialog}
setVisibility={(visible) => this.showLeaveDialog(visible)}
stopSimulation={async () => {
await RunningSimulationService.instance.updateState(this.serverURL, this.simulationID,
EXPERIMENT_STATE.STOPPED);
this.leaveWorkbench();
}}
leaveWorkbench={() => {
this.leaveWorkbench();
}} />
<div className='simulation-view-wrapper'>
<div className='simulation-view-header'>
<div className='simulation-view-controls'>
<div className='simulation-view-control-buttons'>
<button className='nrp-btn btn-default' onClick={() => this.showLeaveDialog(true)}>
<GiExitDoor className='icon' />
</button>
<button disabled={true} className='nrp-btn btn-default'><VscDebugRestart className='icon' /></button>
<button className='nrp-btn btn-default' onClick={() => {
this.onButtonStartPause();
}}>
{this.state.simulationInfo && this.state.simulationInfo.state === EXPERIMENT_STATE.PAUSED
? <RiPlayFill className='icon' />
: <RiPauseFill className='icon' />}
</button>
<button disabled={true} className='nrp-btn btn-default'><TiMediaRecord className='icon' /></button>
</div>
<div className='simulation-view-time-info'>
<div>Simulation time:</div>
<div>{this.state.timingSimulationTime}</div>
<div>Real time:</div>
<div>{this.state.timingRealtime}</div>
<div>Real timeout:</div>
<div>{this.state.timingTimeout}</div>
</div>
</div>
<div className='simulation-view-experiment-title'>{this.state.experimentName}</div>
<button className='nrp-btn btn-default' onClick={() => {
this.onButtonLayout();
}}><RiLayout6Line className='icon' /></button>
</div>
<div className='simulation-view-sidebar'>
{Array.from(ExperimentToolsService.instance.tools.values()).map(tool => {
return (
<OverlayTrigger
key={`overlaytrigger-${tool.flexlayoutNode.component}`}
placement={'right'}
overlay={
<Tooltip id={`tooltip-${tool.flexlayoutNode.component}`}>
{tool.flexlayoutNode.name}
</Tooltip>
}
>
<Button key={tool.flexlayoutNode.component}
className="simulation-tool-button"
onMouseDown={() => {
ExperimentToolsService.instance.startToolDrag(
tool.flexlayoutNode,
this.refLayout);
}}>{tool.getIcon && tool.getIcon()}</Button>
</OverlayTrigger>
);
})}
</div>
<div className='simulation-view-mainview'>
<FlexLayout.Layout ref={this.refLayout} model={this.state.modelFlexLayout}
factory={(node) => {
return ExperimentToolsService.instance.flexlayoutNodeFactory(node);
}} />
</div>
</div>
</div>
);
}
}
ExperimentWorkbench.CONSTANTS = Object.freeze({
INTERVAL_INTERNAL_UPDATE_MS: 1000
});
.leave-simulation-dialog-header {
color: black;
background-color: white;
}
\ No newline at end of file
import React from 'react';
import {Button } from 'react-bootstrap';
import Modal from 'react-bootstrap/Modal';
import './leave-workbench-dialog.css';
export default class LeaveWorkbenchDialog extends React.Component{
render(){
return (
<div>
<div>
<Modal show={this.props.visible} onHide={() => this.props.setVisibility(false)}>
<Modal.Header closeButton className="leave-workbench-dialog-header">
<Modal.Title>Exit menu</Modal.Title>
</Modal.Header>
<Modal.Body>Would you like to leave or stop the simulation?</Modal.Body>
<Modal.Footer>
<div>
<Button variant="light" onClick={() => this.props.leaveWorkbench()}>
Leave
</Button>
<Button variant="danger" onClick={() => this.props.stopSimulation()}>
Stop
</Button>
</div>
</Modal.Footer>
</Modal>
</div>
</div>
);
}
}
\ No newline at end of file
......@@ -13,9 +13,9 @@ import ExperimentList from '../experiment-list/experiment-list.js';
import NrpHeader from '../nrp-header/nrp-header.js';
import ExperimentFilesViewer from '../experiment-files-viewer/experiment-files-viewer.js';
import './experiment-overview.css';
import './experiments-overview.css';
export default class ExperimentOverview extends React.Component {
export default class ExperimentsOverview extends React.Component {
static CONSTANTS = {
TAB_INDEX: {
MY_EXPERIMENTS: 0,
......@@ -35,7 +35,7 @@ export default class ExperimentOverview extends React.Component {
joinableExperiments: [],
availableServers: [],
startingExperiment: undefined,
selectedTabIndex: ExperimentOverview.CONSTANTS.TAB_INDEX.MY_EXPERIMENTS
selectedTabIndex: ExperimentsOverview.CONSTANTS.TAB_INDEX.MY_EXPERIMENTS
};
}
......
......@@ -7,22 +7,22 @@ const SINGLETON_ENFORCER = Symbol();
/**
* Service handling server resources for simulating experiments.
*/
class SimulationToolsService {
class ExperimentToolsService {
constructor(enforcer) {
if (enforcer !== SINGLETON_ENFORCER) {
throw new Error('Use ' + this.constructor.name + '.instance');
}
this.tools = new Map();
for (const toolEntry in SimulationToolsService.TOOLS) {
this.registerToolConfig(SimulationToolsService.TOOLS[toolEntry]);
for (const toolEntry in ExperimentToolsService.TOOLS) {
this.registerToolConfig(ExperimentToolsService.TOOLS[toolEntry]);
}
this.registerToolConfig(NrpCoreDashboard.CONSTANTS.TOOL_CONFIG);
}
static get instance() {
if (_instance == null) {
_instance = new SimulationToolsService(SINGLETON_ENFORCER);
_instance = new ExperimentToolsService(SINGLETON_ENFORCER);
}
return _instance;
......@@ -62,7 +62,7 @@ class SimulationToolsService {
}
}
SimulationToolsService.TOOLS = Object.freeze({
ExperimentToolsService.TOOLS = Object.freeze({
NEST_DESKTOP: {
singleton: true,
flexlayoutNode: {
......@@ -99,11 +99,11 @@ SimulationToolsService.TOOLS = Object.freeze({
}
});
SimulationToolsService.CONSTANTS = Object.freeze({
ExperimentToolsService.CONSTANTS = Object.freeze({
CATEGORY: {
EXTERNAL_IFRAME: 'EXTERNAL_IFRAME',
REACT_COMPONENT: 'REACT_COMPONENT'
}
});
export default SimulationToolsService;
export default ExperimentToolsService;
......@@ -6,7 +6,7 @@ import { GiExitDoor } from 'react-icons/gi';
import { TiMediaRecord } from 'react-icons/ti';
import { VscDebugRestart } from 'react-icons/vsc';
import SimulationToolsService from './simulation-tools-service';
import ExperimentToolsService from './simulation-tools-service';
import ServerResourcesService from '../../services/experiments/execution/server-resources-service.js';
import ExperimentStorageService from '../../services/experiments/files/experiment-storage-service';
import RunningSimulationService from '../../services/experiments/execution/running-simulation-service';
......@@ -167,7 +167,7 @@ export default class SimulationView extends React.Component {
}}><RiLayout6Line className='icon' /></button>
</div>
<div className='simulation-view-sidebar'>
{Array.from(SimulationToolsService.instance.tools.values()).map(tool => {
{Array.from(ExperimentToolsService.instance.tools.values()).map(tool => {
return (
<OverlayTrigger
key={`overlaytrigger-${tool.flexlayoutNode.component}`}
......@@ -181,7 +181,7 @@ export default class SimulationView extends React.Component {
<Button key={tool.flexlayoutNode.component}
className="simulation-tool-button"
onMouseDown={() => {
SimulationToolsService.instance.startToolDrag(
ExperimentToolsService.instance.startToolDrag(
tool.flexlayoutNode,
this.refLayout);
}}>{tool.getIcon && tool.getIcon()}</Button>
......@@ -192,7 +192,7 @@ export default class SimulationView extends React.Component {
<div className='simulation-view-mainview'>
<FlexLayout.Layout ref={this.refLayout} model={this.state.modelFlexLayout}
factory={(node) => {
return SimulationToolsService.instance.flexlayoutNodeFactory(node);
return ExperimentToolsService.instance.flexlayoutNodeFactory(node);
}} />
</div>
</div>
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment