From 20d56e88772ebc4fa0a640981d3b2df410ba2b2f Mon Sep 17 00:00:00 2001
From: Dorian Goepp <dorian.goepp@gmail.com>
Date: Fri, 3 May 2019 10:09:33 +0200
Subject: [PATCH] Fully configurable VideoStream

---
 src/VideoStream/index.js |  94 ++++++++++++-------------------
 src/demo-1-config.js     |   4 --
 src/utils/modeHandler.js | 118 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 155 insertions(+), 61 deletions(-)
 create mode 100644 src/utils/modeHandler.js

diff --git a/src/VideoStream/index.js b/src/VideoStream/index.js
index 28cb96d..b37fb62 100644
--- a/src/VideoStream/index.js
+++ b/src/VideoStream/index.js
@@ -1,75 +1,55 @@
 import React, {Component} from 'react'
-import Form from 'react-jsonschema-form'
+import {has} from 'lodash/object'
 import './VideoStream.css'
-
-const schema = {
-    title: "Video streamer",
-    type: "object",
-    required: ["host", "topic"],
-    properties: {
-        host: {type: "string", title: "Host (IP or name)", default:"localhost:8081"},
-        topic: {type: "string", title: "ROS topic (absolute name)", default: "/pepper/image_raw"}
-    }
-};
-
-const log = (type) => console.log.bind(console, type);
+import { modeHandler } from '../utils/modeHandler';
 
 class VideoStream extends Component {
     constructor(props) {
         super(props)
 
-        const hasUrl = ('url' in this.props && this.props.url);
-        this.topic = '/pepper/image_raw';
-        this.defaultImage = '';
-
-        this.state = {
-            url: hasUrl ? this.props.url : this.defaultImage
+        this.settingsSchema = {
+            title: "Video streamer",
+            type: "object",
+            required: ["host", "topic"],
+            properties: {
+                host: {type: "string", title: "Host (IP or name)", default:"localhost:8081"},
+                topic: {type: "string", title: "ROS topic (absolute name)", default: "/pepper/image_raw"}
+            }
         };
+        this.readme = (
+            <div className="about">
+                <h1>See what the agent is seeing</h1>
+                <p><b>Video stream</b> coming from the robot or the device.</p>
+            </div>
+        );
 
-        this.props.ros.getTopics(function(infos){
-            if (undefined !== infos.topics.find(elem => elem === this.topic)) {
-                this.setState({url: 'http://localhost:8081/stream?topic=' + this.topic});
-            }
-          }.bind(this))
-    }
+        this.defaultImage = '';
 
-    handleSettingsChange(event) {
-        this.setState({url: event.target.value})
+        this.modeHandler = new modeHandler(this,
+                                           () => (this.props.node.getConfig()),
+                                           (config) => {this.props.updateConfig(config)});
     }
 
-    render() {
-
-        const config = this.props.node.getConfig()
-        if (config && 'displayMode' in config) {
-            if (config.displayMode === "settings") {
-                return <div>
-                    {/* <form className="pure-form pure-form-stacked" onSubmit={(event)=>event.preventDefault()}>
-                        <fieldset>
-                            <label>Nice modification option for the module</label>
-
-                            <label htmlFor="url">URL of the stream</label>
-                            <input className="pure-input-1" id="url" name="url" type="text" placeholder="http://localhost:8080/..."
-                                value={this.state.url} onChange={this.handleSettingsChange.bind(this)}/>
-                        </fieldset>
-                    </form>
-                    <p>Changes are automatically saved!</p> */}
-                    <Form schema={schema}
-                        onChange={log("changed")}
-                        onSubmit={({formData}, e) => log(data.formData)}
-                        onError={log("errors")} />
-                </div>;
-            }
-            if (config.displayMode === "readme") {
-                return <div className="about">
-                    <h1>See what the agent is seeing</h1>
-                    <p><b>Video stream</b> coming from the robot or the device.</p>
-                </div>;
-            }
+    /**
+     * Return the url of the video stream to display, based on the current configuration.
+     * If there is no stream configured, gives the URL passed as a property to this object, or the one defined in
+     * this.defaultImage.
+     */
+    getStreamUrl() {
+        const config = this.props.node.getConfig();
+        const config_fields = ['topic', 'host'];
+        // Check that all requierd configuration fields are defined
+        if (config_fields.reduce((accumulator, field) => accumulator && has(config, field), true)) {            
+            return 'http://' + config.host + '/stream?topic=' + config.topic;
+        } else {
+            return ('url' in this.props && this.props.url) ? this.props.url : this.defaultImage;
         }
+    }
 
-        return (
+    render() {
+        return this.modeHandler.modalDisplay(
             <div className="video-stream container">
-                <img src={this.state.url} alt="there should have been a video stream here"/>
+                <img src={this.getStreamUrl()} alt="there should have been a video stream here"/>
             </div>);
     }
 }
diff --git a/src/demo-1-config.js b/src/demo-1-config.js
index 7116aa9..4bea714 100644
--- a/src/demo-1-config.js
+++ b/src/demo-1-config.js
@@ -94,10 +94,6 @@ export const flexlayout_json = {
                   {
                     "type": "tab",
                     "component": "video",
-                    "config": {
-                      "hasReadme": true,
-                      "configurable": true
-                    }
                   }
                 ]
               }
diff --git a/src/utils/modeHandler.js b/src/utils/modeHandler.js
new file mode 100644
index 0000000..8b1afed
--- /dev/null
+++ b/src/utils/modeHandler.js
@@ -0,0 +1,118 @@
+import React from 'react';
+import Form from 'react-jsonschema-form';
+import { merge } from 'lodash/object';
+
+export class modeHandler {
+    constructor(object, configGetter, configSetter) {
+        this.target = object
+        this.configGetter = configGetter;
+        this.configSetter = configSetter;
+
+        // get the current config; if it does not exist yet, create it
+        let config = this.configGetter() || {};
+        
+        merge(config, this.announceCapabilities(config));
+        if (config.configurable) {
+            merge(config, this.defaultConfigFromSchema(config));
+        }
+        this.configSetter(config);
+    }
+
+    /**
+     * Search for the 'settingsSchema' and 'readme' properties of the managed component. A corresponding field in the
+     * configuration is set accordingly. This is used to display or not the UI buttons to show the Readme or the
+     * configuration for of a component.
+     * @param {Object} config current configuration of the object
+     * @return the new configuration object (possibly unmodified)
+     */
+    announceCapabilities(config) {
+        if (this.target.settingsSchema) {
+            config.configurable = true;
+        }
+        else {
+            config.configurable = false;
+        }
+        if (this.target.readme) {
+            config.hasReadme = true;
+        }
+        else {
+            config.hasReadme = false;
+        }
+        return config;
+    }
+
+    /**
+     * Search for properties in a JSON Schema. If they are here, use the default value of each JsonSchema property for
+     * the configuration. This means that if a property is already defined in the configuration, it will not be
+     * altered.
+     * @param {Object} config current configuration of the object
+     * @return the new configuration object (possibly unmodified)
+     */
+    defaultConfigFromSchema(config) {
+        const schema = this.target.settingsSchema;
+        if (!schema) {
+            return config;
+        }
+        else if (!('properties' in schema)) {
+            console.warn("no properties in the schema");
+            return config;
+        }
+        else {
+            for (let [key, value] of Object.entries(schema.properties)) {
+                if (!(key in config)) {
+                    let message = "The key '" + key + "' is missing in the configuration.";
+                    if ('default' in value) {
+                        config[key] = value.default;
+                        message += " Using default value '" + value.default + "'.";
+                    }
+                    console.debug(message);
+                }
+            }
+            return config;
+        }
+    }
+
+    noop(value) {
+        return value;
+    }
+
+    /**
+     * Return either the value of the normal parameter, in normal mode, or the content for the `readme` and `settings`
+     * modes.
+     * 
+     * The display mode is defined by the 'displayMode' attribute of the component's configuraiton.
+     * @param {Jsx} normal The content to return in the normal mode (neither settings nor readme)
+     */
+    modalDisplay(normal) {
+        // return normal;
+        const config = this.configGetter();
+        if (config && 'displayMode' in config) {
+            if (config.configurable && config.displayMode === "settings") {
+                return (
+                <Form
+                    schema={this.target.settingsSchema}
+                    onSubmit={this.handleSettingsChange.bind(this)}
+                    formData={config} />);
+            }
+            if (config.hasReadme && config.displayMode === "readme") {
+                return this.target.readme;
+            }
+        }
+        return normal;
+    }
+
+    /**
+     * Given a new configuration object (from the form), update the configuration of the component (in FlexLayout).
+     *
+     * The update is done through Lodash's merge function.
+     * @param {Object} value update for the configuration of the component (we take the field formData)
+     * @return Also return the updated configuration.
+     */
+    handleSettingsChange({ formData }) {
+        let config = this.configGetter();
+        merge(config, formData);
+        this.configSetter(config);
+        return config;
+    }
+    
+}
\ No newline at end of file
-- 
GitLab