Added faster console rendering, cancel action task

pull/40/head
Piero Toffanin 2016-11-03 13:17:58 -04:00
rodzic 4ffed093e9
commit 955c9af4a6
8 zmienionych plików z 192 dodań i 70 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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">&times;</span></button>
{this.state.error}
</div>
);
}else{
return (<div></div>);
}
}
}
export default ErrorMessage;

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -58,5 +58,11 @@
padding-top: 4px;
padding-bottom: 16px;
padding-left: 16px;
.action-buttons{
button{
margin-right: 4px;
}
}
}
}