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
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

Wyświetl plik

@ -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
```

Wyświetl plik

@ -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

Wyświetl plik

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

Wyświetl plik

@ -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())

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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 (
<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>)}
</div>);
}else{

Wyświetl plik

@ -12,4 +12,9 @@
& > div{
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):
return ['main.js']
def build_jsx_components(self):
return ['app.jsx']
def api_mount_points(self):
return [
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):
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):

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