Commit dae965f0 authored by Dorian Goepp's avatar Dorian Goepp

Merge branch 'dgoepp/widget-managment' into 'master'

Add widgets to the dashboard, live!

See merge request behaviors-ai/the_dashboard!11
parents 8e17f376 64befa3f
......@@ -8233,9 +8233,9 @@
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash._reinterpolate": {
"version": "3.0.0",
......@@ -8559,9 +8559,9 @@
}
},
"mixin-deep": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
"integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
"integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
"requires": {
"for-in": "^1.0.2",
"is-extendable": "^1.0.1"
......@@ -10643,6 +10643,11 @@
"resolved": "https://registry.npmjs.org/react-numeric-input/-/react-numeric-input-2.2.3.tgz",
"integrity": "sha1-S/WRjD6v7YUagN8euZLZQQArtVI="
},
"react-pure-modal": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/react-pure-modal/-/react-pure-modal-1.5.1.tgz",
"integrity": "sha512-ac5H4D9D6W1Mi7IMyFoNGxht+uK/YhFkB0S10rG6ptHs9B7kTvYic718u9mRPDep0AWhonl/pWT3E97dTWlrcg=="
},
"react-scripts": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.0.1.tgz",
......@@ -11438,9 +11443,9 @@
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"set-value": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
"integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
"integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
"requires": {
"extend-shallow": "^2.0.1",
"is-extendable": "^0.1.1",
......@@ -12556,35 +12561,14 @@
}
},
"union-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
"integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
"integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
"requires": {
"arr-union": "^3.1.0",
"get-value": "^2.0.6",
"is-extendable": "^0.1.1",
"set-value": "^0.4.3"
},
"dependencies": {
"extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"requires": {
"is-extendable": "^0.1.0"
}
},
"set-value": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
"integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
"requires": {
"extend-shallow": "^2.0.1",
"is-extendable": "^0.1.1",
"is-plain-object": "^2.0.1",
"to-object-path": "^0.3.0"
}
}
"set-value": "^2.0.1"
}
},
"uniq": {
......
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M13 7h-2v4H7v2h4v4h2v-4h4v-2h-4V7zm-1-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" fill="#A1A1A1"/></svg>
......@@ -2,23 +2,45 @@ import React from 'react';
// Tab-based dynamic layout manager
import FlexLayout from 'flexlayout-react';
// MobX, a state manager
import {toJS} from 'mobx'
import {reaction, 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'
import {RosHGauge} from './HorizontalGauge'
import {RosMemoryRanked, DemoMemoryRanked} from './MemoryRanked'
import {RosInteractionTrace, DemoInteractionTrace} from './InteractionTrace'
import {RosLatestInteraction, DemoLatestInteraction} from './LatestInteraction'
import {RosMood, DemoMood} from './Mood'
import {Graph, DemoGraph} from './GraphVisJs'
import DemoString from './String'
import {VideoStream, DemoVideoStream} from '../VideoStream'
import {RosLineChart, DemoLineChart} from '../LineChart'
import {RosHGauge} from '../HorizontalGauge'
import {RosMemoryRanked, DemoMemoryRanked} from '../MemoryRanked'
import {RosInteractionTrace, DemoInteractionTrace} from '../InteractionTrace'
import {RosLatestInteraction, DemoLatestInteraction} from '../LatestInteraction'
import {RosMood, DemoMood} from '../Mood'
import {Graph, DemoGraph} from '../GraphVisJs'
import DemoString from '../String'
// A component used to display the readme or config form for the widgets
import {GadgetModal} from '../utils/GadgetModal';
// 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'
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'
export const tab_types = {
"video": VideoStream,
"demo-video": DemoVideoStream,
"line-chart": RosLineChart,
"demo-line-chart": DemoLineChart,
"h-gauge": RosHGauge,
"memory": RosMemoryRanked,
"demo-memory": DemoMemoryRanked,
"int-trace": RosInteractionTrace,
"demo-int-trace": DemoInteractionTrace,
"string": DemoString,
"demo-latest-interaction": DemoLatestInteraction,
"latest-interaction": RosLatestInteraction,
"mood": RosMood,
"demo-mood": DemoMood,
"graph": Graph,
"demo-graph": DemoGraph,
}
const Board = observer(
class Board extends React.Component {
......@@ -26,6 +48,8 @@ class Board extends React.Component {
super(props)
this.state = {model: FlexLayout.Model.fromJson(json)}
reaction(() => this.props.store.sharedData.get('newTab'), this.addTab.bind(this));
this.refLayout = React.createRef();
}
......@@ -34,25 +58,6 @@ class Board extends React.Component {
* @param node node to be added, as per the vocabulary of FlexLayout
*/
factory(node) {
const tab_types = {
"video": VideoStream,
"demo-video": DemoVideoStream,
"line-chart": RosLineChart,
"demo-line-chart": DemoLineChart,
"h-gauge": RosHGauge,
"memory": RosMemoryRanked,
"demo-memory": DemoMemoryRanked,
"int-trace": RosInteractionTrace,
"demo-int-trace": DemoInteractionTrace,
"string": DemoString,
"demo-latest-interaction": DemoLatestInteraction,
"latest-interaction": RosLatestInteraction,
"mood": RosMood,
"demo-mood": DemoMood,
"graph": Graph,
"demo-graph": DemoGraph,
}
/** This function gives the React Component correspinding to a tab type.
*
* A set of React components are mapped to "tab types" (which are strings). Hence, the tab type "video" maps
......@@ -77,12 +82,15 @@ class Board extends React.Component {
}, null);
}
onAdd(event) {
this.refLayout.current.addTabWithDragAndDropIndirect("Add panel<br>(Drag to location)", {
component: "h-gauge",
name: "added",
config: {value: 0.5}
}, null);
addTab() {
if (this.props.store.sharedData.has('newTab')) {
const {component, name} = this.props.store.sharedData.get('newTab');
this.refLayout.current.addTabWithDragAndDrop(`Add ${name}<br>(Drag to location)`, {
component: component,
name: name
}, null);
this.props.store.sharedData.delete('newTab')
}
}
/**
......@@ -123,7 +131,9 @@ 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.getId());
this.props.store.displayModal(
<GadgetModal store={this.props.store} component={selectedTab.getId()} mode={modeName}/>
);
} else {
this.props.store.exitModal();
}
......
import React from 'react';
import {decorate, observable} from 'mobx'
import {observer} from 'mobx-react'
import {map} from 'lodash'
// Mappings from 'string' types (as per tab_types in Board.js) and display names of the available widgets.
const tab_names = {
"video": "Video stream",
"line-chart": "Simple line chart",
"h-gauge": "Gauge",
"memory": "Agent's memory",
"int-trace": "Interaction trace",
"latest-interaction": "Latest interaction",
"mood": "Current mood",
"graph": "Graph"
}
const demo_tab_names = {
"demo-video": "Video stream demo",
"demo-line-chart": "Line chart demo",
"demo-memory": "Agent's memory demo",
"demo-int-trace": "Interaction trace demo",
"demo-latest-interaction": "Latest interaction demo",
"demo-mood": "Current mood demo",
"demo-graph": "Graph demo",
"string": "ROS debug",
}
/**
* This React component appears in a modal, once we click on the '+' button in the menu. It offers the user to choose
* which component he/she wants to add to the dashboard.
*/
export const NewWidget = observer(
class NewWidget extends React.Component {
selected = Object.keys(tab_names)[0];
enableDemoWidgets = false;
/**
* Set the data for a new tab/widget to be added in the dashboard. This will trigger a method in Board.js, thanks to
* Mobx's magic. It also closes this modal.
*/
addTab() {
this.props.store.sharedData.set('newTab', {
component: this.selected,
name: tab_names[this.selected]
});
this.props.store.exitModal();
}
/**
* This method is used to generate radio inputs for the widgets we offer the user to add to the dashboard. It's
* called in a map (Lodash style).
* @param {string} name Display name of the widget
* @param {string} type string for the type of the widget, as used in Board.js' tab_types
*/
inputFromTabName(name, type) {
return (
<div key={type}>
<label>
<input
type="radio"
name="component-type"
value={type}
checked={type === this.selected}
onChange={(changeEvent) => {this.selected = changeEvent.target.value}}
/>
&nbsp;{name}
</label>
</div>
);
}
render() {
let inputs = map(tab_names, this.inputFromTabName.bind(this));
// if the user chose to see the demo widgets, add these to the list
if (this.enableDemoWidgets) {
inputs.push(map(demo_tab_names, this.inputFromTabName.bind(this)));
}
return (
<div>
{inputs}
<label>
<input
type="checkbox"
checked={this.enableDemoWidgets}
onChange={(changeEvent) => {this.enableDemoWidgets = changeEvent.target.checked;}}
/>
&nbsp;Show demo widgets
</label><br/>
<button onClick={this.addTab.bind(this)}>Add to the dashboard</button>
</div>
)
}
})
decorate(NewWidget, {
selected: observable,
enableDemoWidgets: observable,
})
......@@ -75,11 +75,23 @@
}
.lock-button {
margin-left:70px;
margin-left: 70px;
}
.about-button {
margin-left:30px;
margin-left: 30px;
}
.add-button {
margin-left: 50px;
position: fixed;
bottom: 5vh;
display: inline-block;
}
.add-button:hover {
filter: invert(67%) sepia(71%) saturate(551%) hue-rotate(1deg) brightness(111%) contrast(86%);
cursor: pointer;
}
.sidenav---sidenav---_2tBP.sidenav---expanded---1KdUL .title {
......
import React from "react";
import {observer} from "mobx-react";
import SideNav, { NavItem, NavIcon, NavText } from '@trendmicro/react-sidenav';
import './Menu.css'
import {NewWidget} from '../Board/NewWidget'
export const Menu = observer(
function Menu(props) {
const lock = props.store.sharedData.get('lock') || 'closed';
function toggle_lock() {
const new_value = (lock === 'closed') ? 'open' : 'closed';
props.store.sharedData.set('lock', new_value);
}
function add_widget() {
props.store.displayModal(<NewWidget store={props.store}/>);
}
// Display an "add" icon if the lock is open. This icon is used to add new widgets to the dashboard.
let add_button = null;
if (lock === 'open') {
add_button = (
<img className="footer add-button"
onClick={add_widget}
src={window.location.origin + "/images/icons/add_circle_outline.svg"}
alt="add a new widget"
/>);
}
export function Menu(props) {
return(
<SideNav
......@@ -29,12 +52,10 @@ export function Menu(props) {
<NavItem eventKey="demo">
<NavIcon>
{/* <a href="#"> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/dashboard-grey.svg"}
alt="Demo mode"
/>
{/* </a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/dashboard-grey.svg"}
alt="Demo mode"
/>
</NavIcon>
<NavText>
Demo
......@@ -51,12 +72,10 @@ export function Menu(props) {
<NavItem eventKey="memory">
<NavIcon>
{/* <a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/memory-grey.svg"}
alt="memory dashboard"
/>
{/* </a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/memory-grey.svg"}
alt="memory dashboard"
/>
</NavIcon>
<NavText>
Memory
......@@ -65,12 +84,10 @@ export function Menu(props) {
<NavItem eventKey="traces">
<NavIcon>
{/* <a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/trace-grey.svg"}
alt="trace dashboard"
/>
{/* </a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/trace-grey.svg"}
alt="trace dashboard"
/>
</NavIcon>
<NavText>
Traces
......@@ -79,12 +96,10 @@ export function Menu(props) {
<NavItem eventKey="mental-states">
<NavIcon>
{/* <a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/mood-grey.svg"}
alt="mood dashboard"
/>
{/* </a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/mood-grey.svg"}
alt="mood dashboard"
/>
</NavIcon>
<NavText>
Mental states
......@@ -93,12 +108,10 @@ export function Menu(props) {
<NavItem eventKey="decision-making">
<NavIcon>
{/* <a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/choice-grey.svg"}
alt="choice dashboard"
/>
{/* </a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/choice-grey.svg"}
alt="choice dashboard"
/>
</NavIcon>
<NavText>
Decision making
......@@ -111,12 +124,10 @@ export function Menu(props) {
<NavItem eventKey="video">
<NavIcon>
{/* <a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/video-grey.svg"}
alt="video dashboard"
/>
{/* </a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/video-grey.svg"}
alt="video dashboard"
/>
</NavIcon>
<NavText>
Video
......@@ -125,12 +136,10 @@ export function Menu(props) {
<NavItem eventKey="audio">
<NavIcon>
{/* <a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/microphone-grey.svg"}
alt="microphone dashboard"
/>
{/* </a> */}
</NavIcon>
<NavText>
Audio
......@@ -139,18 +148,18 @@ export function Menu(props) {
<NavItem eventKey="sensory">
<NavIcon>
{/* <a> */}
<img className="menu-button"
src={window.location.origin + "/images/icons/fingerprint-grey.svg"}
alt="sensory dashboard"
/>
{/* </a> */}
</NavIcon>
<NavText>
Sensory
</NavText>
</NavItem>
{add_button}
<a href="more.html">
<img className="footer menu-button about-button"
src={window.location.origin + "/images/icons/about.svg"}
......@@ -158,13 +167,12 @@ export function Menu(props) {
/>
</a>
{/* <a> */}
<img className="footer menu-button lock-button"
src={window.location.origin + "/images/icons/lock_closed-grey.svg"}
alt="lock/unlock the dashboard"
/>
{/* </a> */}
<img className="footer menu-button lock-button"
src={window.location.origin + "/images/icons/lock_" + lock + "-grey.svg"}
alt="lock/unlock the dashboard"
onClick={toggle_lock}
/>
</SideNav.Nav>
</SideNav>
)
}
})
import React from "react";
import ReactDOM from "react-dom";
// A little library to display modals on top of the page
import PureModal from 'react-pure-modal';
import 'react-pure-modal/dist/react-pure-modal.min.css';
// MobX, a state manager
import {decorate, observable} from "mobx"
import {observer} from 'mobx-react'
// ROS library
import RosLib from 'roslib';
// React Components to be displayed
import {Board} from './Board';
import {Board} from './Board/Board';
import {Header} from './Header';
import {Menu} from "./Menu";
import {Modal} from './utils/modeModal';
import 'bootstrap/dist/css/bootstrap.min.css'
// CSS common to the whole app
......@@ -20,12 +22,10 @@ class ObservableStore {
sharedData = observable.map()
modal = {
enabled: false,
mode: null,
component: null,
}
displayModal(mode, component) {
displayModal(component) {
this.modal.enabled = true;
this.modal.mode = mode;
this.modal.component = component;
}
exitModal() {
......@@ -33,6 +33,7 @@ class ObservableStore {
this.modal.mode = null;
this.modal.component = null;
}
dragging = false
}
decorate(ObservableStore, {
components: observable,
......@@ -67,25 +68,36 @@ class Main extends React.Component {
}
}
// When the esc key is pressed, if the modal is enabled, disable it
handleKeyDown = (event) => {
if(event.keyCode === 27 && this.store.modal.enabled === true) {
this.store.modal.enabled = false;
}
}
componentDidMount(){
document.addEventListener("keydown", this.handleKeyDown, false);
}
componentWillUnmount() {
if (this.ros) {
this.ros.close()
}
if (this.ros) {
this.ros.close()
}
document.removeEventListener("keydown", this.handleKeyDown, false);
}
render() {
let modal = null;
if (this.store.modal.enabled) {
modal = (
<div id="overlay" tabIndex="-1" role="dialog">
<Modal store={this.store}/>
</div>);
function addTab(component, name) {
return () => {
this.store.sharedData.set('newTab', {component: component, name: name});
}
}
return (
<div id="level-1">
<div id="menu">
<Menu ros={this.ros}/>
<Menu ros={this.ros} store={this.store} drag={addTab.bind(this)}/>
</div>
<div id="level-2">
<div id="header">
......@@ -95,7 +107,14 @@ class Main extends React.Component {
<Board ros={this.ros} store={this.store}/>
</div>
</div>
{modal}
<PureModal
width = "700px"
onClose={() => {this.store.modal.enabled = false}}
isOpen = {this.store.modal.enabled}
ref="modal"
>
{this.store.modal.component}