diff --git a/app/api/projects.py b/app/api/projects.py
index 8cbd3b04..b1c749b8 100644
--- a/app/api/projects.py
+++ b/app/api/projects.py
@@ -37,3 +37,9 @@ class ProjectViewSet(viewsets.ModelViewSet):
serializer_class = ProjectSerializer
queryset = models.Project.objects.prefetch_related('task_set').filter(deleting=False).order_by('-created_at')
ordering_fields = '__all__'
+
+ # Disable pagination when not requesting any page
+ def paginate_queryset(self, queryset):
+ if self.paginator and self.request.query_params.get(self.paginator.page_query_param, None) is None:
+ return None
+ return super().paginate_queryset(queryset)
\ No newline at end of file
diff --git a/app/static/app/js/classes/StatusCodes.js b/app/static/app/js/classes/StatusCodes.js
index 7339230f..6e0cce90 100644
--- a/app/static/app/js/classes/StatusCodes.js
+++ b/app/static/app/js/classes/StatusCodes.js
@@ -12,7 +12,7 @@ let statusCodes = {
icon: "far fa-hourglass fa-fw"
},
[RUNNING]: {
- descr: _("Running"),
+ descr: _("Processing"),
icon: "fa fa-cog fa-spin fa-fw"
},
[FAILED]: {
diff --git a/app/static/app/js/components/FormDialog.jsx b/app/static/app/js/components/FormDialog.jsx
index e82c1d88..5597710c 100644
--- a/app/static/app/js/components/FormDialog.jsx
+++ b/app/static/app/js/components/FormDialog.jsx
@@ -17,7 +17,7 @@ class FormDialog extends React.Component {
static propTypes = {
getFormData: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
+ reset: PropTypes.func,
saveAction: PropTypes.func.isRequired,
onShow: PropTypes.func,
onHide: PropTypes.func,
@@ -88,7 +88,7 @@ class FormDialog extends React.Component {
}
show(){
- this.props.reset();
+ if (this.props.reset) this.props.reset();
this.setState({showModal: true, saving: false, error: ""});
}
diff --git a/app/static/app/js/components/MoveTaskDialog.jsx b/app/static/app/js/components/MoveTaskDialog.jsx
new file mode 100644
index 00000000..734bb097
--- /dev/null
+++ b/app/static/app/js/components/MoveTaskDialog.jsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import FormDialog from './FormDialog';
+import PropTypes from 'prop-types';
+import { _ } from '../classes/gettext';
+import $ from 'jquery';
+
+class MoveTaskDialog extends React.Component {
+ static defaultProps = {
+ title: _("Move Task"),
+ saveLabel: _("Save Changes"),
+ savingLabel: _("Moving..."),
+ saveIcon: "far fa-edit",
+ show: true
+ };
+
+ static propTypes = {
+ task: PropTypes.object.isRequired,
+ saveAction: PropTypes.func.isRequired,
+ title: PropTypes.string,
+ saveLabel: PropTypes.string,
+ savingLabel: PropTypes.string,
+ saveIcon: PropTypes.string,
+ show: PropTypes.bool
+ };
+
+ constructor(props){
+ super(props);
+
+ this.state = {
+ projectId: props.task.project.id,
+ projects: [],
+ loading: true
+ };
+
+ this.getFormData = this.getFormData.bind(this);
+ this.onShow = this.onShow.bind(this);
+ }
+
+ getFormData(){
+ return this.state;
+ }
+
+ onShow(){
+ this.setState({loading: true, projects: []});
+
+ // Load projects from API
+ this.serverRequest = $.getJSON(`/api/projects/?ordering=-created_at`, json => {
+ this.setState({
+ projects: json.filter(p => p.permissions.indexOf("add") !== -1)
+ });
+ })
+ .fail((jqXHR, textStatus, errorThrown) => {
+ this.dialog.setState({
+ error: interpolate(_("Could not load projects list: %(error)s"), {error: textStatus})
+ });
+ })
+ .always(() => {
+ this.setState({loading: false});
+ this.serverRequest = null;
+ });
+ }
+
+ show(){
+ this.dialog.show();
+ }
+
+ hide(){
+ this.dialog.hide();
+ }
+
+ componentWillUnmount(){
+ if (this.serverRquest) this.serverRquest.abort();
+ }
+
+ handleProjectChange = e => {
+ this.setState({projectId: e.target.value});
+ }
+
+ render(){
+ return (
+ { this.dialog = domNode; }}>
+ {!this.state.loading ?
+
+
+
+
+
+
+ : }
+
+ );
+ }
+}
+
+export default MoveTaskDialog;
\ No newline at end of file
diff --git a/app/static/app/js/components/ProjectListItem.jsx b/app/static/app/js/components/ProjectListItem.jsx
index 4b26fc1f..eb49af61 100644
--- a/app/static/app/js/components/ProjectListItem.jsx
+++ b/app/static/app/js/components/ProjectListItem.jsx
@@ -47,6 +47,7 @@ class ProjectListItem extends React.Component {
this.handleEditProject = this.handleEditProject.bind(this);
this.updateProject = this.updateProject.bind(this);
this.taskDeleted = this.taskDeleted.bind(this);
+ this.taskMoved = this.taskMoved.bind(this);
this.hasPermission = this.hasPermission.bind(this);
}
@@ -304,6 +305,10 @@ class ProjectListItem extends React.Component {
this.refresh();
}
+ taskMoved(){
+ this.refresh();
+ }
+
handleDelete(){
return $.ajax({
url: `/api/projects/${this.state.data.id}/`,
@@ -570,6 +575,8 @@ class ProjectListItem extends React.Component {
ref={this.setRef("taskList")}
source={`/api/projects/${data.id}/tasks/?ordering=-created_at`}
onDelete={this.taskDeleted}
+ onMove={this.taskMoved}
+ hasPermission={this.hasPermission}
history={this.props.history}
/> : ""}
diff --git a/app/static/app/js/components/TaskList.jsx b/app/static/app/js/components/TaskList.jsx
index 330c535b..6d6279a5 100644
--- a/app/static/app/js/components/TaskList.jsx
+++ b/app/static/app/js/components/TaskList.jsx
@@ -9,7 +9,9 @@ class TaskList extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired,
source: PropTypes.string.isRequired, // URL where to load task list
- onDelete: PropTypes.func
+ onDelete: PropTypes.func,
+ onMove: PropTypes.func,
+ hasPermission: PropTypes.func.isRequired
}
constructor(props){
@@ -69,6 +71,13 @@ class TaskList extends React.Component {
if (this.props.onDelete) this.props.onDelete(id);
}
+ moveTask(id){
+ this.setState({
+ tasks: this.state.tasks.filter(t => t.id !== id)
+ });
+ if (this.props.onMove) this.props.onMove(id);
+ }
+
render() {
let message = "";
if (this.state.loading){
@@ -88,7 +97,9 @@ class TaskList extends React.Component {
data={task}
key={task.id}
refreshInterval={3000}
- onDelete={this.deleteTask}
+ onDelete={this.deleteTask}
+ onMove={this.moveTask}
+ hasPermission={this.props.hasPermission}
history={this.props.history} />
))}
diff --git a/app/static/app/js/components/TaskListItem.jsx b/app/static/app/js/components/TaskListItem.jsx
index 145d7537..d76f05f5 100644
--- a/app/static/app/js/components/TaskListItem.jsx
+++ b/app/static/app/js/components/TaskListItem.jsx
@@ -9,6 +9,7 @@ import AssetDownloadButtons from './AssetDownloadButtons';
import HistoryNav from '../classes/HistoryNav';
import PropTypes from 'prop-types';
import TaskPluginActionButtons from './TaskPluginActionButtons';
+import MoveTaskDialog from './MoveTaskDialog';
import PipelineSteps from '../classes/PipelineSteps';
import Css from '../classes/Css';
import Trans from './Trans';
@@ -19,7 +20,9 @@ class TaskListItem extends React.Component {
history: PropTypes.object.isRequired,
data: PropTypes.object.isRequired, // task json
refreshInterval: PropTypes.number, // how often to refresh info
- onDelete: PropTypes.func
+ onDelete: PropTypes.func,
+ onMove: PropTypes.func,
+ hasPermission: PropTypes.func
}
constructor(props){
@@ -37,7 +40,8 @@ class TaskListItem extends React.Component {
memoryError: false,
friendlyTaskError: "",
pluginActionButtons: [],
- view: "basic"
+ view: "basic",
+ showMoveDialog: false
}
for (let k in props.data){
@@ -271,6 +275,10 @@ class TaskListItem extends React.Component {
this.setAutoRefresh();
}
+ handleMoveTask = () => {
+ this.setState({showMoveDialog: true});
+ }
+
getRestartSubmenuItems(){
const { task } = this.state;
@@ -406,21 +414,6 @@ class TaskListItem extends React.Component {
});
}
- // Ability to change options
- if ([statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1 ||
- (!task.processing_node)){
- addActionButton(_("Edit"), "btn-primary pull-right edit-button", "glyphicon glyphicon-pencil", () => {
- this.startEditing();
- }, {
- className: "inline"
- });
- }
-
- if ([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(task.status) !== -1 &&
- (task.processing_node || imported)){
- addActionButton(_("Cancel"), "btn-primary", "glyphicon glyphicon-remove-circle", this.genActionApiCall("cancel", {defaultError: _("Cannot cancel task.")}));
- }
-
if ([statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1 &&
task.processing_node &&
!imported){
@@ -435,11 +428,6 @@ class TaskListItem extends React.Component {
});
}
- addActionButton(_("Delete"), "btn-danger", "glyphicon glyphicon-trash", this.genActionApiCall("remove", {
- confirm: _("All information related to this task, including images, maps and models will be deleted. Continue?"),
- defaultError: _("Cannot delete task.")
- }));
-
const disabled = this.state.actionButtonsDisabled ||
([pendingActions.CANCEL,
pendingActions.REMOVE,
@@ -580,6 +568,8 @@ class TaskListItem extends React.Component {
;
}
}
+
+ let statusIcon = statusCodes.icon(task.status);
// @param type {String} one of: ['neutral', 'done', 'error']
const getStatusLabel = (text, type = 'neutral', progress = 100) => {
@@ -589,11 +579,10 @@ class TaskListItem extends React.Component {
return (
{text}
);
+ title={text}> {text});
}
let statusLabel = "";
- let statusIcon = statusCodes.icon(task.status);
let showEditLink = false;
if (task.last_error){
@@ -624,8 +613,57 @@ class TaskListItem extends React.Component {
statusLabel = getStatusLabel(status, type, progress);
}
+ const taskActions = [];
+ const addTaskAction = (label, icon, onClick) => {
+ taskActions.push(
+ {label}
+ );
+ };
+
+ if ([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(task.status) !== -1 &&
+ (task.processing_node || imported) && this.props.hasPermission("change")){
+ addTaskAction(_("Cancel"), "glyphicon glyphicon-remove-circle", this.genActionApiCall("cancel", {defaultError: _("Cannot cancel task.")}));
+ }
+
+ // Ability to change options
+ const canAddDelPerms = this.props.hasPermission("add") && this.props.hasPermission("delete");
+ const editable = [statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1;
+
+ if (canAddDelPerms){
+ if (editable || (!task.processing_node)){
+ taskActions.push({_("Edit")});
+ }
+
+ if (editable){
+ taskActions.push(
+ {_("Move")},
+ {_("Duplicate")}
+ );
+ }
+ }
+
+
+ if (this.props.hasPermission("delete")){
+ taskActions.push(
+ ,
+ );
+
+ addTaskAction(_("Delete"), "glyphicon glyphicon-trash", this.genActionApiCall("remove", {
+ confirm: _("All information related to this task, including images, maps and models will be deleted. Continue?"),
+ defaultError: _("Cannot delete task.")
+ }));
+ }
+
return (
+ {this.state.showMoveDialog ?
+
{ this.moveTaskDialog = domNode; }}
+ onHide={() => this.setState({showMoveDialog: false})}
+ saveAction={() => {}}
+ />
+ : ""}
{name}
@@ -642,9 +680,16 @@ class TaskListItem extends React.Component {
: statusLabel}
-
-
-
+ {taskActions.length > 0 ?
+
+ : ""}
{expanded}
diff --git a/app/static/app/js/css/TaskListItem.scss b/app/static/app/js/css/TaskListItem.scss
index 74847d58..bb34217c 100644
--- a/app/static/app/js/css/TaskListItem.scss
+++ b/app/static/app/js/css/TaskListItem.scss
@@ -31,11 +31,8 @@
}
}
- .status-icon{
- margin-top: 3px;
- .fa-cog{
- width: auto;
- }
+ .fa-cog{
+ width: auto;
}
.status-label{