Commit 5f1541a7 authored by Dorian Goepp's avatar Dorian Goepp
Browse files

Merge branch 'master' into dgoepp/config-modules

parents 5dcae08f 0215139b
This diff is collapsed.
...@@ -21,7 +21,10 @@ ...@@ -21,7 +21,10 @@
"react-numeric-input": "^2.2.3", "react-numeric-input": "^2.2.3",
"react-scripts": "^3.0.1", "react-scripts": "^3.0.1",
"react-sizeme": "^2.5.2", "react-sizeme": "^2.5.2",
"roslib": "^1.0.1" "roslib": "^1.0.1",
"ts-pnp": "^1.1.2",
"typescript": "^3.5.1",
"visjs-network": "^4.24.7"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
......
import React from "react"; import React from 'react';
// Tab-based dynamic layout manager
import FlexLayout from 'flexlayout-react';
// MobX, a state manager
import {toJS} from 'mobx' import {toJS} from 'mobx'
import {observer} from 'mobx-react' import {observer} from 'mobx-react'
// Tab-based dynamic layout manager
import FlexLayout from "flexlayout-react";
// The React Components that are going into tabs // The React Components that are going into tabs
import {VideoStream, DemoVideoStream} from "./VideoStream" import {VideoStream, DemoVideoStream} from './VideoStream'
import { RosLineChart, DemoLineChart } from "./LineChart" import {RosLineChart, DemoLineChart} from './LineChart'
import { RosHGauge } from "./HorizontalGauge" import {RosHGauge} from './HorizontalGauge'
import { RosMemoryRanked, DemoMemoryRanked } from './MemoryRanked' import {RosMemoryRanked, DemoMemoryRanked} from './MemoryRanked'
import { RosInteractionTrace, DemoInteractionTrace } from './InteractionTrace' import {RosInteractionTrace, DemoInteractionTrace} from './InteractionTrace'
import { RosLatestInteraction, DemoLatestInteraction } from './LatestInteraction' import {RosLatestInteraction, DemoLatestInteraction} from './LatestInteraction'
import { RosMood, DemoMood } from './Mood' import {RosMood, DemoMood} from './Mood'
import {Graph, DemoGraph} from './GraphVisJs'
import DemoString from './String' import DemoString from './String'
// get the default configuration for the layout // get the default configuration for the layout
...@@ -47,6 +49,8 @@ class Board extends React.Component { ...@@ -47,6 +49,8 @@ class Board extends React.Component {
"latest-interaction": RosLatestInteraction, "latest-interaction": RosLatestInteraction,
"mood": RosMood, "mood": RosMood,
"demo-mood": DemoMood, "demo-mood": DemoMood,
"graph": Graph,
"demo-graph": DemoGraph,
} }
/** This function gives the React Component correspinding to a tab type. /** This function gives the React Component correspinding to a tab type.
......
import React from 'react';
import 'visjs-network/dist/vis-network.min.css'
import vis from 'visjs-network'
export class Graph extends React.Component {
constructor(props){
super(props);
this.ref = React.createRef();
}
/**
* Draw a graph which representation in the Dot format (from Graphviz) is provided as the 'graph' property.
*/
draw() {
const parseData = vis.network.convertDot(this.props.graph);
var data = {
nodes: fixLabels(parseData.nodes),
edges: fixLabels(parseData.edges)
};
var options = parseData.options;
options.nodes = { font: { color : "white"}};
var network = new vis.Network(this.ref.current, data, options);
}
componentDidMount() {
if ('graph' in this.props && this.props.graph) {
this.draw();
}
}
componentDidUpdate() {
if ('graph' in this.props && this.props.graph) {
this.draw();
}
}
render() {
return <div ref={this.ref} style={{minHeight:"200px", height:"100%"}}/>;
}
}
export function DemoGraph(props) {
const machine = `dinetwork {1 -> 1 -> 2; 2 -> 3; 2 -- 4; 2 -> 1 }`;
return <Graph graph={machine}/>;
}
/**
* Fix the labels of nodes or edges, extracted from a Dot (graphviz format) file.
* The Dot parser provided by vis.js stores numeric labels as numbers and the network object of the same library
* expects labels to be strings. This function does the conversion from any type other than string to a string.
* @param {Array(Object)} entities entities (nodes or edges) of a graph
* @return entities which label keys (when applicable) are convertet to string.
*/
function fixLabels(entities) {
entities = entities.map((entity) => {
if ('label' in entity && typeof entity.label != "string") {
entity.label = String(entity.label);
}
return entity;
});
return entities;
}
import React, {Component} from 'react'
import RosLib from 'roslib'
import {Graph} from './Graph'
/**
* React Widget that displays a graph described at ROS topic. This topic is expected to publish a string in the Dot
* format (from Graphviz).
*
* To test this widget, in a terminal with ROS in scope, you can type
* ```
* rostopic pub /graph_dot std_msgs/String -l -r 0.5 'digraph "machine_0" {
* 0 [color=blue, pos="94.8148148148,0.0!", label="1, S0"];
* 1 [color=black, pos="-47.4074074074,46.1880215352!", label="0, S1"];
* 2 [color=black, pos="-47.4074074074,-46.1880215352!", label="0, S2"];
* 0 -> 2 [key=0, label="a0, 1"];
* 1 -> 0 [key=0, label=a3];
* 2 -> 0 [key=0, label=a9];
* 2 -> 1 [key=0, label=a8];
* 2 -> 2 [key=0, label="a2, 3, 7"];
* }'
* ```
*/
export class RosGraph extends Component {
constructor(props) {
super(props);
this.state = {graph: null};
}
static get modes() {
return {
readme: <p className="about">This widget displays graphs from a ROS topic that sends Dot-formatted graph
descriptions.</p>,
settingsSchema: {
title: "Graph",
type: "object",
required: ["topic"],
properties: {
topic: {
type: "string",
title: "ROS topic (absolute name)",
default: "/graph_dot"}
}
},
};
}
componentDidMount() {
if ('ros' in this.props && this.props.ros) {
const config = this.props.node.getConfig();
this.listener = new RosLib.Topic({
ros : this.props.ros,
name: config.topic,
});
this.listener.subscribe(function(message) {
this.setState({graph: message.data});
}.bind(this));
}else {
console.warn('RosGraph expects to be passed a valid Ros object as property, got ', this.props.ros);
}
}
componentWillUnmount() {
if ('listener' in this) {
this.listener.unsubscribe();
}
}
render() {
return <Graph
graph={this.state.graph}
{...this.props}/>;
}
}
export {DemoGraph} from './Graph'
export {RosGraph as Graph} from './RosGraph'
...@@ -3,20 +3,6 @@ ...@@ -3,20 +3,6 @@
height: 110px; height: 110px;
} }
/* Name of this piece of software */
#header .title {
width: 10vw;
font-size: 35px;
font-weight: bold;
letter-spacing: 0.3px;
text-align: left;
color: #edc61b;
line-height: 100px;
float: left;
margin-left: 1vw;
}
/* Name of the algorithm being run */ /* Name of the algorithm being run */
#header .algo-name { #header .algo-name {
...@@ -27,7 +13,7 @@ ...@@ -27,7 +13,7 @@
text-align: center; text-align: center;
letter-spacing: 0.3px; letter-spacing: 0.3px;
padding-top: 25px; padding-top: 25px;
padding-right: 8vw; padding-right: 10vw;
float:left; float:left;
} }
...@@ -55,9 +41,9 @@ ...@@ -55,9 +41,9 @@
/* Buttons to control the algorithm's lifecycle : play/pause, restart, next step, etc. */ /* Buttons to control the algorithm's lifecycle : play/pause, restart, next step, etc. */
#header .controls { #header .controls {
padding-right:7vw; padding-right:9vw;
padding-left:4vw; padding-left:12vw;
padding-top: 20px; padding-top: 24px;
float: left; float: left;
filter: invert(26%) sepia(0%) saturate(0%) hue-rotate(98deg) brightness(99%) contrast(91%); filter: invert(26%) sepia(0%) saturate(0%) hue-rotate(98deg) brightness(99%) contrast(91%);
} }
......
...@@ -17,9 +17,6 @@ export function Header(props) { ...@@ -17,9 +17,6 @@ export function Header(props) {
}) })
return ( return (
<div id="header"> <div id="header">
<div className="title">
Lavizu
</div>
<div className="controls"> <div className="controls">
{buttons} {buttons}
</div> </div>
......
/*! react-sidenav v0.4.5 | (c) 2018 Trend Micro Inc. | MIT | https://github.com/trendmicro-frontend/react-sidenav */ /*! react-sidenav v0.4.5 | (c) 2018 Trend Micro Inc. | MIT | https://github.com/trendmicro-frontend/react-sidenav */
/* Name of this piece of software */
.title {
display:none;
}
/* Menu level 1 title */ /* Menu level 1 title */
.menu-title-level1 { .menu-title-level1 {
width: 100%; width: 100%;
...@@ -13,9 +19,9 @@ ...@@ -13,9 +19,9 @@
letter-spacing: 0.2px; letter-spacing: 0.2px;
color: #484848; color: #484848;
background-color:#d8d8d8; background-color:#d8d8d8;
padding-left: 20px; padding-left: 25px;
margin: 1.2vh 0px 0.5vh 0px; margin: 1.2vh 0px 0.5vh 0px;
border-radius: 4px; border-radius: 2px;
} }
/* Menu level 2 title */ /* Menu level 2 title */
...@@ -30,14 +36,14 @@ ...@@ -30,14 +36,14 @@
line-height: normal; line-height: normal;
letter-spacing: 0.2px; letter-spacing: 0.2px;
color: #484848; color: #484848;
padding-left: 20px; padding-left: 25px;
margin: 1.5vh 0px 0.5vh 0px; margin: 1.5vh 10px 0.5vh 0px;
} }
/*Menu icons*/ /*Menu icons*/
.menu-button { .menu-button {
height: 25px; height: 25px;
margin: 0.9vh 0px 0px 7px; margin: 1.2vh 0px 0px 5px;
filter: invert(14%) sepia(1%) saturate(2442%) hue-rotate(316deg) brightness(98%) contrast(77%); filter: invert(14%) sepia(1%) saturate(2442%) hue-rotate(316deg) brightness(98%) contrast(77%);
} }
...@@ -51,8 +57,9 @@ ...@@ -51,8 +57,9 @@
/* Behaviors logo */ /* Behaviors logo */
.logo { .logo {
margin: 5px 5px 1vh 5px; margin: 20px 0px 1vh 20px;
height: 100px; height: 80px;
display: inline-block;
} }
/* Footer with About us and lock button */ /* Footer with About us and lock button */
...@@ -64,6 +71,7 @@ ...@@ -64,6 +71,7 @@
.footer:hover { .footer:hover {
filter: invert(67%) sepia(71%) saturate(551%) hue-rotate(1deg) brightness(111%) contrast(86%); filter: invert(67%) sepia(71%) saturate(551%) hue-rotate(1deg) brightness(111%) contrast(86%);
cursor: pointer;
} }
.lock-button { .lock-button {
...@@ -74,6 +82,22 @@ ...@@ -74,6 +82,22 @@
margin-left:30px; margin-left:30px;
} }
.sidenav---sidenav---_2tBP.sidenav---expanded---1KdUL .title {
display: inline-block;
float:right;
width: 100px;
height:80px;
font-size: 35px;
font-weight: bold;
letter-spacing: 0.3px;
text-align: left;
color: #edc61b;
line-height: 80px;
margin-right: 22px;
margin-top:18px;
}
/* Side navigation menu */ /* Side navigation menu */
.sidenav---sidenav---_2tBP { .sidenav---sidenav---_2tBP {
z-index: 1006; z-index: 1006;
...@@ -95,7 +119,7 @@ ...@@ -95,7 +119,7 @@
display: block; display: block;
} }
.sidenav---sidenav---_2tBP.sidenav---expanded---1KdUL { .sidenav---sidenav---_2tBP.sidenav---expanded---1KdUL {
min-width: 220px; min-width: 240px;
} }
.sidenav---sidenav---_2tBP.sidenav---expanded---1KdUL .sidenav---sidenav-nav---3tvij > .sidenav---sidenav-navitem---uwIJ- .sidenav---navicon---3gCRo + .sidenav---navtext---1AE_f { .sidenav---sidenav---_2tBP.sidenav---expanded---1KdUL .sidenav---sidenav-nav---3tvij > .sidenav---sidenav-navitem---uwIJ- .sidenav---navicon---3gCRo + .sidenav---navtext---1AE_f {
display: inline-block; display: inline-block;
......
...@@ -13,9 +13,12 @@ export function Menu(props) { ...@@ -13,9 +13,12 @@ export function Menu(props) {
}} }}
> >
<img className="logo" <img className="logo"
src={window.location.origin + "/images/logoBehaviors.png"} src={window.location.origin + "/images/logoBehaviors2.png"}
alt="logo of the Behaviors.ai project" alt="logo of the Behaviors.ai project"
/> />
<div className="title">
Lavizu
</div>
<br></br> <br></br>
<SideNav.Toggle /> <SideNav.Toggle />
......
...@@ -30,7 +30,8 @@ decorate(MoodStore, { ...@@ -30,7 +30,8 @@ decorate(MoodStore, {
config: observable, config: observable,
}) })
const RosMood = sizeMe({monitorHeight: true})(observer( const RosMood = sizeMe({monitorHeight: true})(
observer(
class RosMood extends Component { class RosMood extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
......
import React, {Component} from 'react' import React, {Component} from 'react'
import {decorate, observable, computed} from 'mobx' import {reaction, decorate, observable, computed} from 'mobx'
import {observer} from 'mobx-react' import {observer} from 'mobx-react'
import {defaultConfig} from '../utils/configurationHelpers' import {defaultConfig} from '../utils/configurationHelpers'
...@@ -41,6 +41,11 @@ class VideoStream extends Component { ...@@ -41,6 +41,11 @@ class VideoStream extends Component {
super(props); super(props);
this.store = new VideoStore(); this.store = new VideoStore();
// Automatically detect if the configured topic name is valid
this.hasTopic = observable.box(false);
this.searchForTopic()
// Fill the undefined fields of the configuration object with their default values. // Fill the undefined fields of the configuration object with their default values.
defaultConfig(this.store); defaultConfig(this.store);
this.props.store.components['video'] = this.store; this.props.store.components['video'] = this.store;
...@@ -57,7 +62,7 @@ class VideoStream extends Component { ...@@ -57,7 +62,7 @@ class VideoStream extends Component {
get streamUrl() { get streamUrl() {
if ('url' in this.props && this.props.url) { if ('url' in this.props && this.props.url) {
return this.props.url; return this.props.url;
} else if ('topic' in this.store.config && 'host' in this.store.config) { } else if (this.hasTopic.get()) {
// all requierd configuration fields are defined // all requierd configuration fields are defined
return 'http://' + this.store.config.host + '/stream?topic=' return 'http://' + this.store.config.host + '/stream?topic='
+ this.store.config.topic; + this.store.config.topic;
...@@ -66,6 +71,42 @@ class VideoStream extends Component { ...@@ -66,6 +71,42 @@ class VideoStream extends Component {
} }
} }
/**
* Check that all the requirements are met for subscribing to the required
* topic
*
* Thanks to MobX magic, the result is computed again each time the topic
* configuration is changed.
*/
searchForTopic() {
if (this.props.ros) {
this.topicWatcher = reaction(() => (
this.store.config.topic + this.store.config.host
),
(data, reaction) => {
if ('topic' in this.store.config && 'host' in this.store.config) {
this.props.ros.getTopics(infos => {
this.hasTopic.set(undefined !== infos.topics.find(
elem => elem === this.store.config.topic));
if (!this.hasTopic.get()) {
console.warn("[VideoStream] The required topic "
+ this.store.config.topic + " is not published");
}
console.debug(this.hasTopic.get());
})
} else {
console.warn("[VideoStream] either no topic is configured "
+ "or no host is configured. This should never "
+ "happen at runtime. Contact the developpers.");
}
})
} else {
console.warn("[VideoStream] ROS property is provided. This should "
+ "never happen at runtime. Contact the developpers.");
}
}
render() { render() {
return ( return (
<div className="video-stream container"> <div className="video-stream container">
......
...@@ -5,8 +5,8 @@ export const flexlayout_json = { ...@@ -5,8 +5,8 @@ export const flexlayout_json = {
tabSetHeaderHeight: 25, tabSetHeaderHeight: 25,
enableEdgeDock: false, enableEdgeDock: false,
tabEnableRename: false, tabEnableRename: false,
tabSetEnableHeader: false, // tabSetEnableHeader: false,
tabSetEnableTabStrip: false, // tabSetEnableTabStrip: false,
}, },
layout: { layout: {
"type": "row", "type": "row",
...@@ -14,14 +14,23 @@ export const flexlayout_json = { ...@@ -14,14 +14,23 @@ export const flexlayout_json = {
"children": [ "children": [
{ {
"type": "tabset", "type": "tabset",
"name": "Interaction trace",
"weight": 50, "weight": 50,
"selected": 0, "selected": 0,
"children": [ "children": [
{ {
"name": "Interaction trace",
"type": "tab", "type": "tab",
"component": "int-trace", "component": "int-trace",
}, },
{
"name": "The magic graph",
"type": "tab",
"component": "graph",
"config": {
"hasReadme": true,
"topic": "/graph_dot",
}
},
] ]
}, },
{ {
...@@ -30,11 +39,11 @@ export const flexlayout_json = { ...@@ -30,11 +39,11 @@ export const flexlayout_json = {
"children": [ "children": [
{ {
"type": "tabset", "type": "tabset",
"name": "Robot's memory",
"weight": 60, "weight": 60,
"selected": 0, "selected": 0,
"children": [ "children": [
{ {
"name": "Robot's memory",
"type": "tab", "type": "tab",
"component": "memory", "component": "memory",
}, },
...@@ -50,11 +59,11 @@ export const flexlayout_json = { ...@@ -50,11 +59,11 @@ export const flexlayout_json = {
"children": [ "children": [
{ {
"type": "tabset", "type": "tabset",
"name": "Latest interaction",
"weight": 78, "weight": 78,
"selected": 0, "