Merge pull request #539 from aaj013/download-copy-task-outputs

Always show a button to download or copy to clipboard task outputs
pull/542/head
Piero Toffanin 2018-10-19 08:50:06 -05:00 zatwierdzone przez GitHub
commit a223b0968f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
2 zmienionych plików z 57 dodań i 36 usunięć

Wyświetl plik

@ -28,7 +28,7 @@ class Console extends React.Component {
componentDidMount(){ componentDidMount(){
this.checkAutoscroll(); this.checkAutoscroll();
this.setupDynamicSource(); this.setupDynamicSource();
} }
setupDynamicSource(){ setupDynamicSource(){
@ -72,7 +72,7 @@ class Console extends React.Component {
//Firefox requires the link to be in the body //Firefox requires the link to be in the body
document.body.appendChild(link); document.body.appendChild(link);
//simulate click //simulate click
link.click(); link.click();
@ -86,6 +86,16 @@ class Console extends React.Component {
saveAs("data:application/octet-stream," + encodeURIComponent(this.state.lines.join("\r\n")), filename); saveAs("data:application/octet-stream," + encodeURIComponent(this.state.lines.join("\r\n")), filename);
} }
copyTxt(){
const el = document.createElement('textarea');
el.value = this.state.lines.join("\r\n");
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
console.log("Task output copied to clipboard");
}
tearDownDynamicSource(){ tearDownDynamicSource(){
if (this.sourceTimeout) clearTimeout(this.sourceTimeout); if (this.sourceTimeout) clearTimeout(this.sourceTimeout);
if (this.sourceRequest) this.sourceRequest.abort(); if (this.sourceRequest) this.sourceRequest.abort();
@ -131,8 +141,8 @@ class Console extends React.Component {
let i = 0; let i = 0;
return ( return (
<pre className={`console prettyprint <pre className={`console prettyprint
${this.props.lang ? `lang-${this.props.lang}` : ""} ${this.props.lang ? `lang-${this.props.lang}` : ""}
${this.props.lines ? "linenums" : ""}`} ${this.props.lines ? "linenums" : ""}`}
style={{height: (this.props.height ? this.props.height : "auto")}} style={{height: (this.props.height ? this.props.height : "auto")}}
onMouseOver={this.handleMouseOver} onMouseOver={this.handleMouseOver}
@ -151,7 +161,7 @@ class Console extends React.Component {
$(function(){ $(function(){
$("[data-console]").each(function(){ $("[data-console]").each(function(){
window.ReactDOM.render(<Console window.ReactDOM.render(<Console
lang={$(this).data("console-lang")} lang={$(this).data("console-lang")}
height={$(this).data("console-height")} height={$(this).data("console-height")}
autoscroll={typeof $(this).attr("autoscroll") !== 'undefined' && $(this).attr("autoscroll") !== false} autoscroll={typeof $(this).attr("autoscroll") !== 'undefined' && $(this).attr("autoscroll") !== false}

Wyświetl plik

@ -45,6 +45,7 @@ class TaskListItem extends React.Component {
this.startEditing = this.startEditing.bind(this); this.startEditing = this.startEditing.bind(this);
this.checkForCommonErrors = this.checkForCommonErrors.bind(this); this.checkForCommonErrors = this.checkForCommonErrors.bind(this);
this.downloadTaskOutput = this.downloadTaskOutput.bind(this); this.downloadTaskOutput = this.downloadTaskOutput.bind(this);
this.copyTaskOutput = this.copyTaskOutput.bind(this);
this.handleEditTaskSave = this.handleEditTaskSave.bind(this); this.handleEditTaskSave = this.handleEditTaskSave.bind(this);
} }
@ -105,7 +106,7 @@ class TaskListItem extends React.Component {
}else{ }else{
console.warn("Cannot refresh task: " + json); console.warn("Cannot refresh task: " + json);
} }
this.setAutoRefresh(); this.setAutoRefresh();
}) })
.fail(( _, __, errorThrown) => { .fail(( _, __, errorThrown) => {
@ -132,7 +133,7 @@ class TaskListItem extends React.Component {
const expanded = !this.state.expanded; const expanded = !this.state.expanded;
this.historyNav.toggleQSListItem("project_task_expanded", this.props.data.id, expanded); this.historyNav.toggleQSListItem("project_task_expanded", this.props.data.id, expanded);
this.setState({ this.setState({
expanded: expanded expanded: expanded
}); });
@ -205,6 +206,10 @@ class TaskListItem extends React.Component {
this.console.downloadTxt("task_output.txt"); this.console.downloadTxt("task_output.txt");
} }
copyTaskOutput(){
this.console.copyTxt();
}
optionsToList(options){ optionsToList(options){
if (!Array.isArray(options)) return ""; if (!Array.isArray(options)) return "";
else if (options.length === 0) return "Default"; else if (options.length === 0) return "Default";
@ -223,15 +228,15 @@ class TaskListItem extends React.Component {
checkForCommonErrors(lines){ checkForCommonErrors(lines){
for (let line of lines){ for (let line of lines){
if (line.indexOf("Killed") !== -1 || if (line.indexOf("Killed") !== -1 ||
line.indexOf("MemoryError") !== -1 || line.indexOf("MemoryError") !== -1 ||
line.indexOf("std::bad_alloc") !== -1 || line.indexOf("std::bad_alloc") !== -1 ||
line.indexOf("Child returned 137") !== -1 || line.indexOf("Child returned 137") !== -1 ||
line.indexOf("loky.process_executor.TerminatedWorkerError:") !== -1 || line.indexOf("loky.process_executor.TerminatedWorkerError:") !== -1 ||
line.indexOf("Failed to allocate memory") !== -1){ line.indexOf("Failed to allocate memory") !== -1){
this.setState({memoryError: true}); this.setState({memoryError: true});
}else if (line.indexOf("SVD did not converge") !== -1){ }else if (line.indexOf("SVD did not converge") !== -1){
this.setState({friendlyTaskError: `It looks like the images might have one of the following problems: this.setState({friendlyTaskError: `It looks like the images might have one of the following problems:
<ul> <ul>
<li>Not enough images</li> <li>Not enough images</li>
<li>Not enough overlap between images</li> <li>Not enough overlap between images</li>
@ -327,7 +332,7 @@ class TaskListItem extends React.Component {
}); });
} }
} }
let data = { let data = {
options: task.options options: task.options
}; };
@ -373,19 +378,19 @@ class TaskListItem extends React.Component {
let showOrthophotoMissingWarning = false, let showOrthophotoMissingWarning = false,
showMemoryErrorWarning = this.state.memoryError && task.status == statusCodes.FAILED, showMemoryErrorWarning = this.state.memoryError && task.status == statusCodes.FAILED,
showTaskWarning = this.state.friendlyTaskError !== "" && task.status == statusCodes.FAILED, showTaskWarning = this.state.friendlyTaskError !== "" && task.status == statusCodes.FAILED,
showExitedWithCodeOneHints = task.last_error === "Process exited with code 1" && showExitedWithCodeOneHints = task.last_error === "Process exited with code 1" &&
!showMemoryErrorWarning && !showMemoryErrorWarning &&
!showTaskWarning && !showTaskWarning &&
task.status == statusCodes.FAILED, task.status == statusCodes.FAILED,
memoryErrorLink = this.isMacOS() ? "http://stackoverflow.com/a/39720010" : "https://docs.docker.com/docker-for-windows/#advanced"; memoryErrorLink = this.isMacOS() ? "http://stackoverflow.com/a/39720010" : "https://docs.docker.com/docker-for-windows/#advanced";
let actionButtons = []; let actionButtons = [];
const addActionButton = (label, className, icon, onClick, options = {}) => { const addActionButton = (label, className, icon, onClick, options = {}) => {
actionButtons.push({ actionButtons.push({
className, icon, label, onClick, options className, icon, label, onClick, options
}); });
}; };
if (task.status === statusCodes.COMPLETED){ if (task.status === statusCodes.COMPLETED){
if (task.available_assets.indexOf("orthophoto.tif") !== -1){ if (task.available_assets.indexOf("orthophoto.tif") !== -1){
addActionButton(" View Map", "btn-primary", "fa fa-globe", () => { addActionButton(" View Map", "btn-primary", "fa fa-globe", () => {
@ -394,7 +399,7 @@ class TaskListItem extends React.Component {
}else{ }else{
showOrthophotoMissingWarning = true; showOrthophotoMissingWarning = true;
} }
addActionButton(" View 3D Model", "btn-primary", "fa fa-cube", () => { addActionButton(" View 3D Model", "btn-primary", "fa fa-cube", () => {
location.href = `/3d/project/${task.project}/task/${task.id}/`; location.href = `/3d/project/${task.project}/task/${task.id}/`;
}); });
@ -417,10 +422,10 @@ class TaskListItem extends React.Component {
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){
// By default restart reruns every pipeline // By default restart reruns every pipeline
// step from the beginning // step from the beginning
const rerunFrom = task.can_rerun_from.length > 1 ? const rerunFrom = task.can_rerun_from.length > 1 ?
task.can_rerun_from[1] : task.can_rerun_from[1] :
null; null;
addActionButton("Restart", "btn-primary", "glyphicon glyphicon-repeat", this.genRestartAction(rerunFrom), { addActionButton("Restart", "btn-primary", "glyphicon glyphicon-repeat", this.genRestartAction(rerunFrom), {
@ -436,7 +441,7 @@ class TaskListItem extends React.Component {
const disabled = this.state.actionButtonsDisabled || !!task.pending_action; const disabled = this.state.actionButtonsDisabled || !!task.pending_action;
actionButtons = (<div className="action-buttons"> actionButtons = (<div className="action-buttons">
{task.status === statusCodes.COMPLETED ? {task.status === statusCodes.COMPLETED ?
<AssetDownloadButtons task={this.state.task} disabled={disabled} /> <AssetDownloadButtons task={this.state.task} disabled={disabled} />
: ""} : ""}
{actionButtons.map(button => { {actionButtons.map(button => {
@ -444,18 +449,18 @@ class TaskListItem extends React.Component {
const className = button.options.className || ""; const className = button.options.className || "";
return ( return (
<div key={button.label} className={"inline-block " + <div key={button.label} className={"inline-block " +
(subItems.length > 0 ? "btn-group" : "") + " " + (subItems.length > 0 ? "btn-group" : "") + " " +
className}> className}>
<button type="button" className={"btn btn-sm " + button.className} onClick={button.onClick} disabled={disabled}> <button type="button" className={"btn btn-sm " + button.className} onClick={button.onClick} disabled={disabled}>
<i className={button.icon}></i> <i className={button.icon}></i>
{button.label} {button.label}
</button> </button>
{subItems.length > 0 && {subItems.length > 0 &&
[<button key="dropdown-button" [<button key="dropdown-button"
disabled={disabled} disabled={disabled}
type="button" type="button"
className={"btn btn-sm dropdown-toggle " + button.className} className={"btn btn-sm dropdown-toggle " + button.className}
data-toggle="dropdown"><span className="caret"></span></button>, data-toggle="dropdown"><span className="caret"></span></button>,
<ul key="dropdown-menu" className="dropdown-menu"> <ul key="dropdown-menu" className="dropdown-menu">
{subItems.map(subItem => <li key={subItem.label}> {subItems.map(subItem => <li key={subItem.label}>
@ -484,24 +489,30 @@ class TaskListItem extends React.Component {
: ""} : ""}
{/* TODO: List of images? */} {/* TODO: List of images? */}
{showOrthophotoMissingWarning ? {showOrthophotoMissingWarning ?
<div className="task-warning"><i className="fa fa-warning"></i> <span>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.</span></div> : ""} <div className="task-warning"><i className="fa fa-warning"></i> <span>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.</span></div> : ""}
</div> </div>
<div className="col-md-8"> <div className="col-md-8">
<Console <Console
source={this.consoleOutputUrl} source={this.consoleOutputUrl}
refreshInterval={this.shouldRefresh() ? 3000 : undefined} refreshInterval={this.shouldRefresh() ? 3000 : undefined}
autoscroll={true} autoscroll={true}
height={200} height={200}
ref={domNode => this.console = domNode} ref={domNode => this.console = domNode}
onAddLines={this.checkForCommonErrors} onAddLines={this.checkForCommonErrors}
/> />
<a href="javascript:void(0);" onClick={this.downloadTaskOutput} class="btn btn-sm btn-primary pull-right" title="Download task output">
{showMemoryErrorWarning ? <i class="fa fa-download"></i>
</a>
<a href="javascript:void(0);" onClick={this.copyTaskOutput} class="btn btn-sm btn-primary pull-right" title="Copy task output">
<i class="fa fa-clipboard"></i>
</a>
<div class="clearfix"></div>
{showMemoryErrorWarning ?
<div className="task-warning"><i className="fa fa-support"></i> <span>It looks like your processing node ran out of memory. If you are using docker, make sure that your docker environment has <a href={memoryErrorLink} target="_blank">enough RAM allocated</a>. 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 <a href="javascript:void(0);" onClick={this.startEditing}>options</a>.</span></div> : ""} <div className="task-warning"><i className="fa fa-support"></i> <span>It looks like your processing node ran out of memory. If you are using docker, make sure that your docker environment has <a href={memoryErrorLink} target="_blank">enough RAM allocated</a>. 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 <a href="javascript:void(0);" onClick={this.startEditing}>options</a>.</span></div> : ""}
{showTaskWarning ? {showTaskWarning ?
<div className="task-warning"><i className="fa fa-support"></i> <span dangerouslySetInnerHTML={{__html: this.state.friendlyTaskError}} /></div> : ""} <div className="task-warning"><i className="fa fa-support"></i> <span dangerouslySetInnerHTML={{__html: this.state.friendlyTaskError}} /></div> : ""}
{showExitedWithCodeOneHints ? {showExitedWithCodeOneHints ?
@ -510,7 +521,7 @@ class TaskListItem extends React.Component {
<ul> <ul>
<li>Increase the <b>min-num-features</b> option, especially if your images have lots of vegetation</li> <li>Increase the <b>min-num-features</b> option, especially if your images have lots of vegetation</li>
</ul> </ul>
Still not working? Upload your images somewhere like <a href="https://www.dropbox.com/" target="_blank">Dropbox</a> or <a href="https://drive.google.com/drive/u/0/" target="_blank">Google Drive</a> and <a href="http://community.opendronemap.org/c/webodm" target="_blank">open a topic</a> on our community forum, making Still not working? Upload your images somewhere like <a href="https://www.dropbox.com/" target="_blank">Dropbox</a> or <a href="https://drive.google.com/drive/u/0/" target="_blank">Google Drive</a> and <a href="http://community.opendronemap.org/c/webodm" target="_blank">open a topic</a> on our community forum, making
sure to include a <a href="javascript:void(0);" onClick={this.downloadTaskOutput}>copy of your task's output</a> (the one you see above <i className="fa fa-arrow-up"></i>, click to <a href="javascript:void(0);" onClick={this.downloadTaskOutput}>download</a> it). Our awesome contributors will try to help you! <i className="fa fa-smile-o"></i> sure to include a <a href="javascript:void(0);" onClick={this.downloadTaskOutput}>copy of your task's output</a> (the one you see above <i className="fa fa-arrow-up"></i>, click to <a href="javascript:void(0);" onClick={this.downloadTaskOutput}>download</a> it). Our awesome contributors will try to help you! <i className="fa fa-smile-o"></i>
</div> </div>
</div> </div>
@ -565,12 +576,12 @@ class TaskListItem extends React.Component {
</div> </div>
<div className="col-sm-1 details"> <div className="col-sm-1 details">
<i className="fa fa-image"></i> {task.images_count} <i className="fa fa-image"></i> {task.images_count}
</div> </div>
<div className="col-sm-2 details"> <div className="col-sm-2 details">
<i className="fa fa-clock-o"></i> {this.hoursMinutesSecs(this.state.time)} <i className="fa fa-clock-o"></i> {this.hoursMinutesSecs(this.state.time)}
</div> </div>
<div className="col-sm-3"> <div className="col-sm-3">
{showEditLink ? {showEditLink ?
<a href="javascript:void(0);" onClick={this.startEditing}>{statusLabel}</a> <a href="javascript:void(0);" onClick={this.startEditing}>{statusLabel}</a>
: statusLabel} : statusLabel}
</div> </div>