Commit 8ec79a54 authored by Dorian Goepp's avatar Dorian Goepp

Merge branch 'dgoepp/config-modules' into 'master'

Make all gadgets configurable

See merge request behaviors-ai/the_dashboard!2
parents 0215139b 5f1541a7
......@@ -2455,6 +2455,11 @@
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
},
"bootstrap": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz",
"integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
......@@ -8269,6 +8274,11 @@
"lodash._reinterpolate": "~3.0.0"
}
},
"lodash.topath": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
"integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak="
},
"lodash.unescape": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz",
......@@ -10603,6 +10613,26 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
},
"react-jsonschema-form": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/react-jsonschema-form/-/react-jsonschema-form-1.6.1.tgz",
"integrity": "sha512-rDZjAMzI9GrG5EpBbqmhnch3jgFd9YN9U2bk8zc0PBldgGiENjo+ziIh4vseDNijPNU+07wclD5anIN23nmYxw==",
"requires": {
"ajv": "^6.7.0",
"babel-runtime": "^6.26.0",
"core-js": "^2.5.7",
"lodash.topath": "^4.5.2",
"prop-types": "^15.5.8",
"react-is": "^16.8.4"
},
"dependencies": {
"core-js": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
}
}
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
......
<svg version="1.1" viewBox="0.0 0.0 944.8818897637796 718.1102362204724" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><clipPath id="p.0"><path d="m0 0l944.8819 0l0 718.1102l-944.8819 0l0 -718.1102z" clip-rule="nonzero"/></clipPath><g clip-path="url(#p.0)"><path fill="#000000" fill-opacity="0.0" d="m0 0l944.8819 0l0 718.1102l-944.8819 0z" fill-rule="evenodd"/><path fill="#efefef" d="m0 0l944.8819 0l0 718.1102l-944.8819 0z" fill-rule="evenodd"/><path fill="#cccccc" d="m290.1798 354.03406l0 0c0 -92.37506 71.41586 -167.25983 159.51181 -167.25983l0 0c42.305176 0 82.877594 17.621964 112.7919 48.989273c29.914246 31.367294 46.71991 73.91051 46.71991 118.27055l0 0c0 92.37506 -71.41589 167.25983 -159.51181 167.25983l0 0c-88.09595 0 -159.51181 -74.884766 -159.51181 -167.25983z" fill-rule="evenodd"/><path fill="#d9d9d9" d="m180.32224 75.26723l269.38583 0l0 278.77167l-269.38583 0z" fill-rule="evenodd"/><path fill="#999999" d="m616.34216 513.66l123.93677 -99.38309l123.93677 99.38309l-47.33966 160.80524l-153.19421 0z" fill-rule="evenodd"/></g></svg>
\ No newline at end of file
......@@ -77,7 +77,22 @@ html,body
border:1px solid #ddd;
}
#overlay {
background: rgb(156, 156, 151);
height: 100%;
width: 100%;
opacity: .9;
top: 0;
left: 0;
position: absolute;
padding: 0;
transition: opacity .5s;
}
#overlay:hover {
opacity: .9;
transition: opacity .5s;
}
.flexlayout__layout {
left: 0;
......
import React from 'react';
// Tab-based dynamic layout manager
import FlexLayout, {Actions} from 'flexlayout-react';
import FlexLayout from 'flexlayout-react';
// MobX, a state manager
import {decorate, observable} from 'mobx'
import {toJS} from 'mobx'
import {observer} from 'mobx-react'
// The React Components that are going into tabs
import {VideoStream, DemoVideoStream} from './VideoStream'
import {RosLineChart, DemoLineChart} from './LineChart'
......@@ -16,25 +17,16 @@ import DemoString from './String'
// get the default configuration for the layout
import {flexlayout_json as json} from './demo-1-config'
// import {flexlayout_json as json} from './demo-tabbed-config'
// import {flexlayout_json as json} from './devel-config'
// CSS common to the whole app
import './Common.css'
class ObservableStore {
components = {}
}
decorate(ObservableStore, {
components: observable
})
const Board = observer(
class Board extends React.Component {
constructor(props) {
super(props)
this.state = {model: FlexLayout.Model.fromJson(json)}
this.refLayout = React.createRef();
this.componentStore = new ObservableStore();
}
/** Instanciates the right React Component to be added in a tab
......@@ -73,21 +65,15 @@ class Board extends React.Component {
if (component in tab_types){
return tab_types[component];
} else {
return <div>The component type {component} is not declared.
The mistake is either in the configuration (type of a tab) or
in the source file <pre>index.js</pre> (see <pre>tab_types</pre>).
</div>;
return invalidComponent(component);
}
}
var component = node.getComponent();
return React.createElement(getComponentType(component), {
node: node,
updateConfig: this.updateConfig.bind(this, node.getId()),
ros: this.props.ros,
store: this.componentStore
store: this.props.store
}, null);
}
......@@ -99,22 +85,18 @@ class Board extends React.Component {
}, null);
}
updateConfig(nodeId, config) {
this.state.model.doAction(Actions.updateNodeAttributes(nodeId, {
config: config
}));
console.log("updateConfig called with the following configuration:")
console.log(config)
}
/**
* Add a settings button to the tabsets whose currently activated tab is configurable.
* Add settings and readme buttons to the tabsets whose currently activated
* tab is configurable and has a readme.
*
* One tab is declared as configurable through its 'configurable' config setting in the model.
* The button will toggle the value of the field 'settingsMode' in the configuration of the currently
* activated tab in the tabsed.
* One tab is configurable when it has a 'configSchema' (JSON Schema
* description of its configuration fields) in its MobX store (accessed at
* this.props.store.components['ComponentNameHere']). The readme capability
* is detected with the presence of a 'readme' field, which is a JSX object
* to be displayed as README.
*
* have a look at `App.tsx` in the source code of FlexLayout to understand when this function is called.
* have a look at `App.tsx` in the source code of FlexLayout to understand
* when this function is called.
*
* @param {TabSetNode|BorderNode} node
* @param {any} renderValues
......@@ -124,70 +106,89 @@ class Board extends React.Component {
// The node could be either a tab set or a border
if (node.getType() === "tabset") {
/**
* Switch in and out of a display mode (for a widget in the dashboard). This function returns a fuction
* that does the real switching for a given mode.
*
* This mode is handled through a configuration object handled by FlexLayout.
* @param {String} modeName name of the mode to be switched
* Switch in and out of a modal. This function returns a fuction
* that does the real switching for a given modal.
*
* There currently, 2019/07/02, are two modals : readme and settings.
* The former is showing information about a gadget and the latter
* displays a form to set the configuration of the related gadget.
* @param {String} modeName name of the mode to be switched ("readme"
* or "settings")
* @return function
*/
function modeToggle(modeName) {
const selectedTab = node.getSelectedNode()
return function() {
// `this` is bound to the button's tabset
const tab = this.getSelectedNode(); // retrieve the selected tab
const config = tab.getConfig(); // get the configuration of this tab
if (!("displayMode" in config) || (config.displayMode !== modeName)) {
config.displayMode = modeName;
this._model.doAction(
FlexLayout.Actions.updateNodeAttributes(tab.getId(), {config: config}));
// If no modal is active, display the relevant one;
// otherwise, get out of it.
if (!this.props.store.modal.enabled) {
this.props.store.displayModal(modeName, selectedTab.getComponent());
} else {
delete config.displayMode;
this._model.doAction(
FlexLayout.Actions.updateNodeAttributes(tab.getId(), {config: config}));
this.props.store.exitModal();
}
}
}
};
// Add the readme and settings buttons to the tabset bar, if applicable
const selected = node.getSelectedNode()
const selectedTab = node.getSelectedNode()
// did we actually get a node?
if (typeof selected !== 'undefined') {
// is the active (selected) tab declared as configurable?
const config = selected.getConfig()
if (config && ('configurable' in config) && config['configurable'] === true) {
renderValues.buttons.push(
<img
className= "tabset-button"
src="images/icons/settings.svg"
alt="settings"
key="settings"
onClick={modeToggle("settings").bind(node)}
/>
)
}
// is the active (selected) tab declared as having a readme?
if (config && ('hasReadme' in config) && config['hasReadme'] === true) {
renderValues.buttons.push(
<img
className= "tabset-button"
src="images/icons/info-grey-outline.svg"
alt="readme"
key="readme"
onClick={modeToggle("readme").bind(node)}
/>
)
if (typeof selectedTab !== 'undefined') {
const components = this.props.store.components;
// Is there a store for this gadget?
if (selectedTab.getComponent() in components) {
const component = components[selectedTab.getComponent()];
// is the active (selected) tab configurable?
if ('configSchema' in component) {
renderValues.buttons.push(
<img src="images/icons/settings.svg"
alt="settings"
key="settings"
onClick={modeToggle("settings").bind(this)}/>
);
}
// does the active (selected) tab have a readme?
if ('readme' in component) {
renderValues.buttons.push(
<img src="images/icons/info-grey-outline.svg"
alt="readme"
key="readme"
onClick={modeToggle("readme").bind(this)}/>
);
}
}
}
}
}
render() {
// This line is there only to consume a mobX variable and trigger re-render of FlexLayout on changes in components.
// We need it so that we can have the "readme" and "settings" buttons displayed in the tabSet bars.
// It produces no other side-effect and the return value of this function call is ignored...
toJS(this.props.store.components);
return (
<FlexLayout.Layout ref={this.refLayout}
model={this.state.model}
factory={this.factory.bind(this)}
onRenderTabSet={this.RenderTabsetHandle}/>
onRenderTabSet={this.RenderTabsetHandle.bind(this)}/>
);
}
})
/**
* This function creates a placeholder React component in the case where the user
* asks for a gadget that does not exist.
* @param {String} component name of the required component
*/
function invalidComponent(component) {
return function(props) {
return <div>The component type "{component}"" is not declared.
The mistake is either in the configuration (type of a tab) or
in the source file <tt>src/Board.js</tt> (search for <tt>tab_types</tt>).
</div>;
}
}
export {Board}
......@@ -29,6 +29,12 @@ h2 {
margin: 10px 10px 10px 10px;
}
/* For the forms generated by react-jsonschema-form */
i.glyphicon { display: none; }
.btn-add::after { content: 'Add'; }
.array-item-move-up::after { content: 'Move Up'; }
.array-item-move-down::after { content: 'Move Down'; }
.array-item-remove::after { content: 'Remove'; }
.tabset-button:hover {
filter: invert(67%) sepia(71%) saturate(551%) hue-rotate(1deg) brightness(200%) contrast(86%);
}
......@@ -7,44 +7,34 @@ class HorizontalGauge extends Component {
render() {
const value = ('value' in this.props && this.props.value) ? this.props.value : 0;
// Define the dimensions of the gaug
const margin = { left:10, top: 10, right: 10, bottom: 10}
, width = this.props.size.width - margin.left - margin.right
, height = 20;
// Define the dimensions of the gauge
const margin = { left:10, top: 10, right: 10, bottom: 10},
width = this.props.size.width - margin.left - margin.right,
height = 20;
const viewbox = "0 0 " + (width + margin.left + margin.right) +" "+ (height + margin.top + margin.bottom);
const min = this.props.min || 0,
max = this.props.max || 1;
// Scale to map from values in [0, 1] to values in [0, width] (callable)
const xScale = scaleLinear()
.domain([0, 1])
.domain([min, max])
.range([0, width])
.clamp(true);
const svg = <svg viewBox={viewbox} width={width + margin.left + margin.right} height={height + margin.top + margin.bottom}>
{/* translate to add some margin around the gauge */}
<g className="gauge" transform={"translate(" + margin.left + "," + margin.top + ")"}>
{/* Background of the gauge */}
<rect className="background" x="0" y="0" width={width} height={height} ry="5px"/>
{/* Representation of the value */}
<rect className="overlay" x="0" y="0" width={xScale(value)} height={height} ry="5px"/>
{/* Text of the gauge */}
<text className="label" x={width/2} y={height/2} textAnchor="middle" dominantBaseline="central">
{"" + value.toFixed(2)}
</text>
</g>
{/* translate to add some margin around the gauge */}
<g className="gauge" transform={"translate(" + margin.left + "," + margin.top + ")"}>
{/* Background of the gauge */}
<rect className="background" x="0" y="0" width={width} height={height} ry="5px"/>
{/* Representation of the value */}
<rect className="overlay" x="0" y="0" width={xScale(value)} height={height} ry="5px"/>
{/* Text of the gauge */}
<text className="label" x={width/2} y={height/2} textAnchor="middle" dominantBaseline="central">
{"" + value.toFixed(2)}
</text>
</g>
</svg>;
const config = this.props.node.getConfig()
if (config && 'displayMode' in config) {
if (config.displayMode === "readme") {
return <p className="about">This gauge represent a <b>measure</b> ranging from 0 to 1.</p>;
} else if (config.displayMode === "settings") {
return <div>
<p>config panel</p>
</div>;
}
}
return <div>{svg}</div>;
}
}
......
import React, {Component} from 'react'
import RosLib from 'roslib';
import {decorate, observable, computed, autorun} from 'mobx'
import {observer} from 'mobx-react'
import RosLib from 'roslib'
import {get} from 'lodash/object'
import {defaultConfig} from '../utils/configurationHelpers'
import HorizontalGauge from './index'
class HGaugeStore {
readme = <p className="about">This gauge represents a <b>measure</b> ranging from 0 to 1.</p>;
configSchema = {
title: "Horizontal Gauge",
type: "object",
required: ["topic", "field", "min", "max"],
properties: {
topic: {type: "string", title: "ROS topic (absolute name)",
default: "/predictions/interaction_involvement"},
field: {type: "string", title: "the field to be read from the message",
default: "prediction_of_head_orientation.list[0].values[1]"},
min: {type: "number", title: "Lower end of the data range", default: 0},
max: {type: "number", title: "Upper end of the data range", default: 1},
}
};
}
const RosHGauge = observer(
class RosHGauge extends Component {
constructor(props) {
super(props)
this.state = {datum: null}
this.topic = "/predictions/interaction_involvement"
this.type = "april_messages/head_orientation_prediction"
this.datum = null;
this.props.store.components['h-gauge'] = new HGaugeStore();
defaultConfig(this.props.store.components['h-gauge']);
}
get topic() {
return this.props.store.components['h-gauge'].config.topic;
}
get field() {
return this.props.store.components['h-gauge'].config.field;
}
get min() {
return this.props.store.components['h-gauge'].config.min;
}
get max() {
return this.props.store.components['h-gauge'].config.max;
}
componentDidMount() {
if ('ros' in this.props && this.props.ros) {
this.listener = new RosLib.Topic({
ros : this.props.ros,
name: this.topic,
messageType: this.type
});
this.listener.subscribe(function(message) {
this.setState({datum: message.prediction_of_head_orientation.list[0].values[1]});
}.bind(this));
}else {
this.topicAutorun = autorun( reaction => {
if ('listener' in this) {
this.listener.unsubscribe();
}
this.listener = new RosLib.Topic({
ros : this.props.ros,
name: this.topic,
});
// callback on the topic, that takes stores the value of a field
// (defined in configuration) in 'this.datum'.
this.listener.subscribe(function(message) {
this.datum = get(message, this.field);
}.bind(this));
})
} else {
console.warn('RosHGauge expects to be passed a valid Ros object as property, got ', this.props.ros);
}
}
......@@ -35,8 +76,19 @@ class RosHGauge extends Component {
}
render() {
return <HorizontalGauge value={this.state.datum} {...this.props}/>
return <HorizontalGauge
value={this.datum}
min={this.min}
max={this.max}
{...this.props}/>;
}
}
})
decorate(RosHGauge, {
datum: observable,
topic: computed,
field: computed,
min: computed,
max: computed,
})
export default RosHGauge
\ No newline at end of file
export default RosHGauge
import React, {Component} from 'react'
import {decorate, observable} from 'mobx'
import {decorate, observable, computed} from 'mobx'
import {observer} from 'mobx-react'
import * as d3 from 'd3'
import {defaultConfig} from '../utils/configurationHelpers'
import LinearLayout from '../utils/LinearLayout'
import { extent } from '../utils/extent';
import { getBoredom, getIValence, getEValence } from './accessors';
......@@ -13,10 +14,50 @@ import { Backgrounds, Iteration, Actions, Mood,
class InteractionTraceStore {
currentInteraction = null;
showInteraction = false;
config = {};
configSchema = {
title: "Interaction trace",
type: "object",
properties: {
maxBoredom: {
title: "Maximal expected value for the boredom (used for scaling)",
type: "integer",
default: 2,
},
nbPrimitiveActions: {
title: "How many primitive actions are displayed",
type: "integer",
default: 3,
},
widthInEm: {
title: "The width of each column in em units",
type: "number",
default: 2.5,
},
debug: {
title: "Debug mode",
type: "boolean",
default: false
}
}
};
readme = (
<div className="about">
<h1>Trace of the reasoning process performed by the agent</h1>
<p>The <b>mood</b> represents the effect of the <b>valence</b> on the mood of the agent.</p>
<h2>Boredom</h2>
<p>
Observe the evolution of the <b>interactions</b> performed by the agent. <b>Intended</b> interactions are the
interactions the agent plans to perform whereas <b>enacted</b> interactions are the interactions actually
performed in the physical world of the agent. An interaction is composed of an <b>action</b> and a
<b>valence</b>, the valence being dependent of the response from the environment.
</p>
</div>);
}
decorate(InteractionTraceStore, {
currentInteraction: observable,
showInteraction: observable,
config: observable,
})
const InteractionTrace = observer(
......@@ -26,11 +67,12 @@ class InteractionTrace extends Component {
super(props);
this.interactionStore = new InteractionTraceStore();
this.props.store.components['interactionTrace'] = this.interactionStore;
this.props.store.components['int-trace'] = this.interactionStore;
// Fill the undefined fields of the configuration object with their default values.
defaultConfig(this.interactionStore);
// Use the margin convention practice
// Use the margin convention practice of D3
this.margin = {top: 20, right: 30, bottom: 20, left: 20};
// this.margin = {top: 0, right: 0, bottom: 0, left: 0};
// Blanck space between each element of the figure
this.spacing = {v: 20, h: 15};
......@@ -54,37 +96,37 @@ class InteractionTrace extends Component {
.ratioCell(0.5) // Row for the valences (takes a ratio of the avalable space)
.fixedCell(75); // Row for the actions (temporary value, updated later in function of this.em)
// Value for the boredom at which we plot a red line (showing max value)
this.maxBoredom = 2;
// How many primitive actions (at least) we want to display
this.nbPrimitiveActions = 3;
// Will store the conversion from 1em in px for the LabelColumn.
// This is a tricky trick to set the size of the LabelColumn to match with the longest chain of actions that
// it will have to show.
// Will store the conversion from 1em in px. We have to assume that this computation is approximative.
this.em = undefined;
// In debug mode, we add bounding boxes for the different areas of this figure
this.debug = false;
// Other parameter: how many em the minimal column width has to be (function minXWidth).
}
// The smallest allowed space around each point on the x axis.
minXWidth() {
return 2.5*this.em;
/** In debug mode, we add bounding boxes for the different areas of this figure */
get debug() {
return this.interactionStore.config.debug;
}
/** A red line, showing the maximum accepted boredom level, will be displayed using this value */
get maxBoredom() {
return this.interactionStore.config.maxBoredom;
}
/** How many primitive actions (at least) we want to display */
get nbPrimitiveActions() {
return this.interactionStore.config.nbPrimitiveActions;
}
/** The smallest allowed space around each point on the x axis. */
get minXWidth() {
<