Merge pull request #1122 from HeDo88TH/add-dronedb-integration

Add DroneDB integration (download & share)
pull/1132/head
Piero Toffanin 2022-01-25 10:34:21 -05:00 zatwierdzone przez GitHub
commit 6a16b1593c
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
27 zmienionych plików z 1790 dodań i 1 usunięć

3
.gitignore vendored
Wyświetl plik

@ -74,6 +74,7 @@ target/
.python-version
# celery beat schedule file
celerybeat-schedule.db
celerybeat-schedule
celerybeat.pid
@ -101,4 +102,4 @@ package-lock.json
# Debian builds
dpkg/build
dpkg/deb
dpkg/deb

Wyświetl plik

@ -0,0 +1,5 @@
<h1 align="center">DroneDB</h1>
DroneDB is a WebODM add-on that enables you to import and export your files to DroneDB.

Wyświetl plik

@ -0,0 +1 @@
from .plugin import *

Wyświetl plik

@ -0,0 +1,432 @@
from genericpath import isfile
import importlib
import json
from posixpath import join
import time
import requests
import os
from os import listdir, path
from app import models, pending_actions
from app.plugins.views import TaskView
from app.plugins.worker import run_function_async, task
from app.plugins import get_current_plugin
from app.models import ImageUpload
from app.plugins import GlobalDataStore, get_site_settings, signals as plugin_signals
from coreplugins.dronedb.ddb import DEFAULT_HUB_URL, DroneDB, parse_url, verify_url
from django.dispatch import receiver
from worker.celery import app
from rest_framework.response import Response
from rest_framework import status
VALID_IMAGE_EXTENSIONS = ['.tiff', '.tif', '.png', '.jpeg', '.jpg']
def is_valid(file):
_, file_extension = path.splitext(file)
return file_extension.lower() in VALID_IMAGE_EXTENSIONS or file == 'gcp_list.txt' or file == 'geo.txt'
def get_settings(request):
ds = get_current_plugin().get_user_data_store(request.user)
registry_url = ds.get_string('registry_url') or DEFAULT_HUB_URL
username = ds.get_string('username') or None
password = ds.get_string('password') or None
token = ds.get_string('token') or None
return registry_url, username, password, token
def update_token(request, token):
ds = get_current_plugin().get_user_data_store(request.user)
ds.set_string('token', token)
def get_ddb(request):
registry_url, username, password, token = get_settings(request)
if registry_url == None or username == None or password == None:
raise ValueError('Credentials must be set.')
return DroneDB(registry_url, username, password, token, lambda token: update_token(request, token))
def to_web_protocol(registry_url):
return registry_url.replace('ddb+unsafe://', 'http://').replace('ddb://', 'https://').rstrip('/')
class CheckCredentialsTaskView(TaskView):
def post(self, request):
# Read form data
hub_url = request.data.get('hubUrl', None)
username = request.data.get('userName', None)
password = request.data.get('password', None)
# Make sure both values are set
if hub_url == None or username == None or password == None:
return Response({'error': 'All fields must be set.'}, status=status.HTTP_400_BAD_REQUEST)
try:
ddb = DroneDB(hub_url, username, password)
return Response({'success': ddb.login()}, status=status.HTTP_200_OK)
except(Exception) as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
class OrganizationsTaskView(TaskView):
def get(self, request):
try:
ddb = get_ddb(request)
orgs = ddb.get_organizations()
return Response(orgs, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
class DatasetsTaskView(TaskView):
def get(self, request, org=None):
if org == None:
return Response({'error': 'Organization must be set.'}, status=status.HTTP_400_BAD_REQUEST)
try:
ddb = get_ddb(request)
dss = ddb.get_datasets(org)
return Response(dss, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
class FoldersTaskView(TaskView):
def get(self, request, org=None, ds=None):
if org == None or ds == None:
return Response({'error': 'Organization and dataset must be set.'}, status=status.HTTP_400_BAD_REQUEST)
try:
ddb = get_ddb(request)
folders = ddb.get_folders(org, ds)
return Response(folders, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
class VerifyUrlTaskView(TaskView):
def post(self, request):
# Read form data
url = request.data.get('url', None)
if url == None:
return Response({'error': 'Url must be set.'}, status=status.HTTP_400_BAD_REQUEST)
_, username, password, _ = get_settings(request)
try:
result, org, ds, folder, count, size = verify_url(url, username, password).values()
if (not result):
return Response({'error': 'Invalid url.'}, status=status.HTTP_400_BAD_REQUEST)
return Response({'count': count, 'success': result, 'ds' : ds, 'org': org, 'folder': folder or None, 'size': size}
if org else {'success': False}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
class InfoTaskView(TaskView):
def get(self, request):
registry_url, username, _, _ = get_settings(request)
return Response({ 'hubUrl': registry_url, 'username': username }, status=status.HTTP_200_OK)
class ImportDatasetTaskView(TaskView):
def post(self, request, project_pk=None, pk=None):
task = self.get_and_check_task(request, pk)
# Read form data
ddb_url = request.data.get('ddb_url', None)
if ddb_url == None:
return Response({'error': 'DroneDB url must be set.'}, status=status.HTTP_400_BAD_REQUEST)
registry_url, orgSlug, dsSlug, folder = parse_url(ddb_url).values()
_, username, password, token = get_settings(request)
ddb = DroneDB(registry_url, username, password, token, lambda token: update_token(request, token))
# Get the files from the folder
rawfiles = ddb.get_files_list(orgSlug, dsSlug, folder)
files = [file for file in rawfiles if is_valid(file['path'])]
# Verify that the folder url is valid
if len(files) == 0:
return Response({'error': 'Empty dataset or folder.'}, status=status.HTTP_400_BAD_REQUEST)
# Update the task with the new information
task.console_output += "Importing {} images...\n".format(len(files))
task.images_count = len(files)
task.pending_action = pending_actions.IMPORT
task.save()
# Associate the folder url with the project and task
combined_id = "{}_{}".format(project_pk, pk)
datastore = get_current_plugin().get_global_data_store()
datastore.set_json(combined_id, {
"ddbUrl": ddb_url,
"token": ddb.token,
"ddbWebUrl": "{}/r/{}/{}/{}".format(to_web_protocol(registry_url), orgSlug, dsSlug, folder.rstrip('/'))
})
#ddb.refresh_token()
# Start importing the files in the background
serialized = {'token': ddb.token, 'files': files}
run_function_async(import_files, task.id, serialized)
return Response({}, status=status.HTTP_200_OK)
def import_files(task_id, carrier):
import requests
from app import models
from app.plugins import logger
files = carrier['files']
#headers = CaseInsensitiveDict()
headers = {}
if carrier['token'] != None:
headers['Authorization'] = 'Bearer ' + carrier['token']
def download_file(task, file):
path = task.task_path(file['name'])
logger.info("Downloading file: " + file['url'])
download_stream = requests.get(file['url'], stream=True, timeout=60, headers=headers)
with open(path, 'wb') as fd:
for chunk in download_stream.iter_content(4096):
fd.write(chunk)
models.ImageUpload.objects.create(task=task, image=path)
logger.info("Will import {} files".format(len(files)))
task = models.Task.objects.get(pk=task_id)
task.create_task_directories()
task.save()
try:
downloaded_total = 0
for file in files:
download_file(task, file)
task.check_if_canceled()
models.Task.objects.filter(pk=task.id).update(upload_progress=(float(downloaded_total) / float(len(files))))
downloaded_total += 1
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
raise NodeServerError(e)
task.refresh_from_db()
task.pending_action = None
task.processing_time = 0
task.partial = False
task.save()
class CheckUrlTaskView(TaskView):
def get(self, request, project_pk=None, pk=None):
# Assert that task exists
self.get_and_check_task(request, pk)
# Check if there is an imported url associated with the project and task
combined_id = "{}_{}".format(project_pk, pk)
data = get_current_plugin().get_global_data_store().get_json(combined_id, default = None)
if data == None or 'ddbWebUrl' not in data:
return Response({'ddbWebUrl': None}, status=status.HTTP_200_OK)
else:
return Response({'ddbUrl': data['ddbWebUrl']}, status=status.HTTP_200_OK)
def get_status_key(task_id):
return '{}_ddb'.format(task_id)
@receiver(plugin_signals.task_removed, dispatch_uid="ddb_on_task_removed")
@receiver(plugin_signals.task_completed, dispatch_uid="ddb_on_task_completed")
def ddb_cleanup(sender, task_id, **kwargs):
from app.plugins import logger
# When a task is removed, simply remove clutter
# When a task is re-processed, make sure we can re-share it if we shared a task previously
logger.info("Cleaning up DroneDB datastore for task {}".format(str(task_id)))
datastore = get_current_plugin().get_global_data_store()
status_key = get_status_key(task_id)
logger.info("Info task {0} ({1})".format(str(task_id), status_key))
datastore.del_key(status_key)
class StatusTaskView(TaskView):
def get(self, request, pk):
task = self.get_and_check_task(request, pk)
# Associate the folder url with the project and task
status_key = get_status_key(pk)
datastore = get_current_plugin().get_global_data_store()
task_info = datastore.get_json(status_key, {
'status': 0, # Idle
'shareUrl': None,
'uploadedFiles': 0,
'totalFiles': 0,
'uploadedSize': 0,
'totalSize': 0,
'error': None
})
return Response(task_info, status=status.HTTP_200_OK)
DRONEDB_ASSETS = [
'orthophoto.tif',
'orthophoto.png',
'georeferenced_model.laz',
'dtm.tif',
'dsm.tif',
'shots.geojson',
'report.pdf',
'ground_control_points.geojson'
]
class ShareTaskView(TaskView):
def post(self, request, pk):
from app.plugins import logger
task = self.get_and_check_task(request, pk)
status_key = get_status_key(pk)
datastore = get_current_plugin().get_global_data_store()
data = {
'status': 1, # Running
'shareUrl': None,
'uploadedFiles': 0,
'totalFiles': 0,
'uploadedSize': 0,
'totalSize': 0,
'error': None
}
datastore.set_json(status_key, data)
settings = get_settings(request)
available_assets = [task.get_asset_file_or_zipstream(f)[0] for f in list(set(task.available_assets) & set(DRONEDB_ASSETS))]
if 'textured_model.zip' in task.available_assets:
texture_files = [join(task.assets_path('odm_texturing'), f) for f in listdir(task.assets_path('odm_texturing')) if isfile(join(task.assets_path('odm_texturing'), f))]
available_assets.extend(texture_files)
assets_path = task.assets_path()
files = [{'path': f, 'name': f[len(assets_path)+1:], 'size': os.path.getsize(f)} for f in available_assets]
share_to_ddb.delay(pk, settings, files)
return Response(data, status=status.HTTP_200_OK)
@task
def share_to_ddb(pk, settings, files):
from app.plugins import logger
status_key = get_status_key(pk)
datastore = get_current_plugin().get_global_data_store()
registry_url, username, password, token = settings
ddb = DroneDB(registry_url, username, password, token)
# Init share (to check)
share_token = ddb.share_init()
status = datastore.get_json(status_key)
status['totalFiles'] = len(files)
status['totalSize'] = sum(i['size'] for i in files)
datastore.set_json(status_key, status)
for file in files:
# check that file exists
if not os.path.exists(file['path']):
logger.info("File {} does not exist".format(file['path']))
continue
attempt = 0
while attempt < 3:
try:
attempt += 1
up = ddb.share_upload(share_token, file['path'], file['name'])
logger.info("Uploaded " + file['name'] + " to Dronedb (hash: " + up['hash'] + ")")
status['uploadedFiles'] += 1
status['uploadedSize'] += file['size']
datastore.set_json(status_key, status)
break
except Exception as e:
if (attempt == 3):
logger.error("Error uploading file {}: {}".format(file['name'], str(e)))
status['error'] = "Error uploading file {}: {}".format(file['name'], str(e))
status['status'] = 2 # Error
datastore.set_json(status_key, status)
return
else:
logger.info("Error uploading file {}: {}. Retrying...".format(file['name'], str(e)))
time.sleep(5)
continue
res = ddb.share_commit(share_token)
status['status'] = 3 # Done
status['shareUrl'] = registry_url + res['url']
logger.info("Shared on url " + status['shareUrl'])
datastore.set_json(status_key, status)

Wyświetl plik

@ -0,0 +1,23 @@
import requests
from django import forms
from django.contrib import messages
from django.http import HttpResponse
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from app.plugins import logger
def LoadButtonsView(plugin):
def view(request):
return render(
request,
plugin.template_path("load_buttons.js"),
{
"api_url": "/api" + plugin.public_url("").rstrip("/"),
},
content_type="text/javascript",
)
return view

Wyświetl plik

@ -0,0 +1,303 @@
from functools import reduce
import requests
from os import path
from app.plugins import logger
from urllib.parse import urlparse
VALID_IMAGE_EXTENSIONS = ['.tiff', '.tif', '.png', '.jpeg', '.jpg']
DEFAULT_HUB_URL = 'https://hub.dronedb.app'
class DroneDB:
def __init__(self, registry_url, username, password, token=None, update_token=None):
if not self.validate_url(registry_url):
raise ValueError("Invalid registry URL.")
self.username = username
self.password = password
self.token = token
self.public = False if username else True
self.update_token = update_token
self.__registry_url = registry_url[:-1] if registry_url.endswith('/') else registry_url
self.__authenticate_url = self.__registry_url + "/users/authenticate"
self.__refresh_url = self.__registry_url + "/users/authenticate/refresh"
self.__get_organizations_url = self.__registry_url + "/orgs"
self.__get_datasets_url = self.__registry_url + "/orgs/{0}/ds"
self.__get_folders_url = self.__registry_url + "/orgs/{0}/ds/{1}/search"
self.__get_files_list_url = self.__registry_url + "/orgs/{0}/ds/{1}/list"
self.__download_file_url = self.__registry_url + "/orgs/{0}/ds/{1}/download?path={2}&inline=1"
self.__share_init_url = self.__registry_url + "/share/init"
self.__share_upload_url = self.__registry_url + "/share/upload/{0}"
self.__share_commit_url = self.__registry_url + "/share/commit/{0}"
# Validate url
def validate_url(self, url):
try:
result = urlparse(url)
return all([result.scheme, result.netloc])
except:
return False
def login(self):
if (self.public):
logger.info("No need to login to DroneDB.")
return True
try:
# Authenticate
payload = {'username': self.username, 'password': self.password}
response = requests.post(self.__authenticate_url, data=payload)
if response.status_code != 200:
return False
# Get the token
self.token = response.json()['token']
logger.info("Logged in to DroneDB as user " + self.username + ".")
if (self.update_token is not None):
self.update_token(self.token)
return True
except(Exception) as e:
logger.error(e)
return False
def refresh_token(self):
if (self.public):
logger.info("Cannot refresh token.")
return False
try:
response = self.wrapped_call('POST', self.__refresh_url)
self.token = response.json()['token']
if (self.update_token is not None):
self.update_token(self.token)
except Exception as e:
raise Exception("Failed to refresh token.") from e
def wrapped_call(self, type, url, data=None, params=None, files=None, attempts=3):
headers = {}
cnt = attempts
while True:
if not self.public and self.token is None and not self.login():
raise ValueError("Could not authenticate to DroneDB.")
if self.token is not None:
headers = {'Authorization': 'Bearer ' + self.token }
response = requests.request(type, url, data=data, params=params, headers=headers, files=files)
if response.status_code == 200:
return response
if response.status_code == 401:
if (self.public):
raise DroneDBException("Failed to call '" + url + "': unauthorized.")
if not self.login():
raise DroneDBException("Failed to re-authenticate to DroneDB, cannot call '" + url + "'.")
else:
cnt -= 1
if cnt == 0:
raise DroneDBException("Failed all attempts to re-authenticate to DroneDB, cannot call '" + url + "'.")
else:
res = response.json()
raise DroneDBException("Failed to call '" + url + "'.", res)
def get_organizations(self):
try:
response = self.wrapped_call('GET', self.__get_organizations_url)
return [{'slug': o['slug'], 'name': o['name']} for o in response.json()]
except Exception as e:
raise Exception("Failed to get organizations.") from e
def get_datasets(self, orgSlug):
try:
response = self.wrapped_call('GET', self.__get_datasets_url.format(orgSlug))
return [
{'slug': o['slug'],
'name': o['properties'].get('meta', {}).get('name', {}).get('data', o['name']),
'public': o['properties'].get('public'),
'size': o['size'],
'entries': o['properties'].get('entries')
} for o in response.json()]
except Exception as e:
raise Exception("Failed to get datasets.") from e
def get_folders(self, orgSlug, dsSlug):
try:
# Type 1 is folder
payload = {'query': '*', 'recursive': True, 'type': 1}
response = self.wrapped_call('POST', self.__get_folders_url.format(orgSlug, dsSlug), data=payload)
return [o['path'] for o in response.json()]
except Exception as e:
raise Exception("Failed to get folders.") from e
def get_files_list(self, orgSlug, dsSlug, folder=None):
try:
# Type 1 is folder
params = {'path': '' if folder is None else folder}
logger.info(self.__get_files_list_url.format(orgSlug, dsSlug))
# Get the folders
response = self.wrapped_call('GET', self.__get_files_list_url.format(orgSlug, dsSlug), params=params)
# Exclude folders
files = filter(lambda itm: itm['type'] != 1, response.json())
return [
{'path': o['path'],
# extract name from path
'name': o['path'].split('/')[-1],
'type': o['type'],
'size': o['size'],
'url': self.__download_file_url.format(orgSlug, dsSlug, o['path'])
} for o in files]
except Exception as e:
raise Exception("Failed to get files list.") from e
def share_init(self, tag=None):
try:
data = {'tag': tag} if tag is not None else None
response = self.wrapped_call('POST', self.__share_init_url, data=data)
return response.json()['token']
except Exception as e:
raise Exception("Failed to initialize share.") from e
def share_upload(self, token, path, name):
try:
# Get file name
files = { 'file': open(path, 'rb') }
data = {'path': name}
response = self.wrapped_call('POST', self.__share_upload_url.format(token), files=files, data=data)
return response.json()
except Exception as e:
raise Exception("Failed to upload file.") from e
def share_commit(self, token):
try:
response = self.wrapped_call('POST', self.__share_commit_url.format(token))
return response.json()
except Exception as e:
raise Exception("Failed to commit share.") from e
def verify_url(url, username=None, password=None):
try:
registryUrl, orgSlug, dsSlug, folder = parse_url(url).values()
ddb = DroneDB(registryUrl, username, password)
files = ddb.get_files_list(orgSlug, dsSlug, folder)
# return some info
return {
'success': True,
'orgSlug': orgSlug,
'dsSlug': dsSlug,
'folder': folder,
'count': len(files),
'size': sum(i['size'] for i in files)
}
except Exception as e:
logger.error(e)
return {
'success': False,
'orgSlug': None,
'dsSlug': None,
'folder': None,
'count': None,
'size': None
}
def parse_url(url):
# Check if the url is valid
# Root folder of dataset: ddb://localhost:5001/admin/4uyyyaxcbvahd7qb
# 'test' folder of dataset: ddb://localhost:5001/admin/4uyyyaxcbvahd7qb/test
# using http instead of https: ddb+unsafe://localhost:5000/admin/4uyyyaxcbvahd7qb
# using hub url: https://localhost:5001/r/admin/4uyyyaxcbvahd7qb
# using hub url without /r/ http://localhost:5000/admin/4uyyyaxcbvahd7qb/test
p = urlparse(url)
segments = p.path.split('/')
if p.scheme not in ['ddb', 'ddb+unsafe', 'http', 'https']:
raise ValueError("Invalid URL scheme.")
if p.netloc == '':
raise ValueError("Invalid URL.")
scheme = p.scheme
# used to skip the /r/: if ddb url we have no /r/ instead if http we have it
if p.scheme == 'ddb':
scheme = 'https'
elif p.scheme == 'ddb+unsafe':
scheme = 'http'
offset = 1 if segments[1] == 'r' else 0
if (len(segments) < offset + 3):
raise ValueError("Invalid URL.")
return {
'registryUrl': scheme + '://' + p.netloc,
'orgSlug': segments[1 + offset],
'dsSlug': segments[2 + offset],
'folder': '/'.join(segments[3 + offset:])
}
class DroneDBException(Exception):
def __init__(self, message, res=None):
super().__init__(message)
self.response = res

Wyświetl plik

Wyświetl plik

@ -0,0 +1,13 @@
{
"name": "DroneDB",
"webodmMinVersion": "1.1.2",
"description": "Integrate WebODM with DroneDB: import images and share results",
"version": "0.0.1",
"author": "Luca Di Leo",
"email": "ldileo@digipa.it",
"repository": "https://github.com/OpenDroneMap/WebODM",
"tags": ["ddb", "DroneDB", "cloud"],
"homepage": "https://github.com/OpenDroneMap/WebODM",
"experimental": true,
"deprecated": false
}

Wyświetl plik

@ -0,0 +1,88 @@
from app.plugins import PluginBase, Menu, MountPoint, logger
from coreplugins.dronedb.app_views import LoadButtonsView
from coreplugins.dronedb.ddb import DEFAULT_HUB_URL
from .api_views import (
CheckUrlTaskView,
FoldersTaskView,
ImportDatasetTaskView,
CheckCredentialsTaskView,
OrganizationsTaskView,
DatasetsTaskView,
StatusTaskView,
VerifyUrlTaskView,
InfoTaskView,
ShareTaskView
)
from django.contrib import messages
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django import forms
class SettingsForm(forms.Form):
username = forms.CharField(label='Username', required=False, max_length=1024, widget=forms.TextInput(attrs={'placeholder': 'Username'}))
password = forms.CharField(label='Password', required=False, max_length=1024, widget=forms.PasswordInput(attrs={'placeholder': 'Password'}))
registry_url = forms.CharField(label='Registry URL', required=False, max_length=1024, widget=forms.TextInput(attrs={'placeholder': 'Registry Url'}))
class Plugin(PluginBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def main_menu(self):
return [Menu("DroneDB", self.public_url(""), "ddb-icon fa-fw")]
def include_js_files(self):
return ["load_buttons.js"]
def include_css_files(self):
return ["build/ImportView.css", "style.css"]
def build_jsx_components(self):
return ["ImportView.jsx", "ShareButton.jsx"]
def api_mount_points(self):
return [
MountPoint("projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/import", ImportDatasetTaskView.as_view()),
MountPoint("projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/checkforurl", CheckUrlTaskView.as_view()),
MountPoint("tasks/(?P<pk>[^/.]+)/status", StatusTaskView.as_view()),
MountPoint("tasks/(?P<pk>[^/.]+)/share", ShareTaskView.as_view()),
MountPoint("checkcredentials", CheckCredentialsTaskView.as_view()),
MountPoint("organizations/(?P<org>[^/.]+)/datasets/(?P<ds>[^/.]+)/folders", FoldersTaskView.as_view()),
MountPoint("organizations/(?P<org>[^/.]+)/datasets", DatasetsTaskView.as_view()),
MountPoint("organizations", OrganizationsTaskView.as_view()),
MountPoint("verifyurl", VerifyUrlTaskView.as_view()),
MountPoint("info", InfoTaskView.as_view()),
]
def HomeView(self):
@login_required
def home(request):
ds = self.get_user_data_store(request.user)
# if this is a POST request we need to process the form data
if request.method == 'POST':
form = SettingsForm(request.POST)
if form.is_valid():
ds.set_string('registry_url', form.cleaned_data['registry_url'])
ds.set_string('username', form.cleaned_data['username'])
ds.set_string('password', form.cleaned_data['password'])
ds.set_string('token', None)
messages.success(request, 'Settings updated.')
form = SettingsForm(initial={'username': ds.get_string('username', default=""),
'password': ds.get_string('password', default=""),
'registry_url': ds.get_string('registry_url', default="") or DEFAULT_HUB_URL})
return render(request, self.template_path("app.html"), {
'title': 'DroneDB',
'form': form
})
return home
def app_mount_points(self):
return [
MountPoint("$", self.HomeView()),
MountPoint("load_buttons.js$", LoadButtonsView(self)),
]

Wyświetl plik

@ -0,0 +1,115 @@
import React, { Component, Fragment } from "react";
import PropTypes from 'prop-types';
import ResizeModes from 'webodm/classes/ResizeModes';
import { Modal, Button } from "react-bootstrap";
import SelectUrlDialog from "./components/SelectUrlDialog";
import ErrorDialog from "./components/ErrorDialog";
import ConfigureNewTaskDialog from "./components/ConfigureNewTaskDialog";
import "./ImportView.scss";
export default class TaskView extends Component {
static propTypes = {
projectId: PropTypes.number.isRequired,
apiURL: PropTypes.string.isRequired,
onNewTaskAdded: PropTypes.func.isRequired,
}
state = {
error: "",
ddbRes: null,
isDialogOpen: false
};
onHideDialog = () => this.setState({ ddbRes: null, taskId: null, isDialogOpen: false });
onSelectDdbRes = res => {
console.log("Result", res);
this.setState({ ddbRes: res, isDialogOpen: false });
}
onSaveTask = taskInfo => {
// Create task
const formData = {
name: taskInfo.name,
options: taskInfo.options,
processing_node: taskInfo.selectedNode.id,
auto_processing_node: taskInfo.selectedNode.key == "auto",
partial: true
};
if (taskInfo.resizeMode === ResizeModes.YES) {
formData.resize_to = taskInfo.resizeSize;
}
$.ajax({
url: `/api/projects/${this.props.projectId}/tasks/`,
contentType: 'application/json',
data: JSON.stringify(formData),
dataType: 'json',
type: 'POST'
}).done((task) => {
$.ajax({
url: `${this.props.apiURL}/projects/${this.props.projectId}/tasks/${task.id}/import`,
contentType: 'application/json',
data: JSON.stringify({ddb_url: this.state.ddbRes.url}),
dataType: 'json',
type: 'POST'
}).done(() => {
this.onHideDialog();
this.props.onNewTaskAdded();
}).fail(error => {
this.onErrorInDialog("Failed to start importing.");
});
}).fail(() => {
this.onErrorInDialog("Cannot create new task. Please try again later.");
});
}
onErrorInDialog = msg => {
this.setState({ error: msg });
this.onHideDialog();
};
handleClick = () => {
this.setState({ isDialogOpen: true });
}
render() {
const {
error,
ddbRes,
isDialogOpen
} = this.state;
return (
<Fragment>
{error ? <ErrorDialog errorMessage={error} /> : ""}
<Button
bsStyle={"default"}
bsSize={"small"}
className={"platform-btn"}
onClick={this.handleClick}>
<i className={"ddb-icon fa-fw"} />
DroneDB Import
</Button>
<SelectUrlDialog
show={isDialogOpen}
onHide={this.onHideDialog}
onSubmit={this.onSelectDdbRes}
apiURL={this.props.apiURL}
/>
{ddbRes ?
<ConfigureNewTaskDialog
show={ddbRes !== null}
ddbRes={ddbRes}
onHide={this.onHideDialog}
onSaveTask={this.onSaveTask}
/> : ""}
</Fragment>
);
}
}

Wyświetl plik

@ -0,0 +1,16 @@
.platform-btn {
.ddb-icon{
margin-right: 8px;
}
.fa-cloud {
margin-right: 0.5em;
}
.caret {
margin-left: 1em;
}
.btn-sm {
border-radius: 0;
}
}

Wyświetl plik

@ -0,0 +1,152 @@
import React from 'react';
import PropTypes from 'prop-types';
import Storage from 'webodm/classes/Storage';
import ErrorMessage from 'webodm/components/ErrorMessage';
import $ from 'jquery';
const STATE_IDLE = 0;
const STATE_RUNNING = 1;
const STATE_ERROR = 2;
const STATE_DONE = 3;
const ICON_CLASS_MAPPER = [
// Idle
'ddb-icon fa-fw',
// Running
'fa fa-circle-notch fa-spin fa-fw',
// Error
'fa fa-exclamation-triangle',
// Done
'fas fa-external-link-alt'
];
const BUTTON_TEXT_MAPPER = [
// Idle
'Share to DroneDB',
// Running
'Sharing',
// Error retry
'Error, retry',
// Done
'View on DroneDB'
];
export default class ShareButton extends React.Component{
static defaultProps = {
task: null
};
static propTypes = {
task: PropTypes.object.isRequired,
};
constructor(props){
super(props);
this.state = {
taskInfo: null,
error: '',
monitorTimeout: null
};
}
componentDidMount(){
this.updateTaskInfo(false);
}
updateTaskInfo = (showErrors) => {
const { task } = this.props;
return $.ajax({
type: 'GET',
url: `/api/plugins/dronedb/tasks/${task.id}/status`,
contentType: 'application/json'
}).done(taskInfo => {
console.log(taskInfo);
debugger;
this.setState({taskInfo});
if (taskInfo.error && showErrors) this.setState({error: taskInfo.error});
}).fail(error => {
this.setState({error: error.statusText});
});
}
componentWillUnmount(){
if (this.monitorTimeout) clearTimeout(this.monitorTimeout);
}
shareToDdb = (formData) => {
const { task } = this.props;
return $.ajax({
url: `/api/plugins/dronedb/tasks/${task.id}/share`,
contentType: 'application/json',
dataType: 'json',
type: 'POST'
}).done(taskInfo => {
this.setState({taskInfo});
this.monitorProgress();
});
}
monitorProgress = () => {
if (this.state.taskInfo.status == STATE_RUNNING){
// Monitor progress
this.monitorTimeout = setTimeout(() => {
this.updateTaskInfo(true).always(this.monitorProgress);
}, 3000);
}
}
handleClick = e => {
if (this.state.taskInfo.status == STATE_IDLE || this.state.taskInfo.status == STATE_ERROR) {
this.shareToDdb();
}
if (this.state.taskInfo.status == STATE_DONE){
window.open(this.state.taskInfo.shareUrl, '_blank');
}
}
render(){
const { taskInfo, error } = this.state;
const getButtonIcon = () => {
if (taskInfo == null) return "fa fa-circle-notch fa-spin fa-fw";
if (taskInfo.error) return "fa fa-exclamation-triangle";
return ICON_CLASS_MAPPER[taskInfo.status];
};
const getButtonLabel = () => {
if (taskInfo == null) return "Share to DroneDB";
if (taskInfo.error) return "DroneDB plugin error";
var text = BUTTON_TEXT_MAPPER[taskInfo.status];
if (taskInfo.status == STATE_RUNNING && taskInfo.uploadedSize > 0 && taskInfo.totalSize > 0) {
var progress = (taskInfo.uploadedSize / taskInfo.totalSize) * 100;
text += ` (${progress.toFixed(2)}%)`;
}
return text;
};
return (
<div className="share-button">
<button className="btn btn-primary btn-sm" onClick={this.handleClick} disabled={this.state.taskInfo == null || this.state.taskInfo.status == STATE_RUNNING }>
<i className={getButtonIcon()}></i>&nbsp;
{getButtonLabel()}
</button>
{this.state.error && <div style={{ marginTop: '10px' }}><ErrorMessage bind={[this, 'error']} /></div> }
</div>
);
}
}

Wyświetl plik

@ -0,0 +1,45 @@
import PropTypes from 'prop-types';
import { Component } from "react";
import { Modal } from "react-bootstrap";
import NewTaskPanel from "webodm/components/NewTaskPanel";
import "./ConfigureNewTaskDialog.scss";
export default class ConfigureNewTaskDialog extends Component {
static defaultProps = {
};
static propTypes = {
onHide: PropTypes.func.isRequired,
onSaveTask: PropTypes.func.isRequired,
ddbRes: PropTypes.object,
}
render() {
const {
onHide,
onSaveTask,
ddbRes,
} = this.props;
return (
<Modal className={"new-task"} onHide={onHide} show={true}>
<Modal.Header closeButton>
<Modal.Title>
Import from DroneDB
</Modal.Title>
</Modal.Header>
<Modal.Body>
<NewTaskPanel
onSave={onSaveTask}
onCancel={onHide}
filesCount={ddbRes ? ddbRes.images_count : 0}
getFiles={() => []}
showResize={false}
suggestedTaskName={ddbRes ? ddbRes.name : null}
/>
</Modal.Body>
</Modal>
);
}
}

Wyświetl plik

@ -0,0 +1,7 @@
.modal-backdrop {
z-index: 100000 !important;
}
.new-task.modal button i {
margin-right: 1em;
}

Wyświetl plik

@ -0,0 +1,35 @@
import PropTypes from 'prop-types';
import { Component } from "react";
import { Modal } from "react-bootstrap";
import "./ErrorDialog.scss";
export default class ErrorDialog extends Component {
static propTypes = {
errorMessage: PropTypes.string.isRequired,
}
constructor(props){
super(props);
this.state = { show: true };
}
handleOnHide = () => this.setState({show: false});
render() {
const { errorMessage } = this.props;
return (
<Modal show={this.state.show}>
<Modal.Header closeButton onHide={this.handleOnHide}>
<Modal.Title>
There was an error with Cloud Import :(
</Modal.Title>
</Modal.Header>
<Modal.Body>
{ errorMessage }
</Modal.Body>
</Modal>
);
}
}

Wyświetl plik

@ -0,0 +1,3 @@
.modal-backdrop {
z-index: 100000 !important;
}

Wyświetl plik

@ -0,0 +1,356 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from "react";
import { Modal, Button, FormGroup, ControlLabel, FormControl, HelpBlock } from "react-bootstrap";
import Select from 'react-select';
import "./SelectUrlDialog.scss";
export default class SelectUrlDialog extends Component {
static defaultProps = {
platform: null,
show: false,
ddbUrl: null
};
static propTypes = {
onHide: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
ddbUrl: PropTypes.string,
show: PropTypes.bool.isRequired,
apiURL: PropTypes.string.isRequired,
}
constructor(props){
super(props);
this.resetState();
}
resetState() {
this.state = {
error: "",
organizations: [],
datasets: [],
folders: [],
loadingOrganizations: true,
loadingDatasets: false,
loadingFolders: false,
hasLogin: false,
selectedOrganization: null,
selectedDataset: null,
selectedFolder: null,
info: null,
verDs: null,
verCount: 0,
verSize: 0,
verFolder: null,
// verifyStatus: null (not started), 'loading', 'success', 'error'
verifyStatus: null
};
}
// Format bytes to readable string
formatBytes(bytes, decimals=2) {
if(bytes == 0) return '0 bytes';
var k = 1024,
dm = decimals || 2,
sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
handleOnShow = () => {
this.resetState();
$.get(`${this.props.apiURL}/organizations`)
.done(result => {
var orgs = result.map(org => {
return { label: org.name !== org.slug ? `${org.name} (${org.slug})` : org.slug, value: org.slug };
});
if (orgs.length > 0) {
this.setState({organizations: orgs, loadingOrganizations: false});
// Search for user organization
var userOrg = orgs.find(org => org.value === this.state.info.username);
this.handleSelectOrganization(userOrg != null ? userOrg : orgs[0]);
} else
this.setState({organizations: orgs, loadingOrganizations: false});
})
.fail((error) => {
this.setState({error: "Cannot load organizations. Check your internet connection.", organizations: []});
})
.always(() => {
this.setState({loadingOrganizations: false});
});
$.get(`${this.props.apiURL}/info`)
.done(result => {
this.setState({info: result});
})
.fail((error) => {
this.setState({info: null});
});
}
handleVerify = () => {
this.setState({verifyStatus: 'loading'});
$.post(`${this.props.apiURL}/verifyurl`, { url: this.state.ddbUrl }).done(result => {
if (result != null) {
this.setState({
verifyStatus: result.count > 0 ? 'success' : 'error',
verCount: result.count,
verDs: result.ds,
verSize: result.size,
verFolder: result.folder
});
} else {
this.setState({verifyStatus: 'error'});
}
})
.fail((error) => {
this.setState({verifyStatus: 'error'});
});
}
handleSelectOrganization = (e) => {
if (this.state.selectedOrganization !== null && e.value === this.state.selectedOrganization.value) return;
this.setState({
loadingDatasets: true,
selectedOrganization: e,
selectedDataset: null,
selectedFolder: null,
verifyStatus: null,
datasets: [],
folders: []
});
$.get(`${this.props.apiURL}/organizations/${e.value}/datasets`)
.done(result => {
var dss = result.map(ds => {
return { label: ds.name !== ds.slug ?
`${ds.name} (${ds.slug}) - ${ds.entries} entries (${this.formatBytes(ds.size)})`:
`${ds.name} - ${ds.entries} entries (${this.formatBytes(ds.size)})`, name: ds.name, value: ds.slug };
});
if (dss.length > 0) {
this.setState({datasets: dss, loadingDatasets: false});
this.handleSelectDataset(dss[0]);
} else
this.setState({datasets: dss, loadingDatasets: false});
})
.fail((error) => {
this.setState({error: "Cannot load datasets. Check your internet connection."});
})
.always(() => {
this.setState({loadingDatasets: false});
});
};
handleSelectDataset = (e) => {
if (this.state.selectedDataset !== null && e.value === this.state.selectedDataset.value) return;
this.setState({
selectedDataset: e,
selectedFolder: null,
loadingFolders: true,
verifyStatus: null,
folders: []
});
$.get(`${this.props.apiURL}/organizations/${this.state.selectedOrganization.value}/datasets/${e.value}/folders`)
.done(result => {
var folders = result.map(folder => {
return { label: folder, value: '/' + folder };
});
folders.unshift({label: '/', value: '/'});
folders.sort();
if (folders.length > 0) {
this.setState({folders: folders, loadingFolders: false});
this.handleSelectFolder(folders[0]);
} else
this.setState({folders: folders, loadingFolders: false});
})
.fail((error) => {
this.setState({error: "Cannot load folders. Check your internet connection."});
})
.always(() => {
this.setState({loadingFolders: false});
});
};
handleSelectFolder = e => {
if (this.state.selectedFolder !== null && e.value === this.state.selectedFolder.value) return;
this.setState({selectedFolder: e, verifyStatus: null});
if (this.state.info == null || this.state.info.hubUrl == null) {
console.warn("Cannot generate ddb url, no hub url");
return;
}
var url = `${this.state.info.hubUrl}/${this.state.selectedOrganization.value}/${this.state.selectedDataset.value}${e.value}`
.replace('http://', 'ddb+unsafe://')
.replace('https://', 'ddb://');
this.setState({ddbUrl: url});
}
handleChange = (e) => {
this.setState({ddbUrl: e.target.value, verifyStatus: null});
};
handleSubmit = e => {
this.props.onSubmit(
{
name: this.state.verDs != null ? this.state.verDs : "DroneDB",
url: this.state.ddbUrl,
images_count: this.state.verCount
});
};
render() {
const {
onHide,
ddbUrl,
show
} = this.props;
return (
<Modal className={"folder-select"} onHide={onHide} show={show} onShow={this.handleOnShow}>
<Modal.Header closeButton>
<Modal.Title>
Import from DroneDB
</Modal.Title>
</Modal.Header>
<Modal.Body bsClass="my-modal">
{this.state.organizations!= null && this.state.organizations.length > 0 ?
<div style={{'marginBottom': '20px'}}>
<p>Import images from your DroneDB account</p>
<div className={"select-row"}>
<div className={"icon-cell"}>
<i className={"fas fa-sitemap"}></i>
</div>
<div className={"select-cell"}>
<Select
className="basic-single"
classNamePrefix="select"
isLoading={this.state.loadingOrganizations}
isClearable={false}
isSearchable={true}
value={this.state.selectedOrganization}
onChange={this.handleSelectOrganization}
options={this.state.organizations}
placeholder={this.state.loadingOrganizations ? "Fetching organizations..." : "Please select an organization"}
name="organizations"
/>
</div>
</div>
<div className={"select-row"}>
<div className={"icon-cell"}>
<i className={"fas fa-database"}></i>
</div>
<div className={"select-cell"}>
<Select
className="basic-single"
classNamePrefix="select"
isLoading={this.state.loadingDatasets}
isClearable={false}
isSearchable={true}
value={this.state.selectedDataset}
isDisabled={this.state.selectedOrganization === null}
onChange={this.handleSelectDataset}
options={this.state.datasets}
placeholder={this.state.loadingDatasets ? "Fetching datasets..." : (this.state.datasets.length > 0 ? "Please select a dataset" : "No datasets found")}
name="datasets"
/>
</div>
</div>
<div className={"select-row"}>
<div className={"icon-cell"}>
<i className={"fas fa-folder"}></i>
</div>
<div className={"select-cell"}>
<Select
className="basic-single"
classNamePrefix="select"
isLoading={this.state.loadingFolders}
isClearable={false}
isSearchable={true}
value={this.state.selectedFolder}
isDisabled={this.state.selectedDataset === null || this.state.selectedOrganization === null}
onChange={this.handleSelectFolder}
options={this.state.folders}
placeholder={this.state.loadingFolders ? "Fetching folders..." : "Please select a folder"}
name="folders"
/>
</div>
</div>
</div> : <div className="text-center">
{this.state.loadingOrganizations ? <i className="fa fa-circle-notch fa-spin fa-fw"></i> :
<div className={"alert alert-info"}>
<span><a href="/plugins/dronedb"><strong>Setup your DroneDB credentials</strong></a> to browse your organizations, datasets and folders!</span>
</div>}
</div>
}
<p>DroneDB URL</p>
<div className={"select-row"}>
<div className={"icon-cell"}>
<i className={"fas fa-globe"}></i>
</div>
<div className={"select-cell"}>
<FormControl
type="url"
placeholder={"https://hub.dronedb.app/r/username/dataset"}
value={this.state.ddbUrl || ''}
onChange={this.handleChange} />
</div>
<div className={"icon-cell"}>
{ this.state.verifyStatus==='loading' && <i className={"fas fa-spinner fa-spin"}></i> }
{ this.state.verifyStatus==='success' && <i className={"fas fa-check"}></i> }
{ this.state.verifyStatus==='error' && <i className={"fas fa-times"}></i> }
</div>
</div>
{this.state.verifyStatus != null && this.state.verifyStatus == "success" ?
<div className={"alert alert-success"}>
<span>Found <strong>{this.state.verCount}</strong> files ({this.formatBytes(this.state.verSize)})</span>
</div>
: ""}
</Modal.Body>
<Modal.Footer>
<Button onClick={onHide}>Close</Button>
<Button bsStyle="success" disabled={this.state.ddbUrl == null || this.state.ddbUrl.length == 0} onClick={this.handleVerify}>
<i className={"fas fa-check"} />Verify</Button>
<Button bsStyle="primary" disabled={this.state.verifyStatus !== 'success'} onClick={this.handleSubmit}>
<i className={"fa fa-upload"} />Import</Button>
</Modal.Footer>
</Modal>
);
}
}

Wyświetl plik

@ -0,0 +1,29 @@
.modal-backdrop {
z-index: 100000 !important;
}
.folder-select.modal button i {
margin-right: 1em;
}
.my-modal {
max-height: calc(100vh - 220px);
position: relative;
padding: 20px;
}
.select-row {
display: flex;
width: 100%;
margin-bottom: 10px;
.icon-cell {
width: 40px;
display: flex;
justify-content: center;
align-items: center;
}
.select-cell {
flex-grow: 1;
}
}

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -0,0 +1,12 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2022 by original authors @ fontello.com</metadata>
<defs>
<font id="ddb" horiz-adv-x="1000" >
<font-face font-family="ddb" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="ddb-icon" unicode="&#xe803;" d="M73 167c-32 0-58-26-58-59v-139c0-32 26-58 58-58h1090c33 0 58 26 58 58v139c0 33-25 59-58 59h-1090z m178-64c35 0 64-28 64-63 0-36-29-64-64-64-35 0-63 28-63 64 0 0 0 0 0 0 0 33 28 63 63 63 0 0 0 0 0 0z m374 0c35 0 64-28 64-63 0 0 0 0 0 0 0-36-29-64-64-64 0 0 0 0 0 0-35 0-63 28-63 64 0 0 0 0 0 0 0 33 28 63 63 63 0 0 0 0 0 0z m359 0c35 0 63-28 63-63 0 0 0 0 0 0 0-36-28-64-63-64 0 0 0 0 0 0-35 0-64 28-64 64 0 0 0 0 0 0 0 33 29 63 64 63 0 0 0 0 0 0z m-910 375c-33 0-59-26-59-58v-139c0-33 26-59 59-59h1089c33 0 59 26 59 59v139c0 32-26 58-59 58h-1089z m178-65c35 0 63-28 63-63 0-35-28-64-63-64-36 0-64 29-64 64 0 0 0 0 0 0 0 35 28 63 64 63 0 0 0 0 0 0z m374 0c35 0 63-28 63-63 0 0 0 0 0 0 0-35-28-64-63-64 0 0 0 0 0 0-36 0-64 29-64 64 0 0 0 0 0 0 0 35 28 63 64 63 0 0 0 0 0 0z m358 0c35 0 64-28 64-63 0 0 0 0 0 0 0-35-29-64-64-64 0 0 0 0 0 0-35 0-64 29-64 64 0 0 0 0 0 0 0 35 29 63 64 63 0 0 0 0 0 0z m-910 377c-33 0-59-26-59-59v-139c0-32 26-58 59-58h1089c33 0 59 26 59 58v139c0 33-26 59-59 59h-1089z m178-65c35 0 63-29 63-64 0-35-28-63-63-63-36 0-64 28-64 63 0 0 0 0 0 0 0 35 28 64 64 64 0 0 0 0 0 0z m374 0c35 0 63-29 63-64 0 0 0 0 0 0 0-35-28-63-63-63 0 0 0 0 0 0-36 0-64 28-64 63 0 0 0 0 0 0 0 35 28 64 64 64 0 0 0 0 0 0z m358 0c35 0 64-29 64-64 0 0 0 0 0 0 0-35-29-63-64-63 0 0 0 0 0 0-35 0-64 28-64 63 0 0 0 0 0 0 0 35 29 64 64 64 0 0 0 0 0 0z" horiz-adv-x="1236" />
</font>
</defs>
</svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.9 KiB

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -0,0 +1,6 @@
{
"dependencies": {
"react-bootstrap": "^0.32.4",
"react-select": "^3.0.4"
}
}

Wyświetl plik

@ -0,0 +1,43 @@
@font-face {
font-family: 'ddbfont';
src: url('fonts/ddb.eot?16744445');
src: url('fonts/ddb.eot?16744445#iefix') format('embedded-opentype'),
url('fonts/ddb.woff2?16744445') format('woff2'),
url('fonts/ddb.woff?16744445') format('woff'),
url('fonts/ddb.ttf?16744445') format('truetype'),
url('fonts/ddb.svg?16744445#ddb') format('svg');
font-weight: normal;
font-style: normal;
}
.ddb-icon:before {
font-family: "ddbfont";
font-style: normal;
font-weight: normal;
speak: never;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Font smoothing. That was taken from TWBS */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Uncomment for 3D effect */
text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3);
content: '\e803';
}

Wyświetl plik

@ -0,0 +1,85 @@
{% extends "app/plugins/templates/base.html" %}
{% load bootstrap_extras %}
{% block content %}
<h3>DroneDB</h3>
Provide your credentials to access <a href="https://dronedb.app" target="_blank">DroneDB</a>:
<br/>
<br/>
<form action="" method="post" class="oam-form oam-token-form">
{% csrf_token %}
{% include "app/plugins/templates/form.html" %}
<button id="btnVerify" type="button" class="btn btn-success" disabled><i class="fas fa-user-check"></i>&nbsp;&nbsp;Verify Configuration</button>
<button type="submit" class="btn btn-primary" disabled><i class="fa fa-save fa-fw"></i>&nbsp;&nbsp;Save Configuration</button>
</form>
<div id="alert-result" style="display: none; margin-top: 20px" class="alert" role="alert"></div>
<script>
$(document).ready(function() {
var running = false;
var timeout = null;
$("input[name=registry_url],input[name=username],input[name=password]").on('keyup', function() {
var alert = $("#alert-result").hide();
if (timeout != null) clearTimeout(timeout);
$("#btnVerify,button[type=submit]").prop('disabled',
$("input[name=registry_url]").val().length == 0 ||
$("input[name=username]").val().length == 0 ||
$("input[name=password]").val().length == 0);
});
$("#btnVerify").click(function() {
if (running) return;
running = true;
if (timeout != null) clearTimeout(timeout);
var alert = $("#alert-result");
alert.attr('class', 'alert alert-info');
alert.text("Testing configuration...");
alert.show();
$("#btnVerify").prop('disabled', true);
$.ajax({
url: "/api/plugins/dronedb/checkcredentials",
type: "POST",
data: {
"csrfmiddlewaretoken": $("input[name=csrfmiddlewaretoken]").val(),
"hubUrl": $("input[name=registry_url]").val(),
"userName": $("input[name=username]").val(),
"password": $("input[name=password]").val()
},
success: function(data) {
if (data.success) {
alert.attr('class', 'alert alert-success');
alert.text("Configuration verified successfully!");
} else {
alert.attr('class', 'alert alert-danger');
alert.text("Configuration verification failed!");
}
timeout = setTimeout(function() { alert.hide(); }, 5000);
},
error: function(data) {
alert.attr('class', 'alert alert-danger');
alert.text("Configuration failed: cannot reach server!");
},
complete: function() {
running = false;
$("#btnVerify").prop('disabled', false);
}
});
});
});
</script>
{% endblock %}

Wyświetl plik

@ -0,0 +1,19 @@
PluginsAPI.Dashboard.addNewTaskButton(
["dronedb/build/ImportView.js"],
function(args, ImportView) {
return React.createElement(ImportView, {
onNewTaskAdded: args.onNewTaskAdded,
projectId: args.projectId,
apiURL: "{{ api_url }}",
});
}
);
PluginsAPI.Dashboard.addTaskActionButton(['dronedb/build/ShareButton.js'],function(args, ShareButton){
var task = args.task;
if (task.available_assets !== null && task.available_assets.length > 0){
return React.createElement(ShareButton, {task: task, token: "${token}"});
}
}
);