kopia lustrzana https://github.com/OpenDroneMap/WebODM
Task UI improvements, PoC move task dialog
rodzic
4daa261f7d
commit
9a70c07aca
|
@ -37,3 +37,9 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectSerializer
|
||||||
queryset = models.Project.objects.prefetch_related('task_set').filter(deleting=False).order_by('-created_at')
|
queryset = models.Project.objects.prefetch_related('task_set').filter(deleting=False).order_by('-created_at')
|
||||||
ordering_fields = '__all__'
|
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)
|
|
@ -12,7 +12,7 @@ let statusCodes = {
|
||||||
icon: "far fa-hourglass fa-fw"
|
icon: "far fa-hourglass fa-fw"
|
||||||
},
|
},
|
||||||
[RUNNING]: {
|
[RUNNING]: {
|
||||||
descr: _("Running"),
|
descr: _("Processing"),
|
||||||
icon: "fa fa-cog fa-spin fa-fw"
|
icon: "fa fa-cog fa-spin fa-fw"
|
||||||
},
|
},
|
||||||
[FAILED]: {
|
[FAILED]: {
|
||||||
|
|
|
@ -17,7 +17,7 @@ class FormDialog extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
getFormData: PropTypes.func.isRequired,
|
getFormData: PropTypes.func.isRequired,
|
||||||
reset: PropTypes.func.isRequired,
|
reset: PropTypes.func,
|
||||||
saveAction: PropTypes.func.isRequired,
|
saveAction: PropTypes.func.isRequired,
|
||||||
onShow: PropTypes.func,
|
onShow: PropTypes.func,
|
||||||
onHide: PropTypes.func,
|
onHide: PropTypes.func,
|
||||||
|
@ -88,7 +88,7 @@ class FormDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
show(){
|
show(){
|
||||||
this.props.reset();
|
if (this.props.reset) this.props.reset();
|
||||||
this.setState({showModal: true, saving: false, error: ""});
|
this.setState({showModal: true, saving: false, error: ""});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
@ -47,6 +47,7 @@ class ProjectListItem extends React.Component {
|
||||||
this.handleEditProject = this.handleEditProject.bind(this);
|
this.handleEditProject = this.handleEditProject.bind(this);
|
||||||
this.updateProject = this.updateProject.bind(this);
|
this.updateProject = this.updateProject.bind(this);
|
||||||
this.taskDeleted = this.taskDeleted.bind(this);
|
this.taskDeleted = this.taskDeleted.bind(this);
|
||||||
|
this.taskMoved = this.taskMoved.bind(this);
|
||||||
this.hasPermission = this.hasPermission.bind(this);
|
this.hasPermission = this.hasPermission.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,6 +305,10 @@ class ProjectListItem extends React.Component {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
taskMoved(){
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
handleDelete(){
|
handleDelete(){
|
||||||
return $.ajax({
|
return $.ajax({
|
||||||
url: `/api/projects/${this.state.data.id}/`,
|
url: `/api/projects/${this.state.data.id}/`,
|
||||||
|
@ -570,6 +575,8 @@ class ProjectListItem extends React.Component {
|
||||||
ref={this.setRef("taskList")}
|
ref={this.setRef("taskList")}
|
||||||
source={`/api/projects/${data.id}/tasks/?ordering=-created_at`}
|
source={`/api/projects/${data.id}/tasks/?ordering=-created_at`}
|
||||||
onDelete={this.taskDeleted}
|
onDelete={this.taskDeleted}
|
||||||
|
onMove={this.taskMoved}
|
||||||
|
hasPermission={this.hasPermission}
|
||||||
history={this.props.history}
|
history={this.props.history}
|
||||||
/> : ""}
|
/> : ""}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,9 @@ class TaskList extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
source: PropTypes.string.isRequired, // URL where to load task list
|
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){
|
constructor(props){
|
||||||
|
@ -69,6 +71,13 @@ class TaskList extends React.Component {
|
||||||
if (this.props.onDelete) this.props.onDelete(id);
|
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() {
|
render() {
|
||||||
let message = "";
|
let message = "";
|
||||||
if (this.state.loading){
|
if (this.state.loading){
|
||||||
|
@ -88,7 +97,9 @@ class TaskList extends React.Component {
|
||||||
data={task}
|
data={task}
|
||||||
key={task.id}
|
key={task.id}
|
||||||
refreshInterval={3000}
|
refreshInterval={3000}
|
||||||
onDelete={this.deleteTask}
|
onDelete={this.deleteTask}
|
||||||
|
onMove={this.moveTask}
|
||||||
|
hasPermission={this.props.hasPermission}
|
||||||
history={this.props.history} />
|
history={this.props.history} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import AssetDownloadButtons from './AssetDownloadButtons';
|
||||||
import HistoryNav from '../classes/HistoryNav';
|
import HistoryNav from '../classes/HistoryNav';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TaskPluginActionButtons from './TaskPluginActionButtons';
|
import TaskPluginActionButtons from './TaskPluginActionButtons';
|
||||||
|
import MoveTaskDialog from './MoveTaskDialog';
|
||||||
import PipelineSteps from '../classes/PipelineSteps';
|
import PipelineSteps from '../classes/PipelineSteps';
|
||||||
import Css from '../classes/Css';
|
import Css from '../classes/Css';
|
||||||
import Trans from './Trans';
|
import Trans from './Trans';
|
||||||
|
@ -19,7 +20,9 @@ class TaskListItem extends React.Component {
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
data: PropTypes.object.isRequired, // task json
|
data: PropTypes.object.isRequired, // task json
|
||||||
refreshInterval: PropTypes.number, // how often to refresh info
|
refreshInterval: PropTypes.number, // how often to refresh info
|
||||||
onDelete: PropTypes.func
|
onDelete: PropTypes.func,
|
||||||
|
onMove: PropTypes.func,
|
||||||
|
hasPermission: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
|
@ -37,7 +40,8 @@ class TaskListItem extends React.Component {
|
||||||
memoryError: false,
|
memoryError: false,
|
||||||
friendlyTaskError: "",
|
friendlyTaskError: "",
|
||||||
pluginActionButtons: [],
|
pluginActionButtons: [],
|
||||||
view: "basic"
|
view: "basic",
|
||||||
|
showMoveDialog: false
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let k in props.data){
|
for (let k in props.data){
|
||||||
|
@ -271,6 +275,10 @@ class TaskListItem extends React.Component {
|
||||||
this.setAutoRefresh();
|
this.setAutoRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMoveTask = () => {
|
||||||
|
this.setState({showMoveDialog: true});
|
||||||
|
}
|
||||||
|
|
||||||
getRestartSubmenuItems(){
|
getRestartSubmenuItems(){
|
||||||
const { task } = this.state;
|
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 &&
|
if ([statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1 &&
|
||||||
task.processing_node &&
|
task.processing_node &&
|
||||||
!imported){
|
!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 ||
|
const disabled = this.state.actionButtonsDisabled ||
|
||||||
([pendingActions.CANCEL,
|
([pendingActions.CANCEL,
|
||||||
pendingActions.REMOVE,
|
pendingActions.REMOVE,
|
||||||
|
@ -580,6 +568,8 @@ class TaskListItem extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let statusIcon = statusCodes.icon(task.status);
|
||||||
|
|
||||||
// @param type {String} one of: ['neutral', 'done', 'error']
|
// @param type {String} one of: ['neutral', 'done', 'error']
|
||||||
const getStatusLabel = (text, type = 'neutral', progress = 100) => {
|
const getStatusLabel = (text, type = 'neutral', progress = 100) => {
|
||||||
|
@ -589,11 +579,10 @@ class TaskListItem extends React.Component {
|
||||||
return (<div
|
return (<div
|
||||||
className={"status-label theme-border-primary " + type}
|
className={"status-label theme-border-primary " + type}
|
||||||
style={{background: `linear-gradient(90deg, ${color} ${progress}%, rgba(255, 255, 255, 0) ${progress}%)`}}
|
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 statusLabel = "";
|
||||||
let statusIcon = statusCodes.icon(task.status);
|
|
||||||
let showEditLink = false;
|
let showEditLink = false;
|
||||||
|
|
||||||
if (task.last_error){
|
if (task.last_error){
|
||||||
|
@ -624,8 +613,57 @@ class TaskListItem extends React.Component {
|
||||||
statusLabel = getStatusLabel(status, type, progress);
|
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 (
|
return (
|
||||||
<div className="task-list-item">
|
<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="row">
|
||||||
<div className="col-sm-5 name">
|
<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>
|
<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}
|
: statusLabel}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-1 text-right">
|
<div className="col-sm-1 text-right">
|
||||||
<div className="status-icon">
|
{taskActions.length > 0 ?
|
||||||
<i className={statusIcon}></i>
|
<div className="btn-group">
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{expanded}
|
{expanded}
|
||||||
|
|
|
@ -31,11 +31,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-icon{
|
.fa-cog{
|
||||||
margin-top: 3px;
|
width: auto;
|
||||||
.fa-cog{
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-label{
|
.status-label{
|
||||||
|
|
Ładowanie…
Reference in New Issue