kopia lustrzana https://github.com/OpenDroneMap/WebODM
Added faster console rendering, cancel action task
rodzic
4ffed093e9
commit
955c9af4a6
|
@ -33,7 +33,7 @@ class TaskViewSet(viewsets.ViewSet):
|
|||
# We don't use object level permissions on tasks, relying on
|
||||
# project's object permissions instead (but standard model permissions still apply)
|
||||
permission_classes = (permissions.DjangoModelPermissions, )
|
||||
parser_classes = (parsers.MultiPartParser, parsers.JSONParser, )
|
||||
parser_classes = (parsers.MultiPartParser, parsers.JSONParser, parsers.FormParser, )
|
||||
ordering_fields = '__all__'
|
||||
|
||||
@staticmethod
|
||||
|
@ -62,6 +62,9 @@ class TaskViewSet(viewsets.ViewSet):
|
|||
task.last_error = None
|
||||
task.save()
|
||||
|
||||
# Call the scheduler (speed things up)
|
||||
scheduler.process_pending_tasks(background=True)
|
||||
|
||||
return Response({'success': True})
|
||||
|
||||
@detail_route(methods=['get'])
|
||||
|
|
|
@ -154,7 +154,12 @@ class Task(models.Model):
|
|||
images = [image.path() for image in self.imageupload_set.all()]
|
||||
|
||||
try:
|
||||
self.uuid = self.processing_node.process_new_task(images, self.name, self.options)
|
||||
# This takes a while
|
||||
uuid = self.processing_node.process_new_task(images, self.name, self.options)
|
||||
|
||||
# Refresh task object before committing change
|
||||
self.refresh_from_db()
|
||||
self.uuid = uuid
|
||||
self.save()
|
||||
|
||||
# TODO: log process has started processing
|
||||
|
@ -162,6 +167,24 @@ class Task(models.Model):
|
|||
except ProcessingException as e:
|
||||
self.set_failure(e.message)
|
||||
|
||||
|
||||
if self.pending_action is not None:
|
||||
try:
|
||||
if self.pending_action == self.PendingActions.CANCEL:
|
||||
# Do we need to cancel the task on the processing node?
|
||||
logger.info("Canceling task {}".format(self))
|
||||
if self.processing_node and self.uuid:
|
||||
self.processing_node.cancel_task(self.uuid)
|
||||
self.pending_action = None
|
||||
else:
|
||||
raise ProcessingException("Cannot cancel a task that has no processing node or UUID assigned")
|
||||
except ProcessingException as e:
|
||||
self.last_error = e.message
|
||||
finally:
|
||||
self.save()
|
||||
|
||||
|
||||
if self.processing_node:
|
||||
# Need to update status (first time, queued or running?)
|
||||
if self.uuid and self.status in [None, status_codes.QUEUED, status_codes.RUNNING]:
|
||||
# Update task info from processing node
|
||||
|
@ -194,21 +217,6 @@ class Task(models.Model):
|
|||
except ProcessingException as e:
|
||||
self.set_failure(e.message)
|
||||
|
||||
if self.pending_action is not None:
|
||||
try:
|
||||
if self.pending_action == self.PendingActions.CANCEL:
|
||||
# Do we need to cancel the task on the processing node?
|
||||
logger.info("Canceling task {}".format(self))
|
||||
if self.processing_node and self.uuid:
|
||||
self.processing_node.cancel_task(self.uuid)
|
||||
else:
|
||||
raise ProcessingException("Cannot cancel a task that has no processing node or UUID assigned")
|
||||
except ProcessingException as e:
|
||||
self.last_error = e.message
|
||||
finally:
|
||||
self.pending_action = None
|
||||
self.save()
|
||||
|
||||
|
||||
def set_failure(self, error_message):
|
||||
logger.error("{} ERROR: {}".format(self, error_message))
|
||||
|
|
|
@ -40,7 +40,7 @@ class Console extends React.Component {
|
|||
this.sourceRequest = $.get(sourceUrl, text => {
|
||||
if (text !== ""){
|
||||
let lines = text.split("\n");
|
||||
lines.forEach(line => this.addLine(line));
|
||||
this.addLines(lines);
|
||||
currentLineNumber += (lines.length - 1);
|
||||
}
|
||||
})
|
||||
|
@ -48,6 +48,7 @@ class Console extends React.Component {
|
|||
if (textStatus !== "abort" && this.props.refreshInterval !== undefined){
|
||||
this.sourceTimeout = setTimeout(updateFromSource, this.props.refreshInterval);
|
||||
}
|
||||
this.checkAutoscroll();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -80,9 +81,10 @@ class Console extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
addLine(text){
|
||||
addLines(lines){
|
||||
if (!Array.isArray(lines)) lines = [lines];
|
||||
this.setState(update(this.state, {
|
||||
lines: {$push: [text]}
|
||||
lines: {$push: lines}
|
||||
}));
|
||||
this.checkAutoscroll();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
const CANCEL = 1,
|
||||
DELETE = 2;
|
||||
|
||||
let pendingActions = {
|
||||
[CANCEL]: {
|
||||
descr: "Canceling..."
|
||||
},
|
||||
[DELETE]: {
|
||||
descr: "Deleting..."
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
CANCEL: CANCEL,
|
||||
DELETE: DELETE,
|
||||
|
||||
description: function(pendingAction) {
|
||||
if (pendingActions[pendingAction]) return pendingActions[pendingAction].descr;
|
||||
else return "";
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
|
||||
class ErrorMessage extends React.Component {
|
||||
constructor(props){
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
error: props.message
|
||||
};
|
||||
|
||||
this.close = this.close.bind(this);
|
||||
}
|
||||
|
||||
close(){
|
||||
this.setState({error: ""});
|
||||
}
|
||||
|
||||
render(){
|
||||
if (this.state.error !== ""){
|
||||
return (
|
||||
<div className="alert alert-warning alert-dismissible">
|
||||
<button type="button" className="close" aria-label="Close" onClick={this.close}><span aria-hidden="true">×</span></button>
|
||||
{this.state.error}
|
||||
</div>
|
||||
);
|
||||
}else{
|
||||
return (<div></div>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorMessage;
|
|
@ -61,7 +61,7 @@ class TaskList extends React.Component {
|
|||
{message}
|
||||
|
||||
{this.state.tasks.map(task => (
|
||||
<TaskListItem data={task} key={task.id} />
|
||||
<TaskListItem data={task} key={task.id} refreshInterval={3000} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,6 +2,8 @@ import React from 'react';
|
|||
import '../css/TaskListItem.scss';
|
||||
import Console from '../Console';
|
||||
import statusCodes from '../classes/StatusCodes';
|
||||
import pendingActions from '../classes/PendingActions';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
|
||||
class TaskListItem extends React.Component {
|
||||
constructor(props){
|
||||
|
@ -10,7 +12,9 @@ class TaskListItem extends React.Component {
|
|||
this.state = {
|
||||
expanded: false,
|
||||
task: {},
|
||||
time: props.data.processing_time
|
||||
time: props.data.processing_time,
|
||||
actionError: "",
|
||||
actionButtonsDisabled: false
|
||||
}
|
||||
|
||||
for (let k in props.data){
|
||||
|
@ -44,42 +48,40 @@ class TaskListItem extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount(){
|
||||
let interval = 3000;
|
||||
|
||||
const refresh = () => {
|
||||
// Fetch
|
||||
this.refreshRequest = $.getJSON(`/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/`, json => {
|
||||
if (json.id){
|
||||
let oldStatus = this.state.task.status;
|
||||
|
||||
this.setState({task: json});
|
||||
|
||||
// Update timer if we switched to running
|
||||
if (oldStatus !== this.state.task.status){
|
||||
if (this.state.task.status === statusCodes.RUNNING){
|
||||
this.loadTimer(this.state.task.processing_time);
|
||||
}else{
|
||||
this.setState({time: this.state.task.processing_time});
|
||||
this.unloadTimer();
|
||||
}
|
||||
}
|
||||
}else{
|
||||
console.warn("Cannot refresh task: " + json);
|
||||
}
|
||||
})
|
||||
.always((_, textStatus) => {
|
||||
if (textStatus !== "abort"){
|
||||
if (this.shouldRefresh()) this.refreshTimeout = setTimeout(refresh, interval);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (this.shouldRefresh()) this.refreshTimeout = setTimeout(refresh, interval);
|
||||
if (this.shouldRefresh()) this.refreshTimeout = setTimeout(() => this.refresh(), this.props.refreshInterval || 3000);
|
||||
|
||||
// Load timer if we are in running state
|
||||
if (this.state.task.status === statusCodes.RUNNING) this.loadTimer(this.state.task.processing_time);
|
||||
}
|
||||
|
||||
refresh(){
|
||||
// Fetch
|
||||
this.refreshRequest = $.getJSON(`/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/`, json => {
|
||||
if (json.id){
|
||||
let oldStatus = this.state.task.status;
|
||||
|
||||
this.setState({task: json, actionButtonsDisabled: false});
|
||||
|
||||
// Update timer if we switched to running
|
||||
if (oldStatus !== this.state.task.status){
|
||||
if (this.state.task.status === statusCodes.RUNNING){
|
||||
this.loadTimer(this.state.task.processing_time);
|
||||
}else{
|
||||
this.setState({time: this.state.task.processing_time});
|
||||
this.unloadTimer();
|
||||
}
|
||||
}
|
||||
}else{
|
||||
console.warn("Cannot refresh task: " + json);
|
||||
}
|
||||
})
|
||||
.always((_, textStatus) => {
|
||||
if (textStatus !== "abort"){
|
||||
if (this.shouldRefresh()) this.refreshTimeout = setTimeout(() => this.refresh(), this.props.refreshInterval || 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
this.unloadTimer();
|
||||
if (this.refreshTimeout) clearTimeout(this.refreshTimeout);
|
||||
|
@ -119,21 +121,69 @@ class TaskListItem extends React.Component {
|
|||
render() {
|
||||
let name = this.state.task.name !== null ? this.state.task.name : `Task #${this.state.task.id}`;
|
||||
|
||||
let status = statusCodes.description(this.state.task.status);
|
||||
if (status === "") status = "Uploading images";
|
||||
if (this.state.task.pending_action !== null) status = pendingActions.description(this.state.task.pending_action);
|
||||
|
||||
let expanded = "";
|
||||
if (this.state.expanded){
|
||||
let actionButtons = [];
|
||||
const addActionButton = (label, className, icon, onClick) => {
|
||||
actionButtons.push({
|
||||
className, icon, label, onClick
|
||||
});
|
||||
};
|
||||
const genActionApiCall = (action) => {
|
||||
return () => {
|
||||
this.setState({actionButtonsDisabled: true});
|
||||
|
||||
let actionButtons = "";
|
||||
actionButtons = (<div>
|
||||
<button type="button" className="btn btn-primary btn-sm">
|
||||
<i className="glyphicon glyphicon-remove-circle"></i>
|
||||
Restart
|
||||
</button> <button type="button" className="btn btn-primary btn-sm">
|
||||
<i className="glyphicon glyphicon-pencil"></i>
|
||||
Edit
|
||||
</button> <button type="button" className="btn btn-danger btn-sm">
|
||||
<i className="glyphicon glyphicon-trash"></i>
|
||||
Delete
|
||||
</button>
|
||||
$.post(`/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/${action}/`,
|
||||
{
|
||||
uuid: this.state.task.uuid
|
||||
}
|
||||
).done(json => {
|
||||
if (json.success){
|
||||
this.refresh();
|
||||
}else{
|
||||
this.setState({
|
||||
actionError: json.error,
|
||||
actionButtonsDisabled: false
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
this.setState({
|
||||
error: url + " is unreachable.",
|
||||
actionButtonsDisabled: false
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
if ([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(this.state.task.status) !== -1){
|
||||
addActionButton("Cancel", "btn-primary", "glyphicon glyphicon-remove-circle", genActionApiCall("cancel"));
|
||||
}
|
||||
|
||||
// addActionButton("Restart", "btn-primary", "glyphicon glyphicon-play", genActionApiCall("cancel"));
|
||||
|
||||
// addActionButton("Edit", "btn-primary", "glyphicon glyphicon-pencil", () => {
|
||||
// console.log("edit call");
|
||||
// });
|
||||
|
||||
// addActionButton("Delete", "btn-danger", "glyphicon glyphicon-trash", () => {
|
||||
// console.log("Delete call");
|
||||
// });
|
||||
|
||||
actionButtons = (<div className="action-buttons">
|
||||
{actionButtons.map(button => {
|
||||
return (
|
||||
<button key={button.label} type="button" className={"btn btn-sm " + button.className} onClick={button.onClick} disabled={this.state.actionButtonsDisabled || !!this.state.task.pending_action}>
|
||||
<i className={button.icon}></i>
|
||||
{button.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>);
|
||||
|
||||
expanded = (
|
||||
|
@ -141,10 +191,10 @@ class TaskListItem extends React.Component {
|
|||
<div className="row">
|
||||
<div className="col-md-4 no-padding">
|
||||
<div className="labels">
|
||||
<strong>Created at: </strong> {(new Date(this.state.task.created_at)).toLocaleString()}<br/>
|
||||
<strong>Created on: </strong> {(new Date(this.state.task.created_at)).toLocaleString()}<br/>
|
||||
</div>
|
||||
<div className="labels">
|
||||
<strong>Status: </strong> {statusCodes.description(this.state.task.status) || "Preprocessing"}<br/>
|
||||
<strong>Status: </strong> {status}<br/>
|
||||
</div>
|
||||
{/* TODO: List of images? */}
|
||||
</div>
|
||||
|
@ -157,6 +207,7 @@ class TaskListItem extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<ErrorMessage message={this.state.actionError} />
|
||||
{actionButtons}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -176,9 +227,7 @@ class TaskListItem extends React.Component {
|
|||
statusLabel = getStatusLabel("Processing node not set");
|
||||
statusIcon = "fa fa-hourglass-3";
|
||||
}else{
|
||||
let descr = statusCodes.description(this.state.task.status);
|
||||
if (descr === "") descr = "Preprocessing";
|
||||
statusLabel = getStatusLabel(descr, this.state.task.status == 40 ? "done" : "");
|
||||
statusLabel = getStatusLabel(status, this.state.task.status == 40 ? "done" : "");
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -58,5 +58,11 @@
|
|||
padding-top: 4px;
|
||||
padding-bottom: 16px;
|
||||
padding-left: 16px;
|
||||
|
||||
.action-buttons{
|
||||
button{
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue