diff --git a/Dockerfile b/Dockerfile index 43930c8b..2a74ac19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,7 @@ WORKDIR /webodm/nodeodm/external/node-OpenDroneMap RUN npm install --quiet WORKDIR /webodm -RUN npm install --quiet -g webpack && npm install --quiet -g webpack-cli && npm install --quiet && webpack +RUN npm install --quiet -g webpack && npm install --quiet -g webpack-cli && npm install --quiet && webpack --mode production RUN python manage.py collectstatic --noinput RUN rm /webodm/webodm/secret_key.py diff --git a/README.md b/README.md index f87c0273..202e0fd6 100644 --- a/README.md +++ b/README.md @@ -358,7 +358,7 @@ pip install -r requirements.txt sudo npm install -g webpack sudo npm install -g webpack-cli npm install -webpack +webpack --mode production python manage.py collectstatic --noinput chmod +x start.sh && ./start.sh --no-gunicorn ``` diff --git a/app/plugins/functions.py b/app/plugins/functions.py index 7932bf8d..1db76f8a 100644 --- a/app/plugins/functions.py +++ b/app/plugins/functions.py @@ -7,6 +7,9 @@ import django import json from django.conf.urls import url from functools import reduce +from string import Template + +from django.http import HttpResponse from webodm import settings @@ -18,9 +21,36 @@ def register_plugins(): # Check for package.json in public directory # and run npm install if needed if plugin.path_exists("public/package.json") and not plugin.path_exists("public/node_modules"): - logger.info("Running npm install for {}".format(plugin.get_name())) + logger.info("Running npm install for {}".format(plugin)) subprocess.call(['npm', 'install'], cwd=plugin.get_path("public")) + # Check if we need to generate a webpack.config.js + if len(plugin.build_jsx_components()) > 0 and plugin.path_exists('public'): + logger.info("Generating webpack.config.js for {}".format(plugin)) + + build_paths = map(lambda p: os.path.join(plugin.get_path('public'), p), plugin.build_jsx_components()) + paths_ok = not (False in map(lambda p: os.path.exists, build_paths)) + + if paths_ok: + wpc_path = os.path.join(settings.BASE_DIR, 'app', 'plugins', 'templates', 'webpack.config.js.tmpl') + with open(wpc_path) as f: + tmpl = Template(f.read()) + + # Create entry configuration + entry = {} + for e in plugin.build_jsx_components(): + entry[os.path.splitext(os.path.basename(e))[0]] = [os.path.join('.', e)] + wpc_content = tmpl.substitute({ + 'entry_json': json.dumps(entry) + }) + + with open(plugin.get_path('public/webpack.config.js'), 'w') as f: + f.write(wpc_content) + logger.info('Wrote public/webpack.config.js for {}'.format(plugin)) + else: + logger.warning("Cannot generate webpack.config.js for {}, a path is missing: {}".format(plugin, ' '.join(build_paths))) + + # Check for webpack.config.js (if we need to build it) if plugin.path_exists("public/webpack.config.js") and not plugin.path_exists("public/build"): logger.info("Running webpack for {}".format(plugin.get_name())) @@ -125,6 +155,22 @@ def get_plugins_path(): return os.path.abspath(os.path.join(current_path, "..", "..", "plugins")) +def get_dynamic_script_handler(script_path, callback=None, **kwargs): + def handleRequest(request): + if callback is not None: + template_params = callback(request, **kwargs) + if not template_params: + return HttpResponse("") + else: + template_params = kwargs + + with open(script_path) as f: + tmpl = Template(f.read()) + return HttpResponse(tmpl.substitute(template_params)) + + return handleRequest + + def versionToInt(version): """ Converts a WebODM version string (major.minor.build) to a integer value diff --git a/app/plugins/grass_engine.py b/app/plugins/grass_engine.py index 021824ee..7ac08a98 100644 --- a/app/plugins/grass_engine.py +++ b/app/plugins/grass_engine.py @@ -3,7 +3,6 @@ import shutil import tempfile import subprocess import os -import geojson from string import Template diff --git a/app/plugins/plugin_base.py b/app/plugins/plugin_base.py index a28c1fb9..15c9c6a7 100644 --- a/app/plugins/plugin_base.py +++ b/app/plugins/plugin_base.py @@ -82,6 +82,15 @@ class PluginBase(ABC): """ return [] + def build_jsx_components(self): + """ + Experimental + Should be overriden by plugins that want to automatically + build JSX files. + All paths are relative to a plugin's /public folder. + """ + return [] + def main_menu(self): """ Should be overriden by plugins that want to add @@ -106,5 +115,20 @@ class PluginBase(ABC): """ return [] + def get_dynamic_script(self, script_path, callback = None, **template_args): + """ + Retrieves a view handler that serves a dynamic script from + the plugin's directory. Dynamic scripts are normal Javascript + files that optionally support Template variable substitution + via ${vars}, computed on the server. + :param script_path: path to script relative to plugin's directory. + :param callback: optional callback. The callback can prevent the script from being returned if it returns False. + If it returns a dictionary, the dictionary items are used for variable substitution. + :param template_args: Parameters to use for variable substitution (unless a callback is specified) + :return: Django view + """ + from app.plugins import get_dynamic_script_handler + return get_dynamic_script_handler(self.get_path(script_path), callback, **template_args) + def __str__(self): return "[{}]".format(self.get_module_name()) \ No newline at end of file diff --git a/plugins/measure/public/webpack.config.js b/app/plugins/templates/webpack.config.js.tmpl similarity index 94% rename from plugins/measure/public/webpack.config.js rename to app/plugins/templates/webpack.config.js.tmpl index 607d9ef3..b28f0450 100644 --- a/plugins/measure/public/webpack.config.js +++ b/app/plugins/templates/webpack.config.js.tmpl @@ -10,9 +10,7 @@ module.exports = { mode: 'production', context: __dirname, - entry: { - app: ['./app.jsx'] - }, + entry: ${entry_json}, output: { path: path.join(__dirname, './build'), @@ -30,7 +28,7 @@ module.exports = { module: { rules: [ { - test: /\.jsx?$/, + test: /\.jsx?$$/, exclude: /(node_modules|bower_components)/, use: [ { @@ -49,7 +47,7 @@ module.exports = { ], }, { - test: /\.s?css$/, + test: /\.s?css$$/, use: ExtractTextPlugin.extract({ use: 'css-loader!sass-loader' }) diff --git a/app/static/app/js/classes/plugins/ApiFactory.js b/app/static/app/js/classes/plugins/ApiFactory.js index 749b6bfd..39aeb09d 100644 --- a/app/static/app/js/classes/plugins/ApiFactory.js +++ b/app/static/app/js/classes/plugins/ApiFactory.js @@ -46,8 +46,20 @@ export default class ApiFactory{ obj[triggerEventName] = (params, responseCb) => { preTrigger(params, responseCb); + if (responseCb){ + this.events.addListener(`${api.namespace}::${eventName}::Response`, (...args) => { + // Give time to all listeners to receive the replies + // then remove the listener to avoid sending duplicate responses + const curSub = this.events._currentSubscription; + + setTimeout(() => { + curSub.remove(); + }, 0); + + responseCb(...args); + }); + } this.events.emit(`${api.namespace}::${eventName}`, params); - if (responseCb) this.events.addListener(`${api.namespace}::${eventName}::Response`, responseCb); }; } diff --git a/app/static/app/js/components/TaskListItem.jsx b/app/static/app/js/components/TaskListItem.jsx index 37bf24a4..34a43de9 100644 --- a/app/static/app/js/components/TaskListItem.jsx +++ b/app/static/app/js/components/TaskListItem.jsx @@ -514,7 +514,7 @@ class TaskListItem extends React.Component { {actionButtons} - + ); diff --git a/app/static/app/js/components/TaskPluginActionButtons.jsx b/app/static/app/js/components/TaskPluginActionButtons.jsx index 57304ef7..de9d8a8d 100644 --- a/app/static/app/js/components/TaskPluginActionButtons.jsx +++ b/app/static/app/js/components/TaskPluginActionButtons.jsx @@ -6,11 +6,13 @@ import update from 'immutability-helper'; class TaskPluginActionButtons extends React.Component { static defaultProps = { - task: null + task: null, + disabled: false }; static propTypes = { task: PropTypes.object.isRequired, + disabled: PropTypes.bool }; constructor(props){ @@ -24,7 +26,10 @@ class TaskPluginActionButtons extends React.Component { componentDidMount(){ PluginsAPI.Dashboard.triggerAddTaskActionButton({ task: this.props.task - }, ({button, task}) => { + }, (result) => { + if (!result) return; + const {button, task} = result; + // Only process callbacks for // for the current task if (task === this.props.task){ @@ -38,7 +43,7 @@ class TaskPluginActionButtons extends React.Component { render(){ if (this.state.buttons.length > 0){ return ( -
+
{this.state.buttons.map((button, i) =>
{button}
)}
); }else{ diff --git a/app/static/app/js/css/TaskPluginActionButtons.scss b/app/static/app/js/css/TaskPluginActionButtons.scss index 102f39f2..ca8d417f 100644 --- a/app/static/app/js/css/TaskPluginActionButtons.scss +++ b/app/static/app/js/css/TaskPluginActionButtons.scss @@ -12,4 +12,9 @@ & > div{ display: inline-block; } + + &.disabled{ + opacity: 0.65; + pointer-events: none; + } } \ No newline at end of file diff --git a/plugins/.gitignore b/plugins/.gitignore new file mode 100644 index 00000000..6de001d8 --- /dev/null +++ b/plugins/.gitignore @@ -0,0 +1 @@ +webpack.config.js diff --git a/plugins/measure/plugin.py b/plugins/measure/plugin.py index 9576fd0b..cbc67bcc 100644 --- a/plugins/measure/plugin.py +++ b/plugins/measure/plugin.py @@ -6,6 +6,9 @@ class Plugin(PluginBase): def include_js_files(self): return ['main.js'] + def build_jsx_components(self): + return ['app.jsx'] + def api_mount_points(self): return [ MountPoint('task/(?P[^/.]+)/volume', TaskVolume.as_view()) diff --git a/plugins/openaerialmap/load_buttons.js b/plugins/openaerialmap/load_buttons.js new file mode 100644 index 00000000..7119149b --- /dev/null +++ b/plugins/openaerialmap/load_buttons.js @@ -0,0 +1,16 @@ +PluginsAPI.Dashboard.addTaskActionButton([ + 'openaerialmap/build/ShareButton.js', + 'openaerialmap/build/ShareButton.css' + ],function(options, ShareButton){ + var task = options.task; + + if (task.available_assets.indexOf("orthophoto.tif") !== -1){ + console.log("INSTANTIATED"); + + return { + button: React.createElement(ShareButton, {task: task, token: "${token}"}), + task: task + }; + } + } +); \ No newline at end of file diff --git a/plugins/openaerialmap/plugin.py b/plugins/openaerialmap/plugin.py index 66c7b201..5d9eb379 100644 --- a/plugins/openaerialmap/plugin.py +++ b/plugins/openaerialmap/plugin.py @@ -17,12 +17,27 @@ class Plugin(PluginBase): def include_js_files(self): return ['main.js'] + def build_jsx_components(self): + return ['ShareButton.jsx'] + def include_css_files(self): return ['style.css'] def app_mount_points(self): + def load_buttons_cb(request): + if request.user.is_authenticated: + ds = self.get_user_data_store(request.user) + return {'token': ds.get_string('token')} + else: + return False + return [ - MountPoint('$', self.home_view()) + MountPoint('$', self.home_view()), + MountPoint('main.js', self.get_dynamic_script( + 'load_buttons.js', + load_buttons_cb + ) + ) ] def home_view(self): diff --git a/plugins/openaerialmap/public/ShareButton.jsx b/plugins/openaerialmap/public/ShareButton.jsx new file mode 100644 index 00000000..c97cc9c2 --- /dev/null +++ b/plugins/openaerialmap/public/ShareButton.jsx @@ -0,0 +1,37 @@ +import './ShareButton.scss'; +import React from 'react'; +import PropTypes from 'prop-types'; + +module.exports = class ShareButton extends React.Component{ + static defaultProps = { + task: null, + token: "" + }; + + static propTypes = { + task: PropTypes.object.isRequired, + token: PropTypes.string.isRequired // OAM Token + }; + + constructor(props){ + super(props); + + this.state = { + loading: true + }; + } + + handleClick = () => { + console.log("HEY!", this.props.token); + } + + render(){ + return (); + } +} \ No newline at end of file diff --git a/plugins/openaerialmap/public/ShareButton.scss b/plugins/openaerialmap/public/ShareButton.scss new file mode 100644 index 00000000..10eab0d1 --- /dev/null +++ b/plugins/openaerialmap/public/ShareButton.scss @@ -0,0 +1,3 @@ +.oam-share-button{ + +} \ No newline at end of file diff --git a/plugins/openaerialmap/public/main.js b/plugins/openaerialmap/public/main.js deleted file mode 100644 index ee06ef31..00000000 --- a/plugins/openaerialmap/public/main.js +++ /dev/null @@ -1,15 +0,0 @@ -PluginsAPI.Dashboard.addTaskActionButton(function(options){ - - console.log("INVOKED"); - - return { - button: React.createElement("button", { - type: "button", - className: "btn btn-sm btn-primary", - onClick: function(){ - console.log("HEY"); - } - }, React.createElement("i", {className: "oam-icon fa"}, ""), " Share to OAM"), - task: options.task - }; -}); \ No newline at end of file