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'; import EditTaskPanel from './EditTaskPanel'; 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'; import { _, interpolate } from '../classes/gettext'; class TaskListItem extends React.Component { static propTypes = { history: PropTypes.object.isRequired, data: PropTypes.object.isRequired, // task json refreshInterval: PropTypes.number, // how often to refresh info onDelete: PropTypes.func, onMove: PropTypes.func, hasPermission: PropTypes.func } constructor(props){ super(); this.historyNav = new HistoryNav(props.history); this.state = { expanded: this.historyNav.isValueInQSList("project_task_expanded", props.data.id), task: {}, time: props.data.processing_time, actionError: "", actionButtonsDisabled: false, editing: false, memoryError: false, friendlyTaskError: "", pluginActionButtons: [], view: "basic", showMoveDialog: false } for (let k in props.data){ this.state.task[k] = props.data[k]; } this.toggleExpanded = this.toggleExpanded.bind(this); this.consoleOutputUrl = this.consoleOutputUrl.bind(this); this.stopEditing = this.stopEditing.bind(this); this.startEditing = this.startEditing.bind(this); this.checkForCommonErrors = this.checkForCommonErrors.bind(this); this.handleEditTaskSave = this.handleEditTaskSave.bind(this); this.setView = this.setView.bind(this); // Retrieve CSS values for status bar colors this.backgroundSuccessColor = Css.getValue('theme-background-success', 'backgroundColor'); this.backgroundFailedColor = Css.getValue('theme-background-failed', 'backgroundColor'); } shouldRefresh(){ if (this.state.task.pending_action !== null) return true; // If a task is completed, or failed, etc. we don't expect it to change if ([statusCodes.COMPLETED, statusCodes.FAILED, statusCodes.CANCELED].indexOf(this.state.task.status) !== -1) return false; return (([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(this.state.task.status) !== -1 && this.state.task.processing_node) || (!this.state.task.uuid && this.state.task.processing_node && !this.state.task.last_error)); } loadTimer(startTime){ if (!this.processingTimeInterval){ this.setState({time: startTime}); this.processingTimeInterval = setInterval(() => { this.setState({time: this.state.time += 1000}); }, 1000); } } setView(type){ return () => { this.setState({view: type}); } } unloadTimer(){ if (this.processingTimeInterval){ clearInterval(this.processingTimeInterval); this.processingTimeInterval = null; } if (this.state.task.processing_time) this.setState({time: this.state.task.processing_time}); } componentDidMount(){ 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){ if (this.console) this.console.clear(); if (this.basicView) this.basicView.reset(); this.loadTimer(this.state.task.processing_time); }else{ this.setState({time: this.state.task.processing_time}); this.unloadTimer(); } if (this.state.task.status !== statusCodes.FAILED){ this.setState({memoryError: false, friendlyTaskError: ""}); } } }else{ console.warn("Cannot refresh task: " + json); } this.setAutoRefresh(); }) .fail(( _, __, errorThrown) => { if (errorThrown === "Not Found"){ // Don't translate this one // Assume this has been deleted if (this.props.onDelete) this.props.onDelete(this.state.task.id); }else{ this.setAutoRefresh(); } }); } setAutoRefresh(){ if (this.shouldRefresh()) this.refreshTimeout = setTimeout(() => this.refresh(), this.props.refreshInterval || 3000); } componentWillUnmount(){ this.unloadTimer(); if (this.refreshRequest) this.refreshRequest.abort(); if (this.refreshTimeout) clearTimeout(this.refreshTimeout); } toggleExpanded(){ const expanded = !this.state.expanded; this.historyNav.toggleQSListItem("project_task_expanded", this.props.data.id, expanded); this.setState({ expanded: expanded }); } consoleOutputUrl(line){ return `/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/output/?line=${line}`; } hoursMinutesSecs(t){ if (t === 0 || t === -1) return "-- : -- : --"; let ch = 60 * 60 * 1000, cm = 60 * 1000, h = Math.floor(t / ch), m = Math.floor( (t - h * ch) / cm), s = Math.round( (t - h * ch - m * cm) / 1000), pad = function(n){ return n < 10 ? '0' + n : n; }; if( s === 60 ){ m++; s = 0; } if( m === 60 ){ h++; m = 0; } return [pad(h), pad(m), pad(s)].join(':'); } genActionApiCall(action, options = {}){ return () => { const doAction = () => { this.setState({actionButtonsDisabled: true}); let url = `/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/${action}/`; $.post(url, { uuid: this.state.task.uuid } ).done(json => { if (json.success){ this.refresh(); if (options.success !== undefined) options.success(); }else{ this.setState({ actionError: json.error || options.defaultError || _("Cannot complete operation."), actionButtonsDisabled: false }); } }) .fail(() => { this.setState({ actionError: options.defaultError || _("Cannot complete operation."), actionButtonsDisabled: false }); }); } if (options.confirm){ if (window.confirm(options.confirm)){ doAction(); } }else{ doAction(); } }; } optionsToList(options){ if (!Array.isArray(options)) return ""; else if (options.length === 0) return "Default"; else { return options.map(opt => `${opt.name}: ${opt.value}`).join(", "); } } startEditing(){ this.setState({expanded: true, editing: true}); } stopEditing(){ this.setState({editing: false}); } checkForCommonErrors(lines){ for (let line of lines){ if (line.indexOf("Killed") !== -1 || line.indexOf("MemoryError") !== -1 || line.indexOf("std::bad_alloc") !== -1 || line.indexOf("Child returned 137") !== -1 || line.indexOf("loky.process_executor.TerminatedWorkerError:") !== -1 || line.indexOf("Failed to allocate memory") !== -1){ this.setState({memoryError: true}); }else if (line.indexOf("SVD did not converge") !== -1 || line.indexOf("0 partial reconstructions in total") !== -1){ this.setState({friendlyTaskError: interpolate(_("It looks like there might be one of the following problems: %(problems)s You can read more about best practices for capturing good images %(link)s."), { problems: ``, link: `${_("here")}`})}); }else if (line.indexOf("Illegal instruction") !== -1 || line.indexOf("Child returned 132") !== -1){ this.setState({friendlyTaskError: interpolate(_("It looks like this computer might be too old. WebODM requires a computer with a 64-bit CPU supporting MMX, SSE, SSE2, SSE3 and SSSE3 instruction set support or higher. You can still run WebODM if you compile your own docker images. See %(link)s for more information."), { link: `${_("this page")}` } )}); }else if (line.indexOf("Child returned 127") !== -1){ this.setState({friendlyTaskError: _("The processing node is missing a program necessary to complete the task. This might indicate a corrupted installation. If you built OpenDroneMap, please check that all programs built without errors.")}); } } } isMacOS(){ return window.navigator.platform === "MacIntel"; } handleEditTaskSave(task){ this.setState({task, editing: false}); this.setAutoRefresh(); } handleMoveTask = () => { this.setState({showMoveDialog: true}); } getRestartSubmenuItems(){ const { task } = this.state; // Map rerun-from parameters to display items const rfMap = {}; PipelineSteps.get().forEach(rf => rfMap[rf.action] = rf); // Create onClick handlers for (let rfParam in rfMap){ rfMap[rfParam].label = interpolate(_("From %(stage)s"), { stage: rfMap[rfParam].label}); rfMap[rfParam].onClick = this.genRestartAction(rfParam); } let items = task.can_rerun_from .map(rf => rfMap[rf]) .filter(rf => rf !== undefined); if (items.length > 0 && [statusCodes.CANCELED, statusCodes.FAILED].indexOf(task.status) !== -1){ // Add resume "pseudo button" to help users understand // how to resume a task that failed for memory/disk issues. items.unshift({ label: _("Resume Processing"), icon: "fa fa-bolt", onClick: this.genRestartAction(task.can_rerun_from[task.can_rerun_from.length - 1]) }); } return items; } genRestartAction(rerunFrom = null){ const { task } = this.state; const restartAction = this.genActionApiCall("restart", { success: () => { this.setState({time: -1}); }, defaultError: _("Cannot restart task.") } ); const setTaskRerunFrom = (value) => { this.setState({actionButtonsDisabled: true}); // Removing rerun-from? if (value === null){ task.options = task.options.filter(opt => opt['name'] !== 'rerun-from'); }else{ // Adding rerun-from let opt = null; if (opt = task.options.find(opt => opt['name'] === 'rerun-from')){ opt['value'] = value; }else{ // Not in existing list of options, append task.options.push({ name: 'rerun-from', value: value }); } } let data = { options: task.options }; // Force reprocess if (value === null) data.uuid = ''; return $.ajax({ url: `/api/projects/${task.project}/tasks/${task.id}/`, contentType: 'application/json', data: JSON.stringify(data), dataType: 'json', type: 'PATCH' }).done((taskJson) => { this.setState({task: taskJson}); }) .fail(() => { this.setState({ actionError: interpolate(_("Cannot restart task from (stage)s."), { stage: value || "the start"}), actionButtonsDisabled: false }); }); }; return () => { setTaskRerunFrom(rerunFrom) .then(restartAction); }; } moveTaskAction = (formData) => { if (formData.project !== this.state.task.project){ return $.ajax({ url: `/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/`, contentType: 'application/json', data: JSON.stringify(formData), dataType: 'json', type: 'PATCH' }).done(this.props.onMove); }else return false; } render() { const task = this.state.task; const name = task.name !== null ? task.name : interpolate(_("Task #%(number)s"), { number: task.id }); const imported = task.import_url !== ""; let status = statusCodes.description(task.status); if (status === "") status = _("Uploading images to processing node"); if (!task.processing_node && !imported) status = _("Waiting for a node..."); if (task.pending_action !== null) status = pendingActions.description(task.pending_action); let expanded = ""; if (this.state.expanded){ let showOrthophotoMissingWarning = false, showMemoryErrorWarning = this.state.memoryError && task.status == statusCodes.FAILED, showTaskWarning = this.state.friendlyTaskError !== "" && task.status == statusCodes.FAILED, showExitedWithCodeOneHints = task.last_error === "Process exited with code 1" && !showMemoryErrorWarning && !showTaskWarning && task.status == statusCodes.FAILED, memoryErrorLink = this.isMacOS() ? "http://stackoverflow.com/a/39720010" : "https://docs.docker.com/docker-for-windows/#advanced"; let actionButtons = []; const addActionButton = (label, className, icon, onClick, options = {}) => { actionButtons.push({ className, icon, label, onClick, options }); }; if (task.status === statusCodes.COMPLETED){ if (task.available_assets.indexOf("orthophoto.tif") !== -1){ addActionButton(" " + _("View Map"), "btn-primary", "fa fa-globe", () => { location.href = `/map/project/${task.project}/task/${task.id}/`; }); }else{ showOrthophotoMissingWarning = true; } addActionButton(" " + _("View 3D Model"), "btn-primary", "fa fa-cube", () => { location.href = `/3d/project/${task.project}/task/${task.id}/`; }); } if ([statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1 && task.processing_node && !imported){ // By default restart reruns every pipeline // step from the beginning const rerunFrom = task.can_rerun_from.length > 1 ? task.can_rerun_from[1] : null; addActionButton(_("Restart"), "btn-primary", "glyphicon glyphicon-repeat", this.genRestartAction(rerunFrom), { subItems: this.getRestartSubmenuItems() }); } const disabled = this.state.actionButtonsDisabled || ([pendingActions.CANCEL, pendingActions.REMOVE, pendingActions.RESTART].indexOf(task.pending_action) !== -1); actionButtons = (
{task.status === statusCodes.COMPLETED ? : ""} {actionButtons.map(button => { const subItems = button.options.subItems || []; const className = button.options.className || ""; let buttonHtml = (); if (subItems.length > 0){ // The button expands sub items buttonHtml = (); } return (
0 ? "btn-group" : "") + " " + className}> {buttonHtml} {subItems.length > 0 && [, ]}
); })}
); const stats = task.statistics; expanded = (
{_("Task Output:")}
{_("Created on:")} {(new Date(task.created_at)).toLocaleString()}
{_("Processing Node:")} {task.processing_node_name || "-"} ({task.auto_processing_node ? _("auto") : _("manual")})
{Array.isArray(task.options) ?
{_("Options:")} {this.optionsToList(task.options)}
: ""} {stats && stats.gsd ?
{_("Average GSD:")} {stats.gsd.toFixed(2)} cm
: ""} {stats && stats.area ?
{_("Area:")} {stats.area.toFixed(2)} m²
: ""} {stats && stats.pointcloud && stats.pointcloud.points ?
{_("Reconstructed Points:")} {stats.pointcloud.points.toLocaleString()}
: ""}
{this.state.view === 'console' ? this.console = domNode} onAddLines={this.checkForCommonErrors} showConsoleButtons={true} maximumLines={500} /> : ""} {showOrthophotoMissingWarning ?
{_("An orthophoto could not be generated. To generate one, make sure GPS information is embedded in the EXIF tags of your images, or use a Ground Control Points (GCP) file.")}
: ""} {showMemoryErrorWarning ?
${_("enough RAM allocated")}`, cloudlink: `${_("cloud processing node")}` }}>{_("It looks like your processing node ran out of memory. If you are using docker, make sure that your docker environment has %(memlink)s. Alternatively, make sure you have enough physical RAM, reduce the number of images, make your images smaller, or reduce the max-concurrency parameter from the task's options. You can also try to use a %(cloudlink)s.")}
: ""} {showTaskWarning ?
: ""} {showExitedWithCodeOneHints ?
DroneDB`, link2: `Google Drive`, open_a_topic: `${_("open a topic")}`, }}>{_("\"Process exited with code 1\" means that part of the processing failed. Sometimes it's a problem with the dataset, sometimes it can be solved by tweaking the Task Options and sometimes it might be a bug! If you need help, upload your images somewhere like %(link1)s or %(link2)s and %(open_a_topic)s on our community forum, making sure to include a copy of your task's output. Our awesome contributors will try to help you!")}
: ""}
{actionButtons}
); // If we're editing, the expanded view becomes the edit panel if (this.state.editing){ expanded =
; } } let statusIcon = statusCodes.icon(task.status); // @param type {String} one of: ['neutral', 'done', 'error'] const getStatusLabel = (text, type = 'neutral', progress = 100) => { let color = 'rgba(255, 255, 255, 0.0)'; if (type === 'done') color = this.backgroundSuccessColor; else if (type === 'error') color = this.backgroundFailedColor; return (
{text}
); } let statusLabel = ""; let showEditLink = false; if (task.last_error){ statusLabel = getStatusLabel(task.last_error, 'error'); }else if (!task.processing_node && !imported){ statusLabel = getStatusLabel(_("Set a processing node")); statusIcon = "fa fa-hourglass-3"; showEditLink = true; }else if (task.partial && !task.pending_action){ statusIcon = "fa fa-hourglass-3"; statusLabel = getStatusLabel(_("Waiting for image upload...")); }else{ let progress = 100; let type = 'done'; if (task.pending_action === pendingActions.RESIZE){ progress = task.resize_progress * 100; }else if (task.status === null){ progress = task.upload_progress * 100; }else if (task.status === statusCodes.RUNNING){ progress = task.running_progress * 100; }else if (task.status === statusCodes.FAILED){ type = 'error'; }else if (task.status !== statusCodes.COMPLETED){ type = 'neutral'; } 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 editable = [statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1; if (this.props.hasPermission("change")){ 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={this.moveTaskAction} /> : ""}
    {name}
    {task.images_count}
    {this.hoursMinutesSecs(this.state.time)}
    {showEditLink ? {statusLabel} : statusLabel}
    {taskActions.length > 0 ?
      {taskActions}
    : ""}
    {expanded}
    ); } } export default TaskListItem;