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
|
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
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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())
|
|
@ -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'
|
||||||
})
|
})
|
|
@ -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);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -12,4 +12,9 @@
|
||||||
& > div{
|
& > div{
|
||||||
display: inline-block;
|
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):
|
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())
|
||||||
|
|
|
@ -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):
|
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):
|
||||||
|
|
|
@ -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