kopia lustrzana https://github.com/OpenDroneMap/WebODM
Automatic webpack.config.js construction, JS API improvements, OAM ES6 button component, dynamic javascript file support
rodzic
c8904be3e7
commit
78b6c41dd2
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,7 +3,6 @@ import shutil
|
|||
import tempfile
|
||||
import subprocess
|
||||
import os
|
||||
import geojson
|
||||
|
||||
from string import Template
|
||||
|
||||
|
|
|
@ -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())
|
|
@ -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'
|
||||
})
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -514,7 +514,7 @@ class TaskListItem extends React.Component {
|
|||
<ErrorMessage bind={[this, 'actionError']} />
|
||||
{actionButtons}
|
||||
</div>
|
||||
<TaskPluginActionButtons task={task} />
|
||||
<TaskPluginActionButtons task={task} disabled={disabled} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -12,4 +12,9 @@
|
|||
& > div{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.disabled{
|
||||
opacity: 0.65;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
webpack.config.js
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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):
|
||||
|
|
|
@ -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>);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.oam-share-button{
|
||||
|
||||
}
|
|
@ -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
|
||||
};
|
||||
});
|
Ładowanie…
Reference in New Issue