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