kopia lustrzana https://github.com/OpenDroneMap/WebODM
Added processing node removed signal, lightnin plugin dashboard, login
rodzic
37a23ddcc5
commit
9275325fe3
|
@ -3,3 +3,5 @@ import django.dispatch
|
|||
task_completed = django.dispatch.Signal(providing_args=["task_id"])
|
||||
task_removing = django.dispatch.Signal(providing_args=["task_id"])
|
||||
task_removed = django.dispatch.Signal(providing_args=["task_id"])
|
||||
|
||||
processing_node_removed = django.dispatch.Signal(providing_args=["processing_node_id"])
|
||||
|
|
|
@ -9,6 +9,8 @@ from rest_framework_jwt.settings import api_settings
|
|||
|
||||
from app import pending_actions
|
||||
from app.models import Project, Task
|
||||
from app.plugins.signals import processing_node_removed
|
||||
from app.tests.utils import catch_signal
|
||||
from nodeodm.models import ProcessingNode, OFFLINE_MINUTES
|
||||
from .classes import BootTestCase
|
||||
|
||||
|
@ -364,8 +366,11 @@ class TestApi(BootTestCase):
|
|||
client.login(username="testsuperuser", password="test1234")
|
||||
|
||||
# Can delete a processing node as super user
|
||||
res = client.delete('/api/processingnodes/{}/'.format(pnode.id))
|
||||
self.assertTrue(res.status_code, status.HTTP_200_OK)
|
||||
# and a signal is sent when a processing node is deleted
|
||||
with catch_signal(processing_node_removed) as h1:
|
||||
res = client.delete('/api/processingnodes/{}/'.format(pnode.id))
|
||||
self.assertTrue(res.status_code, status.HTTP_200_OK)
|
||||
h1.assert_called_once_with(sender=ProcessingNode, processing_node_id=pnode.id, signal=processing_node_removed)
|
||||
|
||||
# Can create a processing node as super user
|
||||
res = client.post('/api/processingnodes/', {'hostname': 'localhost', 'port':'1000'})
|
||||
|
|
|
@ -208,6 +208,14 @@ class ProcessingNode(models.Model):
|
|||
else:
|
||||
raise ProcessingError("Unknown response: {}".format(result))
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
pnode_id = self.id
|
||||
super(ProcessingNode, self).delete(using, keep_parents)
|
||||
|
||||
from app.plugins import signals as plugin_signals
|
||||
plugin_signals.processing_node_removed.send_robust(sender=self.__class__, processing_node_id=pnode_id)
|
||||
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_processingnode', 'Can view processing node'),
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
from django.http import HttpResponse
|
||||
import json
|
||||
|
||||
from app.plugins import PluginBase, Menu, MountPoint, UserDataStore
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpResponse
|
||||
from guardian.shortcuts import get_objects_for_user, assign_perm
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
from app.plugins import GlobalDataStore, logger
|
||||
from app.plugins import PluginBase, Menu, MountPoint, UserDataStore, signals
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from nodeodm.models import ProcessingNode
|
||||
from app.api.processingnodes import ProcessingNodeSerializer
|
||||
|
||||
def JsonResponse(dict):
|
||||
return HttpResponse(json.dumps(dict), content_type='application/json')
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def main_menu(self):
|
||||
return [Menu("Lightning Network", self.public_url(""), "fa fa-bolt fa-fw")]
|
||||
|
@ -22,19 +34,77 @@ class Plugin(PluginBase):
|
|||
@login_required
|
||||
@require_POST
|
||||
def save_api_key(request):
|
||||
|
||||
api_key = request.POST.get('api_key')
|
||||
if api_key is None:
|
||||
return HttpResponse({'error': 'api_key is required'}, content_type='application/json')
|
||||
return JsonResponse({'error': 'api_key is required'})
|
||||
|
||||
ds = UserDataStore('lightning', request.user)
|
||||
ds.set_string('api_key', api_key)
|
||||
|
||||
return HttpResponse({'success': True}, content_type='application/json')
|
||||
return JsonResponse({'success': True})
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def sync_processing_node(request):
|
||||
ds = GlobalDataStore('lightning')
|
||||
|
||||
hostname = request.POST.get('hostname')
|
||||
port = int(request.POST.get('port'))
|
||||
token = request.POST.get('token')
|
||||
|
||||
if hostname is not None and port is not None and token is not None:
|
||||
nodes = get_objects_for_user(request.user, 'view_processingnode', ProcessingNode,
|
||||
accept_global_perms=False)
|
||||
matches = [n for n in nodes if n.hostname == hostname and n.port == port and n.token == token]
|
||||
if len(matches) == 0:
|
||||
# Add
|
||||
node = ProcessingNode.objects.create(hostname=hostname, port=port, token=token)
|
||||
assign_perm('view_processingnode', request.user, node)
|
||||
assign_perm('change_processingnode', request.user, node)
|
||||
assign_perm('delete_processingnode', request.user, node)
|
||||
|
||||
# Keep track of lightning node IDs
|
||||
lightning_nodes = ds.get_json('nodes', [])
|
||||
lightning_nodes.append(node.id)
|
||||
ds.set_json('nodes', lightning_nodes)
|
||||
|
||||
return get_processing_nodes(request)
|
||||
else:
|
||||
# Already added
|
||||
return get_processing_nodes(request)
|
||||
else:
|
||||
return JsonResponse({'error': 'Invalid call (params missing)'})
|
||||
|
||||
@login_required
|
||||
def get_processing_nodes(request):
|
||||
ds = GlobalDataStore('lightning')
|
||||
|
||||
nodes = get_objects_for_user(request.user, 'view_processingnode', ProcessingNode,
|
||||
accept_global_perms=False)
|
||||
lightning_node_ids = ds.get_json("nodes", [])
|
||||
|
||||
nodes = [n for n in nodes if n.id in lightning_node_ids]
|
||||
serializer = ProcessingNodeSerializer(nodes, many=True)
|
||||
|
||||
return JsonResponse(serializer.data)
|
||||
|
||||
|
||||
return [
|
||||
MountPoint('$', main),
|
||||
MountPoint('save_api_key$', save_api_key)
|
||||
MountPoint('save_api_key$', save_api_key),
|
||||
MountPoint('sync_processing_node$', sync_processing_node),
|
||||
MountPoint('get_processing_nodes$', get_processing_nodes),
|
||||
]
|
||||
|
||||
|
||||
@receiver(signals.processing_node_removed, dispatch_uid="lightning_on_processing_node_removed")
|
||||
def lightning_on_processing_node_removed(sender, processing_node_id, **kwargs):
|
||||
ds = GlobalDataStore('lightning')
|
||||
|
||||
node_ids = ds.get_json('nodes', [])
|
||||
try:
|
||||
node_ids.remove(processing_node_id)
|
||||
logger.info("Removing lightning node {}".format(str(processing_node_id)))
|
||||
ds.set_json('nodes', node_ids)
|
||||
except ValueError:
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
import React from 'react';
|
||||
import ErrorMessage from 'webodm/components/ErrorMessage';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Dashboard.scss';
|
||||
import $ from 'jquery';
|
||||
|
||||
export default class Dashboard extends React.Component {
|
||||
static defaultProps = {
|
||||
};
|
||||
static propTypes = {
|
||||
apiKey: PropTypes.string.isRequired,
|
||||
onLogout: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: "",
|
||||
loading: true,
|
||||
loadingMessage: "Loading dashboard...",
|
||||
user: null,
|
||||
nodes: [],
|
||||
syncingNodes: false
|
||||
}
|
||||
}
|
||||
|
||||
apiUrl = url => {
|
||||
return `http://192.168.2.253:5000${url}?api_key=${this.props.apiKey}`;
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
$.get(this.apiUrl('/r/user')).done(json => {
|
||||
if (json.balance !== undefined){
|
||||
this.setState({ loading: false, user: json });
|
||||
this.handleSyncProcessingNode();
|
||||
}else{
|
||||
this.setState({ loading: false, error: `Cannot load dashboard. Invalid response from server: ${JSON.stringify(json)}. Are you running the latest version of WebODM?` });
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
this.setState({ loading: false, error: `Cannot load dashboard. Please make sure you are connected to the internet, or try again in an hour.`});
|
||||
});
|
||||
}
|
||||
|
||||
handleSyncProcessingNode = () => {
|
||||
if (!this.state.user) return;
|
||||
const { node, tokens } = this.state.user;
|
||||
if (!node || !tokens) return;
|
||||
|
||||
this.setState({syncingNodes: true, nodes: []});
|
||||
|
||||
$.post('sync_processing_node', {
|
||||
hostname: node.hostname,
|
||||
port: node.port,
|
||||
token: tokens[0].id
|
||||
}).done(json => {
|
||||
console.log(json);
|
||||
if (json.error){
|
||||
this.setState({error: json.error});
|
||||
}else{
|
||||
this.setState({nodes: json});
|
||||
}
|
||||
})
|
||||
.fail(e => {
|
||||
this.setState({error: `Cannot sync nodes: ${JSON.stringify(e)}. Are you connected to the internet?`});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({syncingNodes: false});
|
||||
});
|
||||
}
|
||||
|
||||
handeLogout = () => {
|
||||
this.setState({loading: true, loadingMessage: "Logging out..."});
|
||||
|
||||
$.post("save_api_key", {
|
||||
api_key: ""
|
||||
}).done(json => {
|
||||
if (!json.success){
|
||||
this.setState({error: `Cannot logout: ${JSON.stringify(json)}`});
|
||||
}
|
||||
this.setState({loading: false});
|
||||
this.props.onLogout();
|
||||
}).fail(e => {
|
||||
this.setState({loading: false, error: `Cannot logout: ${JSON.stringify(e)}`});
|
||||
});
|
||||
}
|
||||
|
||||
handleBuyCredits = () => {
|
||||
window.open("https://webodm.net/dashboard?bc=0");
|
||||
}
|
||||
|
||||
handleOpenDashboard = () => {
|
||||
window.open("https://webodm.net/dashboard");
|
||||
}
|
||||
|
||||
render(){
|
||||
const { loading, loadingMessage, user, syncingNodes, nodes } = this.state;
|
||||
|
||||
return (<div className="lightning-dashboard">
|
||||
<ErrorMessage bind={[this, "error"]} />
|
||||
|
||||
{ loading ?
|
||||
<div className="text-center loading">{ loadingMessage } <i className="fa fa-spin fa-circle-o-notch"></i></div> :
|
||||
<div>
|
||||
{ user ?
|
||||
<div>
|
||||
<div className="balance">Balance: <strong>{ user.balance }</strong> credits <button className="btn btn-primary btn-sm" onClick={this.handleBuyCredits}><i className="fa fa-shopping-cart"></i> Buy Credits</button></div>
|
||||
<h4>Hello, <a href="javascript:void(0)" onClick={this.handleOpenDashboard}>{ user.email }</a></h4>
|
||||
|
||||
<div className="lightning-nodes">
|
||||
<h5>Synced Nodes</h5>
|
||||
{ syncingNodes ? <i className="fa fa-spin fa-refresh"></i> :
|
||||
<div>
|
||||
<ul>
|
||||
{nodes.map(n =>
|
||||
<li key={n.id}><i className="fa fa-laptop"></i> <a href={`/processingnode/${n.id}/`}>{n.hostname}:{n.port}</a></li>
|
||||
)}
|
||||
</ul>
|
||||
<button className="btn btn-sm btn-default" onClick={this.handleSyncProcessingNode}><i className="fa fa-refresh"></i> Resync</button>
|
||||
</div> }
|
||||
</div>
|
||||
|
||||
<div className="estimator">
|
||||
<h5>Cost Calculator</h5>
|
||||
Drag & drop some images below to estimate the number of credits required to process them with lightning.
|
||||
</div>
|
||||
|
||||
<div className="buttons text-right">
|
||||
<hr/>
|
||||
<button className="btn btn-sm btn-primary logout" onClick={this.handeLogout}>
|
||||
<i className="fa fa-power-off"></i> Logout
|
||||
</button>
|
||||
</div>
|
||||
</div> : ""}
|
||||
</div>}
|
||||
</div>);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
.plugin-lightning .lightning-dashboard{
|
||||
.loading{
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.balance{
|
||||
float: right;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.buttons > button{
|
||||
clear: both;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.lightning-nodes{
|
||||
margin-top: 12px;
|
||||
ul{
|
||||
padding-left: 0;
|
||||
}
|
||||
li{
|
||||
list-style-type: none;
|
||||
}
|
||||
}
|
||||
|
||||
.estimator{
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
h5{
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import './Login.scss';
|
||||
import ErrorMessage from 'webodm/components/ErrorMessage';
|
||||
import PropTypes from 'prop-types';
|
||||
import $ from 'jquery';
|
||||
|
||||
|
@ -7,30 +8,106 @@ export default class Login extends React.Component {
|
|||
static defaultProps = {
|
||||
};
|
||||
static propTypes = {
|
||||
onLogin: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: "",
|
||||
loggingIn: false,
|
||||
email: "",
|
||||
password: ""
|
||||
}
|
||||
}
|
||||
|
||||
handleEmailChange = (e) => {
|
||||
this.setState({email: e.target.value});
|
||||
}
|
||||
|
||||
handlePasswordChange = (e) => {
|
||||
this.setState({password: e.target.value});
|
||||
}
|
||||
|
||||
handleLogin = () => {
|
||||
this.setState({loggingIn: true});
|
||||
|
||||
$.post("http://192.168.2.253:5000/r/auth/login",//"https://webodm.net/r/auth/login",
|
||||
{
|
||||
username: this.state.email,
|
||||
password: this.state.password
|
||||
}
|
||||
).done(json => {
|
||||
if (json.api_key){
|
||||
this.saveApiKey(json.api_key, (err) => {
|
||||
this.setState({loggingIn: false});
|
||||
|
||||
if (!err){
|
||||
this.props.onLogin(json.api_key);
|
||||
}else{
|
||||
this.setState({ error: err.message });
|
||||
}
|
||||
});
|
||||
}else if (json.message){
|
||||
this.setState({ loggingIn: false, error: json.message });
|
||||
}else{
|
||||
this.setState({ loggingIn: false, error: "Cannot login. Invalid response: " + JSON.stringify(json)});
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
this.setState({loggingIn: false, error: "Cannot login. Please make sure you are connected to the internet, or try again in an hour."});
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyPress = (e) => {
|
||||
if (e.key === 'Enter'){
|
||||
this.handleLogin();
|
||||
}
|
||||
}
|
||||
|
||||
saveApiKey = (api_key, cb) => {
|
||||
$.post("save_api_key", {
|
||||
api_key: api_key
|
||||
}).done(json => {
|
||||
if (!json.success){
|
||||
cb(new Error(`Cannot save API key: ${JSON.stringify(json)}`));
|
||||
}else cb();
|
||||
}).fail(e => {
|
||||
cb(new Error(`Cannot save API key: ${JSON.stringify(e)}`));
|
||||
});
|
||||
}
|
||||
|
||||
render(){
|
||||
return (<div className="lightning-login">
|
||||
<div className="row">
|
||||
<div className="col-md-6 col-md-offset-3 col-sm-12">
|
||||
<ErrorMessage bind={[this, "error"]} />
|
||||
<div className="form-group text-left">
|
||||
<input id="next" name="next" type="hidden" value="" />
|
||||
<p>
|
||||
<label htmlFor="email">Email Address</label> <input className="form-control" id="email" name="email" required="" type="text" value="" />
|
||||
<label htmlFor="email">E-mail Address</label>
|
||||
<input className="form-control" id="email" name="email" required=""
|
||||
type="text" value={this.state.email}
|
||||
onChange={this.handleEmailChange}
|
||||
onKeyPress={this.handleKeyPress} />
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="password">Password</label> <input className="form-control" id="password" name="password" required="" type="password" value="" />
|
||||
<label htmlFor="password">Password</label>
|
||||
<input className="form-control" id="password" name="password" required=""
|
||||
type="password" value={this.state.password}
|
||||
onChange={this.handlePasswordChange}
|
||||
onKeyPress={this.handleKeyPress} />
|
||||
</p>
|
||||
<div style={{float: 'right'}} >
|
||||
<a href="https://webodm.net/register" target="_blank">Don't have an account?</a><br/>
|
||||
<a href="https://webodm.net/reset" target="_blank">Forgot password?</a>
|
||||
</div>
|
||||
<p><button className="btn btn-primary"><i className="fa fa-lock"></i> Login and Sync</button></p>
|
||||
<p><button className="btn btn-primary" onClick={this.handleLogin} disabled={this.state.loggingIn}>
|
||||
{this.state.loggingIn ?
|
||||
<span><i className="fa fa-spin fa-circle-o-notch"></i></span> :
|
||||
<span><i className="fa fa-lock"></i> Login and Sync</span>}
|
||||
</button></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Login from './Login';
|
||||
import Dashboard from './Dashboard';
|
||||
import $ from 'jquery';
|
||||
|
||||
export default class LightningPanel extends React.Component {
|
||||
|
@ -14,16 +15,34 @@ export default class LightningPanel extends React.Component {
|
|||
constructor(props){
|
||||
super(props);
|
||||
|
||||
|
||||
this.state = {
|
||||
apiKey: props.apiKey
|
||||
}
|
||||
}
|
||||
|
||||
handleLogin = (apiKey) => {
|
||||
this.setState({ apiKey });
|
||||
}
|
||||
|
||||
handleLogout = () => {
|
||||
this.setState({ apiKey: ""});
|
||||
}
|
||||
|
||||
render(){
|
||||
const { apiKey } = this.state;
|
||||
|
||||
return (<div className="plugin-lightning">
|
||||
<h4><i className="fa fa-bolt"/> Lightning Network</h4>
|
||||
|
||||
Lightning is a service that allows you to quickly process small and large datasets using high performance servers in the cloud.
|
||||
Below you can enter your <a href="https://webodm.net" target="_blank">webodm.net</a> credentials to sync your account and automatically setup a new processing node. If you don't have an account, you can <a href="https://webodm.net/register" target="_blank">register</a> for free.
|
||||
|
||||
<Login />
|
||||
{ !apiKey ?
|
||||
<div>
|
||||
<h4><i className="fa fa-bolt"/> Lightning Network</h4>
|
||||
Lightning is a service that allows you to quickly process small and large datasets using high performance servers in the cloud.
|
||||
Below you can enter your <a href="https://webodm.net" target="_blank">webodm.net</a> credentials to sync your account and automatically setup a new processing node. If you don't have an account, you can <a href="https://webodm.net/register" target="_blank">register</a> for free.
|
||||
<Login onLogin={this.handleLogin} />
|
||||
</div> :
|
||||
<div>
|
||||
<Dashboard apiKey={apiKey} onLogout={this.handleLogout} />
|
||||
</div>}
|
||||
</div>);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,4 @@ PluginsAPI.App.ready([
|
|||
<div data-lightning-panel data-api-key="{{ api_key }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
{% endblock %}
|
Ładowanie…
Reference in New Issue