Task UI improvements, PoC move task dialog

pull/1031/head
Piero Toffanin 2021-08-04 13:09:27 -04:00
rodzic 4daa261f7d
commit 9a70c07aca
8 zmienionych plików z 207 dodań i 37 usunięć

Wyświetl plik

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

Wyświetl plik

@ -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]: {

Wyświetl plik

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

Wyświetl plik

@ -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 (
<FormDialog {...this.props}
getFormData={this.getFormData}
onShow={this.onShow}
ref={(domNode) => { this.dialog = domNode; }}>
{!this.state.loading ?
<div className="form-group">
<label className="col-sm-2 control-label">{_("Project")}</label>
<div className="col-sm-10">
<select className="form-control"
value={this.state.projectId}
onChange={this.handleProjectChange}>
{this.state.projects.map(p =>
<option value={p.id} key={p.id}>{p.name}</option>
)}
</select>
</div>
</div>
: <i className="fa fa-circle-notch fa-spin fa-fw name-loading"></i>}
</FormDialog>
);
}
}
export default MoveTaskDialog;

Wyświetl plik

@ -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}
/> : ""}

Wyświetl plik

@ -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} />
))}
</div>

Wyświetl plik

@ -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 {
</div>;
}
}
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 (<div
className={"status-label theme-border-primary " + type}
style={{background: `linear-gradient(90deg, ${color} ${progress}%, rgba(255, 255, 255, 0) ${progress}%)`}}
title={text}>{text}</div>);
title={text}><i className={statusIcon}></i> {text}</div>);
}
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(
<li key={label}><a href="javascript:void(0)" onClick={onClick}><i className={icon}></i>{label}</a></li>
);
};
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(<li key="edit"><a href="javascript:void(0)" onClick={this.startEditing}><i className="glyphicon glyphicon-pencil"></i>{_("Edit")}</a></li>);
}
if (editable){
taskActions.push(
<li key="move"><a href="javascript:void(0)" onClick={this.handleMoveTask}><i className="fa fa-arrows-alt"></i>{_("Move")}</a></li>,
<li key="duplicate"><a href="javascript:void(0)"><i className="fa fa-copy"></i>{_("Duplicate")}</a></li>
);
}
}
if (this.props.hasPermission("delete")){
taskActions.push(
<li key="sep" role="separator" className="divider"></li>,
);
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 (
<div className="task-list-item">
{this.state.showMoveDialog ?
<MoveTaskDialog
task={task}
ref={(domNode) => { this.moveTaskDialog = domNode; }}
onHide={() => this.setState({showMoveDialog: false})}
saveAction={() => {}}
/>
: ""}
<div className="row">
<div className="col-sm-5 name">
<i onClick={this.toggleExpanded} className={"clickable far " + (this.state.expanded ? "fa-minus-square" : " fa-plus-square")}></i> <a href="javascript:void(0);" onClick={this.toggleExpanded}>{name}</a>
@ -642,9 +680,16 @@ class TaskListItem extends React.Component {
: statusLabel}
</div>
<div className="col-sm-1 text-right">
<div className="status-icon">
<i className={statusIcon}></i>
</div>
{taskActions.length > 0 ?
<div className="btn-group">
<button disabled={this.state.actionButtonsDisabled} className="btn task-actions btn-secondary btn-xs dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i className="fa fa-ellipsis-h"></i>
</button>
<ul className="dropdown-menu dropdown-menu-right">
{taskActions}
</ul>
</div>
: ""}
</div>
</div>
{expanded}

Wyświetl plik

@ -31,11 +31,8 @@
}
}
.status-icon{
margin-top: 3px;
.fa-cog{
width: auto;
}
.fa-cog{
width: auto;
}
.status-label{