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

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

Allow gadgets to have their own configuration

See merge request behaviors-ai/the_dashboard!9
parents 0ea73528 43a62cec
......@@ -71,7 +71,7 @@ class Board extends React.Component {
var component = node.getComponent();
return React.createElement(getComponentType(component), {
node: node,
id: node.getId(),
ros: this.props.ros,
store: this.props.store
}, null);
......@@ -108,7 +108,7 @@ class Board extends React.Component {
/**
* 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.
......@@ -123,7 +123,7 @@ class Board extends React.Component {
// 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());
this.props.store.displayModal(modeName, selectedTab.getId());
} else {
this.props.store.exitModal();
}
......@@ -137,8 +137,8 @@ class Board extends React.Component {
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()];
if (selectedTab.getId() in components) {
const component = components[selectedTab.getId()];
// is the active (selected) tab configurable?
if ('configSchema' in component) {
renderValues.buttons.push(
......
......@@ -30,23 +30,23 @@ class RosHGauge extends Component {
super(props)
this.datum = null;
this.props.store.components['h-gauge'] = new HGaugeStore();
defaultConfig(this.props.store.components['h-gauge']);
this.props.store.components[this.props.id] = new HGaugeStore();
defaultConfig(this.props.store.components[this.props.id]);
}
get topic() {
return this.props.store.components['h-gauge'].config.topic;
return this.props.store.components[this.props.id].config.topic;
}
get field() {
return this.props.store.components['h-gauge'].config.field;
return this.props.store.components[this.props.id].config.field;
}
get min() {
return this.props.store.components['h-gauge'].config.min;
return this.props.store.components[this.props.id].config.min;
}
get max() {
return this.props.store.components['h-gauge'].config.max;
return this.props.store.components[this.props.id].config.max;
}
componentDidMount() {
if ('ros' in this.props && this.props.ros) {
this.topicAutorun = autorun( reaction => {
......@@ -57,7 +57,7 @@ class RosHGauge extends Component {
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) {
......@@ -68,13 +68,13 @@ class RosHGauge extends Component {
console.warn('RosHGauge expects to be passed a valid Ros object as property, got ', this.props.ros);
}
}
componentWillUnmount() {
if ('listener' in this) {
this.listener.unsubscribe();
}
}
render() {
return <HorizontalGauge
value={this.datum}
......
......@@ -12,8 +12,6 @@ import { Backgrounds, Iteration, Actions, Mood,
Legend } from './figureElements';
class InteractionTraceStore {
currentInteraction = null;
showInteraction = false;
config = {};
configSchema = {
title: "Interaction trace",
......@@ -55,8 +53,6 @@ class InteractionTraceStore {
</div>);
}
decorate(InteractionTraceStore, {
currentInteraction: observable,
showInteraction: observable,
config: observable,
})
......@@ -67,7 +63,7 @@ class InteractionTrace extends Component {
super(props);
this.interactionStore = new InteractionTraceStore();
this.props.store.components['int-trace'] = this.interactionStore;
this.props.store.components[this.props.id] = this.interactionStore;
// Fill the undefined fields of the configuration object with their default values.
defaultConfig(this.interactionStore);
......@@ -100,7 +96,7 @@ class InteractionTrace extends Component {
this.em = undefined;
}
/** In debug mode, we add bounding boxes for the different areas of this figure */
/** In debug mode, we add bounding boxes for the different areas of this figure */
get debug() {
return this.interactionStore.config.debug;
}
......@@ -308,7 +304,7 @@ class InteractionTrace extends Component {
y: 4,
xScale: xScale,
minActions: this.nbPrimitiveActions,
interactionStore: this.interactionStore,
store: this.props.store.sharedData,
},
{ // Labels for all figure elements
component: ComponentLabels,
......
......@@ -13,17 +13,17 @@ class RosInteractionTrace extends Component {
required: ["trace", "boredom", "mood"],
properties: {
trace: {
title: "Topic for the trace",
title: "Topic for the trace",
type: "string",
default: "/algorithm/trace"
},
boredom: {
title: "Topic for the boredom",
title: "Topic for the boredom",
type: "string",
default: "/algorithm/boredom"
},
mood: {
title: "Topic for the mood",
title: "Topic for the mood",
type: "string",
default: "/algorithm/mood"
},
......@@ -37,31 +37,31 @@ class RosInteractionTrace extends Component {
constructor(props) {
super(props)
this.data = [];
}
get traceTopic() {
return this.props.store.components['int-trace'].config.trace;
return this.props.store.components[this.props.id].config.trace;
}
get boredomTopic() {
return this.props.store.components['int-trace'].config.boredom;
return this.props.store.components[this.props.id].config.boredom;
}
get moodTopic() {
return this.props.store.components['int-trace'].config.mood;
return this.props.store.components[this.props.id].config.mood;
}
get lowerIdResets() {
return this.props.store.components['int-trace'].config.lowerIdResets;
return this.props.store.components[this.props.id].config.lowerIdResets;
}
componentDidMount() {
// Append the configuration schema of this component to the one of InteractionTrace
// Doing so, the configuration form will have all fields for both components.
appendConfigSchema(this.props.store.components['int-trace'], this.newSchema);
appendConfigSchema(this.props.store.components[this.props.id], this.newSchema);
if ('ros' in this.props && this.props.ros) {
// With this MobX autorun, if the topics are changed in the configuration,
// we will automatically unsubscribe from the old ones and register to the
......@@ -101,7 +101,7 @@ class RosInteractionTrace extends Component {
console.warn('RosInteractionTrace expects to be passed a valid Ros object as property, got ', this.props.ros);
}
}
componentWillUnmount() {
if ('trace_listener' in this) {
this.trace_listener.unsubscribe();
......@@ -113,7 +113,7 @@ class RosInteractionTrace extends Component {
this.mood_listener.unsubscribe();
}
}
render() {
return <InteractionTrace data={this.data} {...this.props}/>
}
......
......@@ -12,7 +12,7 @@ export function Separators(props) {
// elements.push(<rect x="0" y={y} width="100%" height={height} fill="white" stroke="none"/>);
const line_y = props.yLayout.upper(i) + props.yLayout.domain.padding / 2;
elements.push(
<line x1="0" y1={line_y} x2="100%" y2={line_y}
<line x1="0" y1={line_y} x2="100%" y2={line_y}
key={i} className="separator"/>);
}
return elements;
......@@ -57,17 +57,16 @@ export function Actions(props) {
if (interaction === undefined || interaction.enacted === undefined) {
return null;
}
// These two methods will handle when the mouse hovers and leaves an action. As a result, the hovered interaction
// is stored in props.interactionStore (mobx). The intent is to display information of this interaction somewhere
// else in the application.
const interactionStore = props.interactionStore
// is stored in props.store (mobx). The intent is to display information of this interaction somewhere else in the
// application.
const handleOver = () => {
props.interactionStore.currentInteraction = interaction;
interactionStore.showInteraction = true;
props.store.set('currentInteraction', interaction);
props.store.set('showInteraction', true);
}
const handleOut = () => {
interactionStore.showInteraction = false;
props.store.set('showInteraction', false);
}
const actions = cloneDeep(interaction.enacted.action_names);
......@@ -80,7 +79,7 @@ export function Actions(props) {
actions.push('');
valences.push(0);
}
return (
<text key={actions.join('')+interaction.id} y="0" style={style}
onMouseOver={handleOver} onMouseOut={handleOut}>
......@@ -182,17 +181,17 @@ export function Plots(props) {
// d3's line generator
const line = d3.line()
.x(d => props.xScale(d.x)) // set the x values for the line generator
.y(d => props.yScale(d.y)) // set the y values for the line generator
.y(d => props.yScale(d.y)) // set the y values for the line generator
.curve(d3.curveLinear); // don't apply smoothing to the line
// .curve(d3.curveMonotoneX) // apply smoothing to the line
// For each curve
return Object.keys(props.data).map((name) => {
const data = props.data[name].filter((point, i) => {
return !(point === undefined || point.x === undefined || point.y === undefined)
});
const colour = props.colour(name);
// Add markers on the line (by default)
const markers = data.map((point, i) => {
return <circle className="dot" key={"dot-"+i} fill={colour}
......@@ -216,7 +215,7 @@ export function Plots(props) {
*/
export function Legend(props) {
const em = props.em || 1;
const names = Object.keys(props.data);
const yScale = index => props.yOffset + index * em * 1.2;
......@@ -237,8 +236,8 @@ export function Legend(props) {
</text>,
<path key={"line" + name} className={"line " + name}
d={line(data)} stroke={props.colour(name)} />
]})}
</g>
);
}
\ No newline at end of file
}
......@@ -7,18 +7,18 @@ export const LatestInteraction = observer(
function LatestInteraction(props) {
let intended, enacted;
let iteration = null;
if ('int-trace' in props.store.components && props.store.components['int-trace'].showInteraction) {
intended = props.store.components['int-trace'].currentInteraction.intended;
enacted = props.store.components['int-trace'].currentInteraction.enacted;
const iteration_id = props.store.components['int-trace'].currentInteraction.id;
iteration = [<p key="first"><span className="label">Iteration:</span> {iteration_id}</p>, <hr key="last"/>];
if (props.store.sharedData.has('currentInteraction') && props.store.sharedData.get('showInteraction')) {
intended = props.store.sharedData.get('currentInteraction').intended;
enacted = props.store.sharedData.get('currentInteraction').enacted;
const iteration_id = props.store.sharedData.get('currentInteraction').id;
iteration = [<p key="first"><span className="label">Iteration:</span> {iteration_id}</p>, <hr key="last"/>];
} else {
intended = props.intended;
enacted = props.enacted;
}
/**
* Return the
* Return the
* @param {Object} inter object representing the interaction to display (either intended or enacted)
* @param {Number} i index of primitive action which row this is makeing
*/
......@@ -27,7 +27,7 @@ function LatestInteraction(props) {
return (
<td className={actionClassName(inter.primitive_valences[i])}>
{inter.action_names[i]} ({inter.primitive_valences[i]})
</td>);
</td>);
} else {
return null;
}
......
......@@ -24,7 +24,7 @@ class LatestInteractionStore {
required: ["trace"],
properties: {
trace: {
title: "Topic for the trace",
title: "Topic for the trace",
type: "string",
default: "/algorithm/trace"
},
......@@ -40,9 +40,9 @@ class RosLatestInteraction extends Component {
constructor(props) {
super(props);
this.props.store.components['latest-interaction'] = new LatestInteractionStore();
this.props.store.components[this.id] = new LatestInteractionStore();
// Fill the undefined fields of the configuration object with their default values.
defaultConfig(this.props.store.components['latest-interaction']);
defaultConfig(this.props.store.components[this.id]);
this.latestInteraction = {
intended: {
......@@ -57,8 +57,12 @@ class RosLatestInteraction extends Component {
this.topicType = "april_messages/trace";
}
get id() {
return this.props.id;
}
get topicName() {
return this.props.store.components['latest-interaction'].config.trace;
return this.props.store.components[this.id].config.trace;
}
componentDidMount() {
......@@ -77,7 +81,7 @@ class RosLatestInteraction extends Component {
name: this.topicName,
messageType: this.topicType
});
// Subscribe (and declare a callback)
this.trace_listener.subscribe(message => {
this.latestInteraction = message;
......
......@@ -26,7 +26,7 @@ class LineChartStore {
type: "object",
properties: {
topic: {
title: "topic",
title: "topic",
type: "string",
default: "/std_msgs/int64"
},
......@@ -80,7 +80,7 @@ class RosLineChart extends Component {
super(props);
this.store = new LineChartStore();
this.props.store.components['line-chart'] = this.store;
this.props.store.components[this.props.id] = this.store;
// Fill the undefined fields of the configuration object with their default values.
defaultConfig(this.store);
......@@ -90,7 +90,7 @@ class RosLineChart extends Component {
/**
* Extract a field from a ros message and add it to the state, for plotting.
*
*
* If there are already maxPoints in storage, we remove the oldest data point.
* @param {Object} curve has two entries, `field`, path to the field to be extracted from incoming messages
* and `topic` the ROS topic from which we extract the field.
......
......@@ -93,28 +93,28 @@ class MemoryRanked extends Component {
super(props);
this.store = new MemoryStore();
this.props.store.components['memory'] = this.store;
this.props.store.components[this.props.id] = this.store;
// Fill the undefined fields of the configuration object with their default values.
defaultConfig(this.store);
// Use the margin convention practice
this.margin = {top: 10, right: 20, bottom: 20, left: 20}
// The content of each figure element is offset this much down, to leave room for the main labels
this.yOffset = 50;
// 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.
this.em = undefined;
// The increment in Y coordinate between two entries in the memory
this.vStep = 30;
// // How frequent the horizontal guide lines should be put
// // If set to 4, one guide line will be displayed every four line of data
// this.guideLinesStep = config.guideLinesStep;
// this.guideLinesStep = config.guideLinesStep;
// // The highest number of actions displayed in one memory entry.
// this.maxActionLength = config.maxActionLength;
}
......@@ -126,11 +126,11 @@ class MemoryRanked extends Component {
get maxActionLength() {
return this.store.maxActionLength;
}
/** Get the font size of a node in the DOM. It tells us how big one em is in px.
*
*
* This value is used to adjust the width of the LabelColumn based on the width of its content.
*
*
* @param {DOMnode} node the DOM node from which we retrieve information
*/
getEmSize(node) {
......@@ -138,15 +138,15 @@ class MemoryRanked extends Component {
this.em = parseFloat(getComputedStyle(node).fontSize);
}
}
/** Y scale will use the randomly generated number
/** Y scale will use the randomly generated number
* @param {Number} i the index of a line of data
* @return how many pixels down on the ordinate axis the line of data must be displayed
*/
yScale(i) {
return i*this.vStep;
}
/** Generate guide lines every `this.guideLineCount` line of data
* @return array of ordinate, one for each line to be added
*/
......@@ -163,10 +163,10 @@ class MemoryRanked extends Component {
// Here, 50 is the top margin used in all the columns of this component
y.push(this.margin.top + 50 + this.yScale(this.guideLinesStep*i) + this.vStep / 2)
}
return y;
}
sort(data) {
let sorter;
// Make a sorting function based on the configuration or, by default, sort
......@@ -186,22 +186,22 @@ class MemoryRanked extends Component {
return b.valence - a.valence;
}
}
return data.sort(sorter);
}
render() {
const data = this.sort(this.props.data);
// Minimal required height for this figure
const minHeight = this.margin.top + this.yOffset + this.yScale(data.length+2) + this.margin.bottom;
const outerHeight = Math.max(minHeight, this.props.size.height);
// width and height are the inner dimensions of the drawing
const width = this.props.size.width - this.margin.left - this.margin.right,
height = outerHeight - this.margin.top - this.margin.bottom;
// Object that manages the correct spacing and placing of each figure element, in the horizontal direction.
this.xLayout = new LinearLayout(this.props.size.width)
.magnitudeAuto()
......@@ -210,7 +210,7 @@ class MemoryRanked extends Component {
.fixedCell(this.maxActionLength*this.em*0.8 || 60)
.fixedCell(50)
.ratioCell(1);
const figureElements = [
{ // Actions column
component: LabelColumn,
......@@ -251,29 +251,29 @@ class MemoryRanked extends Component {
</div>;
}
}
const viewbox = "0 0 " + (width + this.margin.left + this.margin.right)
+ " " + (height + this.margin.top + this.margin.bottom);
return (
<svg viewBox={viewbox} width="100%" height={outerHeight} className="memory-ranked">
{/* Draw a reactangle around the drawing area of the SVG */}
{/* <rect x="0" y="0" width="100%" height="100%" fill="none" stroke="red"/> */}
{/* 1. The main figure elements */}
{figureElements.map(this.makeSVGComponent.bind(this))}
{/* 2. Vertical partition between the actions and the occurences */}
<line className="partition"
x1={this.xLayout.upper(0) + this.xLayout.padding()/2} y1={this.margin.top}
x2={this.xLayout.upper(0) + this.xLayout.padding()/2} y2={this.margin.top + height}/>
{/* 3. Vertical partition between the occurences and the valences */}
<line className="partition"
x1={this.xLayout.upper(1) + this.xLayout.padding()/2} y1={this.margin.top}
x2={this.xLayout.upper(1) + this.xLayout.padding()/2} y2={this.margin.top + height}/>
{/* 4. Now, horizontal guide lines, to help reading this visualisation */}
{this.guideLines().map((yValue) => {
return <line key={yValue+"guideline"} className="guideline"
......@@ -282,14 +282,14 @@ class MemoryRanked extends Component {
})}
</svg>);
}
/**
* Create the react Components declared in declaration and place them at the required x and y coordinates.
*
*
* *Note*: it passes an additional `width` attribute on to the elements so that they know how much horizontal
* space is available for them.
* @param {Object} declaration object defining what elements will be appended to the SVG.
* Expected attributes:
* Expected attributes:
* - component: React component (function or class) to be added
* - x: index of the element, on the horizontal axis
* - all additional attributes are passed to the React Component
......@@ -304,10 +304,10 @@ class MemoryRanked extends Component {
let props = Object.assign({}, declaration);
delete props.x; delete props.y; delete props.component;
props.width = this.xLayout.width(declaration.x);
// Instanciate the component
const component = React.createElement(declaration.component, props);
return (
<g key={"SVG"+i} transform={"translate(" + x + "," + y + ")"}>
{/* <rect x="0" y="0" width={valenceLocation.width} height={valenceLocation.height} fill="none" stroke="steelblue"/> */}
......@@ -320,5 +320,5 @@ decorate(MemoryRanked, {
guideLinesStep: computed,
maxActionLength: computed,
})
export default MemoryRanked;
\ No newline at end of file
export default MemoryRanked;
......@@ -29,13 +29,13 @@ class RosMemoryRanked extends Component {
};
get topic() {
return this.props.store.components['memory'].config.topic;
return this.props.store.components[this.props.id].config.topic;
}
componentDidMount() {
// Append the configuration schema of this component to the one of MemoryRanked
// Doing so, the configuration form will have all fields for both components.
appendConfigSchema(this.props.store.components['memory'], this.newSchema);
appendConfigSchema(this.props.store.components[this.props.id], this.newSchema);
if ('ros' in this.props && this.props.ros) {
// With this MobX autorun, if the topics are changed in the configuration,
......
......@@ -37,7 +37,7 @@ class RosMood extends Component {
super(props)
this.store = new MoodStore();
this.props.store.components['mood'] = this.store;
this.props.store.components[this.props.id] = this.store;
// Fill the undefined fields of the configuration object with their default values.
defaultConfig(this.store);
......
......@@ -44,7 +44,7 @@ class VideoStream extends Component {
// Fill the undefined fields of the configuration object with their default values.
defaultConfig(this.store);
this.props.store.components['video'] = this.store;
this.props.store.components[this.props.id] = this.store;