Automatic webpack.config.js construction, JS API improvements, OAM ES6 button component, dynamic javascript file support

pull/492/head
Piero Toffanin 2018-07-26 15:14:48 -04:00
rodzic c8904be3e7
commit 78b6c41dd2
17 zmienionych plików z 179 dodań i 30 usunięć

Wyświetl plik

@ -36,7 +36,7 @@ WORKDIR /webodm/nodeodm/external/node-OpenDroneMap
RUN npm install --quiet RUN npm install --quiet
WORKDIR /webodm 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 python manage.py collectstatic --noinput
RUN rm /webodm/webodm/secret_key.py RUN rm /webodm/webodm/secret_key.py

Wyświetl plik

@ -358,7 +358,7 @@ pip install -r requirements.txt
sudo npm install -g webpack sudo npm install -g webpack
sudo npm install -g webpack-cli sudo npm install -g webpack-cli
npm install npm install
webpack webpack --mode production
python manage.py collectstatic --noinput python manage.py collectstatic --noinput
chmod +x start.sh && ./start.sh --no-gunicorn chmod +x start.sh && ./start.sh --no-gunicorn
``` ```

Wyświetl plik

@ -7,6 +7,9 @@ import django
import json import json
from django.conf.urls import url from django.conf.urls import url
from functools import reduce from functools import reduce
from string import Template
from django.http import HttpResponse
from webodm import settings from webodm import settings
@ -18,9 +21,36 @@ def register_plugins():
# Check for package.json in public directory # Check for package.json in public directory
# and run npm install if needed # and run npm install if needed
if plugin.path_exists("public/package.json") and not plugin.path_exists("public/node_modules"): 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")) 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) # 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"): if plugin.path_exists("public/webpack.config.js") and not plugin.path_exists("public/build"):
logger.info("Running webpack for {}".format(plugin.get_name())) 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")) 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): def versionToInt(version):
""" """
Converts a WebODM version string (major.minor.build) to a integer value Converts a WebODM version string (major.minor.build) to a integer value

Wyświetl plik

@ -3,7 +3,6 @@ import shutil
import tempfile import tempfile
import subprocess import subprocess
import os import os
import geojson
from string import Template from string import Template

Wyświetl plik

@ -82,6 +82,15 @@ class PluginBase(ABC):
""" """
return [] 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): def main_menu(self):
""" """
Should be overriden by plugins that want to add Should be overriden by plugins that want to add
@ -106,5 +115,20 @@ class PluginBase(ABC):
""" """
return [] 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): def __str__(self):
return "[{}]".format(self.get_module_name()) return "[{}]".format(self.get_module_name())

Wyświetl plik

@ -10,9 +10,7 @@ module.exports = {
mode: 'production', mode: 'production',
context: __dirname, context: __dirname,
entry: { entry: ${entry_json},
app: ['./app.jsx']
},
output: { output: {
path: path.join(__dirname, './build'), path: path.join(__dirname, './build'),
@ -30,7 +28,7 @@ module.exports = {
module: { module: {
rules: [ rules: [
{ {
test: /\.jsx?$/, test: /\.jsx?$$/,
exclude: /(node_modules|bower_components)/, exclude: /(node_modules|bower_components)/,
use: [ use: [
{ {
@ -49,7 +47,7 @@ module.exports = {
], ],
}, },
{ {
test: /\.s?css$/, test: /\.s?css$$/,
use: ExtractTextPlugin.extract({ use: ExtractTextPlugin.extract({
use: 'css-loader!sass-loader' use: 'css-loader!sass-loader'
}) })

Wyświetl plik

@ -46,8 +46,20 @@ export default class ApiFactory{
obj[triggerEventName] = (params, responseCb) => { obj[triggerEventName] = (params, responseCb) => {
preTrigger(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); this.events.emit(`${api.namespace}::${eventName}`, params);
if (responseCb) this.events.addListener(`${api.namespace}::${eventName}::Response`, responseCb);
}; };
} }

Wyświetl plik

@ -514,7 +514,7 @@ class TaskListItem extends React.Component {
<ErrorMessage bind={[this, 'actionError']} /> <ErrorMessage bind={[this, 'actionError']} />
{actionButtons} {actionButtons}
</div> </div>
<TaskPluginActionButtons task={task} /> <TaskPluginActionButtons task={task} disabled={disabled} />
</div> </div>
); );

Wyświetl plik

@ -6,11 +6,13 @@ import update from 'immutability-helper';
class TaskPluginActionButtons extends React.Component { class TaskPluginActionButtons extends React.Component {
static defaultProps = { static defaultProps = {
task: null task: null,
disabled: false
}; };
static propTypes = { static propTypes = {
task: PropTypes.object.isRequired, task: PropTypes.object.isRequired,
disabled: PropTypes.bool
}; };
constructor(props){ constructor(props){
@ -24,7 +26,10 @@ class TaskPluginActionButtons extends React.Component {
componentDidMount(){ componentDidMount(){
PluginsAPI.Dashboard.triggerAddTaskActionButton({ PluginsAPI.Dashboard.triggerAddTaskActionButton({
task: this.props.task task: this.props.task
}, ({button, task}) => { }, (result) => {
if (!result) return;
const {button, task} = result;
// Only process callbacks for // Only process callbacks for
// for the current task // for the current task
if (task === this.props.task){ if (task === this.props.task){
@ -38,7 +43,7 @@ class TaskPluginActionButtons extends React.Component {
render(){ render(){
if (this.state.buttons.length > 0){ if (this.state.buttons.length > 0){
return ( return (
<div className="row plugin-action-buttons"> <div className={"row plugin-action-buttons " + (this.props.disabled ? "disabled" : "")}>
{this.state.buttons.map((button, i) => <div key={i}>{button}</div>)} {this.state.buttons.map((button, i) => <div key={i}>{button}</div>)}
</div>); </div>);
}else{ }else{

Wyświetl plik

@ -12,4 +12,9 @@
& > div{ & > div{
display: inline-block; display: inline-block;
} }
&.disabled{
opacity: 0.65;
pointer-events: none;
}
} }

1
plugins/.gitignore vendored 100644
Wyświetl plik

@ -0,0 +1 @@
webpack.config.js

Wyświetl plik

@ -6,6 +6,9 @@ class Plugin(PluginBase):
def include_js_files(self): def include_js_files(self):
return ['main.js'] return ['main.js']
def build_jsx_components(self):
return ['app.jsx']
def api_mount_points(self): def api_mount_points(self):
return [ return [
MountPoint('task/(?P<pk>[^/.]+)/volume', TaskVolume.as_view()) MountPoint('task/(?P<pk>[^/.]+)/volume', TaskVolume.as_view())

Wyświetl plik

@ -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
};
}
}
);

Wyświetl plik

@ -17,12 +17,27 @@ class Plugin(PluginBase):
def include_js_files(self): def include_js_files(self):
return ['main.js'] return ['main.js']
def build_jsx_components(self):
return ['ShareButton.jsx']
def include_css_files(self): def include_css_files(self):
return ['style.css'] return ['style.css']
def app_mount_points(self): 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 [ 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): def home_view(self):

Wyświetl plik

@ -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 (<button
onClick={this.handleClick}
className="btn btn-sm btn-primary">
{this.state.loading
? <i className="fa fa-circle-o-notch fa-spin fa-fw"></i>
: [<i className="oam-icon fa"></i>, "Share To OAM"]}
</button>);
}
}

Wyświetl plik

@ -0,0 +1,3 @@
.oam-share-button{
}

Wyświetl plik

@ -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
};
});