Added processing node removed signal, lightnin plugin dashboard, login

pull/580/head
Piero Toffanin 2018-12-31 14:35:55 -05:00
rodzic 37a23ddcc5
commit 9275325fe3
9 zmienionych plików z 370 dodań i 19 usunięć

Wyświetl plik

@ -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"])

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -20,6 +20,4 @@ PluginsAPI.App.ready([
<div data-lightning-panel data-api-key="{{ api_key }}"></div>
</div>
</div>
<hr/>
{% endblock %}