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_completed = django.dispatch.Signal(providing_args=["task_id"])
|
||||||
task_removing = 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"])
|
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 import pending_actions
|
||||||
from app.models import Project, Task
|
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 nodeodm.models import ProcessingNode, OFFLINE_MINUTES
|
||||||
from .classes import BootTestCase
|
from .classes import BootTestCase
|
||||||
|
|
||||||
|
@ -364,8 +366,11 @@ class TestApi(BootTestCase):
|
||||||
client.login(username="testsuperuser", password="test1234")
|
client.login(username="testsuperuser", password="test1234")
|
||||||
|
|
||||||
# Can delete a processing node as super user
|
# Can delete a processing node as super user
|
||||||
res = client.delete('/api/processingnodes/{}/'.format(pnode.id))
|
# and a signal is sent when a processing node is deleted
|
||||||
self.assertTrue(res.status_code, status.HTTP_200_OK)
|
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
|
# Can create a processing node as super user
|
||||||
res = client.post('/api/processingnodes/', {'hostname': 'localhost', 'port':'1000'})
|
res = client.post('/api/processingnodes/', {'hostname': 'localhost', 'port':'1000'})
|
||||||
|
|
|
@ -208,6 +208,14 @@ class ProcessingNode(models.Model):
|
||||||
else:
|
else:
|
||||||
raise ProcessingError("Unknown response: {}".format(result))
|
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:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
('view_processingnode', 'Can view processing node'),
|
('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.shortcuts import render
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.views.decorators.http import require_POST
|
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):
|
class Plugin(PluginBase):
|
||||||
def main_menu(self):
|
def main_menu(self):
|
||||||
return [Menu("Lightning Network", self.public_url(""), "fa fa-bolt fa-fw")]
|
return [Menu("Lightning Network", self.public_url(""), "fa fa-bolt fa-fw")]
|
||||||
|
@ -22,19 +34,77 @@ class Plugin(PluginBase):
|
||||||
@login_required
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def save_api_key(request):
|
def save_api_key(request):
|
||||||
|
|
||||||
api_key = request.POST.get('api_key')
|
api_key = request.POST.get('api_key')
|
||||||
if api_key is None:
|
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 = UserDataStore('lightning', request.user)
|
||||||
ds.set_string('api_key', api_key)
|
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 [
|
return [
|
||||||
MountPoint('$', main),
|
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 React from 'react';
|
||||||
import './Login.scss';
|
import './Login.scss';
|
||||||
|
import ErrorMessage from 'webodm/components/ErrorMessage';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
@ -7,30 +8,106 @@ export default class Login extends React.Component {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
};
|
};
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
onLogin: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
super(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(){
|
render(){
|
||||||
return (<div className="lightning-login">
|
return (<div className="lightning-login">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-6 col-md-offset-3 col-sm-12">
|
<div className="col-md-6 col-md-offset-3 col-sm-12">
|
||||||
|
<ErrorMessage bind={[this, "error"]} />
|
||||||
<div className="form-group text-left">
|
<div className="form-group text-left">
|
||||||
<input id="next" name="next" type="hidden" value="" />
|
<input id="next" name="next" type="hidden" value="" />
|
||||||
<p>
|
<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>
|
||||||
<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>
|
</p>
|
||||||
<div style={{float: 'right'}} >
|
<div style={{float: 'right'}} >
|
||||||
<a href="https://webodm.net/register" target="_blank">Don't have an account?</a><br/>
|
<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>
|
<a href="https://webodm.net/reset" target="_blank">Forgot password?</a>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Login from './Login';
|
import Login from './Login';
|
||||||
|
import Dashboard from './Dashboard';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
export default class LightningPanel extends React.Component {
|
export default class LightningPanel extends React.Component {
|
||||||
|
@ -14,16 +15,34 @@ export default class LightningPanel extends React.Component {
|
||||||
constructor(props){
|
constructor(props){
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
apiKey: props.apiKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLogin = (apiKey) => {
|
||||||
|
this.setState({ apiKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLogout = () => {
|
||||||
|
this.setState({ apiKey: ""});
|
||||||
}
|
}
|
||||||
|
|
||||||
render(){
|
render(){
|
||||||
|
const { apiKey } = this.state;
|
||||||
|
|
||||||
return (<div className="plugin-lightning">
|
return (<div className="plugin-lightning">
|
||||||
<h4><i className="fa fa-bolt"/> Lightning Network</h4>
|
{ !apiKey ?
|
||||||
|
<div>
|
||||||
Lightning is a service that allows you to quickly process small and large datasets using high performance servers in the cloud.
|
<h4><i className="fa fa-bolt"/> Lightning Network</h4>
|
||||||
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.
|
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 />
|
<Login onLogin={this.handleLogin} />
|
||||||
|
</div> :
|
||||||
|
<div>
|
||||||
|
<Dashboard apiKey={apiKey} onLogout={this.handleLogout} />
|
||||||
|
</div>}
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,6 +20,4 @@ PluginsAPI.App.ready([
|
||||||
<div data-lightning-panel data-api-key="{{ api_key }}"></div>
|
<div data-lightning-panel data-api-key="{{ api_key }}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr/>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
Ładowanie…
Reference in New Issue