Merge pull request #9138 from jmchilton/wfrun2vue_0

Refactor Workflow Run Form Toward Vue (part 1)
This commit is contained in:
Aysam Guerler
2020-02-17 14:00:03 -05:00
committed by GitHub
5 changed files with 323 additions and 182 deletions
@@ -0,0 +1,67 @@
/** Button variant with waiting and progress tracking. */
<template>
<button class="ui-button-default" v-bind:class="buttonClass" type="button" v-on:click="$emit('click')">
<i class="icon fa" v-bind:class="iconClass"> </i>
<span class="title"> {{ currentText }} </span>
<div class="progress" v-if="percentage != -1">
<div class="progress-bar" v-bind:style="{ width: progressWidth }"></div>
</div>
</button>
</template>
<script>
export default {
props: {
title: {
type: String,
default: ""
},
icon: {
type: String,
deafult: ""
},
percentage: {
type: Number,
default: -1
},
waiting: {
type: Boolean,
default: false
},
waitText: {
type: String,
default: "Sending..."
},
variant: {
type: String,
default: "info"
}
},
computed: {
progressWidth() {
return `${this.percentage}%`;
},
buttonClass() {
if (this.waiting) {
return "disabled";
} else {
return `btn btn-${this.variant}`;
}
},
iconClass() {
if (this.waiting) {
return "fa-spinner fa-spin mr-1";
} else {
return this.icon + " mr-1";
}
},
currentText() {
if (this.waiting) {
return this.waitText;
} else {
return this.title;
}
}
}
};
</script>
@@ -0,0 +1,115 @@
<template>
<span>
<b-alert variant="error" show v-if="error">
{{ error }}
</b-alert>
<b-alert v-if="loading" variant="info" show>
<loading-span message="Loading workflow run data" />
</b-alert>
<workflow-run-success v-else-if="invocations != null" :invocations="invocations" :workflowName="workflowName" />
<div v-else ref="run" class="ui-form-composite">
<div class="ui-form-composite-messages mb-4">
<b-alert v-if="hasUpgradeMessages" variant="warning" show>
Some tools in this workflow may have changed since it was last saved or some errors were found. The
workflow may still run, but any new options will have default values. Please review the messages
below to make a decision about whether the changes will affect your analysis.
</b-alert>
<b-alert v-if="hasStepVersionChanges" variant="warning" show>
Some tools are being executed with different versions compared to those available when this workflow
was last saved because the other versions are not or no longer available on this Galaxy instance. To
upgrade your workflow and dismiss this message simply edit the workflow and re-save it.
</b-alert>
</div>
<!-- h4 as a class here looks odd but it was in the Backbone -->
<div class="ui-form-composite-header h4">
<b>Workflow: {{ workflowName }}</b>
<wait-button
title="Run Workflow"
id="run-workflow"
variant="primary"
icon="fa-check"
:disabled="!runButtonEnabled"
:waiting="!runButtonEnabled"
:waitText="runButtonWaitText"
:percentage="runButtonPercentage"
@click="execute"
>
</wait-button>
</div>
<div class="ui-form-composite-form" ref="form"></div>
</div>
</span>
</template>
<script>
import axios from "axios";
import WaitButton from "components/WaitButton";
import LoadingSpan from "components/LoadingSpan";
import WorkflowRunSuccess from "./WorkflowRunSuccess";
import { getAppRoot } from "onload";
import ToolFormComposite from "mvc/tool/tool-form-composite";
import { errorMessageAsString } from "utils/simple-error";
export default {
components: {
LoadingSpan,
WaitButton,
WorkflowRunSuccess
},
props: {
workflowId: { type: String }
},
data() {
return {
error: false,
loading: true,
hasUpgradeMessages: false,
hasStepVersionChanges: false,
workflowName: "",
runForm: null,
runButtonEnabled: true,
runButtonWaitText: "",
runButtonPercentage: -1,
invocations: null
};
},
created() {
const url = `${getAppRoot()}api/workflows/${this.workflowId}/download?style=run`;
axios
.get(url)
.then(response => {
const runData = response.data;
this.hasUpgradeMessages = runData.has_upgrade_messages;
this.hasStepVersionChanges = runData.step_version_changes && runData.step_version_changes.length > 0;
this.workflowName = runData.name;
this.loading = false;
this.$nextTick(() => {
const el = this.$refs["form"];
const formProps = {
el,
setRunButtonStatus: this.setRunButtonStatus,
handleInvocations: this.handleInvocations
};
this.runForm = new ToolFormComposite.View(Object.assign({}, runData, formProps));
});
})
.catch(response => {
this.error = errorMessageAsString(response);
});
},
methods: {
execute() {
this.runForm.execute();
},
setRunButtonStatus(enabled, waitText, percentage) {
this.runButtonEnabled = enabled;
this.runButtonWaitText = waitText;
this.runButtonPercentage = percentage;
},
handleInvocations(invocations) {
this.invocations = invocations;
}
}
};
</script>
@@ -0,0 +1,106 @@
<template>
<div>
<div class="donemessagelarge">
<p>
Successfully invoked workflow <b>{{ workflowName }}</b
><em v-if="multipleInvocations"> - {{ timesExecuted }} times</em>.
</p>
<p v-if="multipleInvocations">
This workflow will generate results in multiple histories. You can observe progress in the
<a :href="historyTarget">history multi-view</a>.
</p>
<p v-else-if="wasNewHistoryTarget">
This workflow will generate results in a new history.
<a :href="historyTarget">Switch to that history now</a>.
</p>
<p v-else>
You can check the status of queued jobs and view the resulting data by refreshing the History pane, if
this has not already happened automatically.
</p>
</div>
<workflow-invocation-state v-for="invocation in invocations" :key="invocation.id" :invocationId="invocation.id">
</workflow-invocation-state>
<div id="webhook-view"></div>
</div>
</template>
<script>
import { WorkflowInvocationState } from "components/WorkflowInvocationState";
import Webhooks from "mvc/webhooks";
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
export default {
components: {
WorkflowInvocationState
},
props: {
workflowName: {
type: String,
required: true
},
invocations: {
type: Array,
required: true
}
},
data() {
return {
refreshHistoryTimeout: null
};
},
computed: {
timesExecuted() {
return this.invocations.length;
},
multipleInvocations() {
return this.timesExecuted > 1;
},
historyTarget() {
if (this.multipleInvocations) {
return `${getAppRoot()}history/view_multiple`;
} else {
return `${getAppRoot()}history/switch_to_history?hist_id=${this.invocations[0].history_id}`;
}
},
wasNewHistoryTarget() {
if (this.invocations.length < 1) {
return false;
}
const Galaxy = getGalaxyInstance();
return (
(this.invocations[0].history_id &&
Galaxy.currHistoryPanel &&
Galaxy.currHistoryPanel.model.id != this.invocations[0].history_id) ||
false
);
}
},
methods: {
_refreshHistory() {
const Galaxy = getGalaxyInstance();
var history = Galaxy && Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model;
if (this.refreshHistoryTimeout) {
window.clearTimeout(this.refreshHistoryTimeout);
}
if (history) {
history.refresh().success(() => {
if (history.numOfUnfinishedShownContents() === 0) {
this.refreshHistoryTimeout = window.setTimeout(() => {
this._refreshHistory();
}, history.UPDATE_DELAY);
}
});
}
}
},
mounted() {
new Webhooks.WebhookView({
type: "workflow",
toolId: null,
toolVersion: null
});
this._refreshHistory();
}
};
</script>
@@ -31,13 +31,11 @@ import WorkflowList from "components/Workflow/WorkflowList.vue";
import HistoryImport from "components/HistoryImport.vue";
import HistoryView from "components/HistoryView.vue";
import WorkflowInvocationReport from "components/WorkflowInvocationReport.vue";
import WorkflowRun from "components/Workflow/Run/WorkflowRun.vue";
import RecentInvocations from "components/User/RecentInvocations.vue";
import HistoryList from "mvc/history/history-list";
import PluginList from "components/PluginList.vue";
import ToolFormComposite from "mvc/tool/tool-form-composite";
import QueryStringParsing from "utils/query-string-parsing";
import Utils from "utils/utils";
import Ui from "mvc/ui/ui-misc";
import DatasetError from "mvc/dataset/dataset-error";
import DatasetEditAttributes from "mvc/dataset/dataset-edit-attributes";
import Citations from "components/Citations.vue";
@@ -94,9 +92,12 @@ export const getAnalysisRouter = Galaxy =>
return (Galaxy.user && Galaxy.user.id) || this.require_login.indexOf(name) == -1;
},
_display_vue_helper: function(component, propsData = {}) {
_display_vue_helper: function(component, propsData = {}, active_tab = null) {
const instance = Vue.extend(component);
const container = document.createElement("div");
if (active_tab) {
container.active_tab = active_tab;
}
this.page.display(container);
new instance({ store, propsData }).$mount(container);
},
@@ -406,21 +407,7 @@ export const getAnalysisRouter = Galaxy =>
/** load workflow by its url in run mode */
_loadWorkflow: function() {
Utils.get({
url: `${getAppRoot()}api/workflows/${Utils.getQueryString("id")}/download?style=run`,
success: response => {
this.page.display(new ToolFormComposite.View(_.extend(response, { active_tab: "workflow" })));
},
error: response => {
const error_msg = response.err_msg || "Error occurred while loading the resource.";
const options = {
message: error_msg,
status: "danger",
persistent: true,
active_tab: "workflow"
};
this.page.display(new Ui.Message(options));
}
});
const workflowId = QueryStringParsing.get("id");
this._display_vue_helper(WorkflowRun, { workflowId: workflowId }, "workflow");
}
});
@@ -7,14 +7,11 @@ import { getGalaxyInstance } from "app";
import _l from "utils/localization";
import Utils from "utils/utils";
import Deferred from "utils/deferred";
import Ui from "mvc/ui/ui-misc";
import Form from "mvc/form/form-view";
import FormData from "mvc/form/form-data";
import ToolFormBase from "mvc/tool/tool-form-base";
import Modal from "mvc/ui/ui-modal";
import Webhooks from "mvc/webhooks";
import WorkflowIcons from "mvc/workflow/workflow-icons";
import { mountWorkflowInvocationState } from "components/WorkflowInvocationState";
var View = Backbone.View.extend({
initialize: function(options) {
@@ -25,14 +22,9 @@ var View = Backbone.View.extend({
if (options && options.active_tab) {
this.active_tab = options.active_tab;
}
this.setElement(
$("<div/>")
.addClass("ui-form-composite")
.append((this.$message = $("<div/>").addClass("mb-4")))
.append((this.$header = $("<div/>")))
.append((this.$steps = $("<div/>")))
);
$("body").append(this.$el);
this.setRunButtonStatus = options.setRunButtonStatus;
this.handleInvocations = options.handleInvocations;
this.setElement(options.el);
this._configure();
this.render();
},
@@ -208,8 +200,6 @@ var View = Backbone.View.extend({
render: function() {
var self = this;
this.deferred.reset();
this._renderHeader();
this._renderMessage();
this._renderParameters();
this._renderHistory();
this._renderUseCachedJob();
@@ -219,53 +209,6 @@ var View = Backbone.View.extend({
});
},
/** Render header */
_renderHeader: function() {
var self = this;
this.execute_btn = new Ui.Button({
id: "run-workflow",
icon: "fa-check",
title: _l("Run workflow"),
cls: "btn btn-primary",
onclick: function() {
self._execute();
}
});
this.$header
.addClass("h4")
.empty()
.append(`<b>Workflow: ${this.model.get("name")}<b>`)
.append(this.execute_btn.$el);
},
/** Render message */
_renderMessage: function() {
this.$message.empty();
if (this.model.get("has_upgrade_messages")) {
this.$message.append(
new Ui.Message({
message:
"Some tools in this workflow may have changed since it was last saved or some errors were found. The workflow may still run, but any new options will have default values. Please review the messages below to make a decision about whether the changes will affect your analysis.",
status: "warning",
persistent: true,
fade: false
}).$el
);
}
var step_version_changes = this.model.get("step_version_changes");
if (step_version_changes && step_version_changes.length > 0) {
this.$message.append(
new Ui.Message({
message:
"Some tools are being executed with different versions compared to those available when this workflow was last saved because the other versions are not or no longer available on this Galaxy instance. To upgrade your workflow and dismiss this message simply edit the workflow and re-save it.",
status: "warning",
persistent: true,
fade: false
}).$el
);
}
},
/** Render workflow parameters */
_renderParameters: function() {
var self = this;
@@ -283,7 +226,7 @@ var View = Backbone.View.extend({
});
}
});
this._append(this.$steps.empty(), this.wp_form.$el);
this._append(this.$el.empty(), this.wp_form.$el);
}
},
@@ -319,7 +262,7 @@ var View = Backbone.View.extend({
}
]
});
this._append(this.$steps, this.history_form.$el);
this._append(this.$el, this.history_form.$el);
},
/** Render Workflow Options */
@@ -331,7 +274,7 @@ var View = Backbone.View.extend({
title: "<b>Workflow Resource Options</b>",
inputs: this.model.get("workflow_resource_parameters")
});
this._append(this.$steps, this.workflow_resource_parameters_form.$el);
this._append(this.$el, this.workflow_resource_parameters_form.$el);
}
},
@@ -365,7 +308,7 @@ var View = Backbone.View.extend({
}
]
});
this._append(this.$steps, this.job_options_form.$el);
this._append(this.$el, this.job_options_form.$el);
}
},
@@ -375,7 +318,7 @@ var View = Backbone.View.extend({
var self = this;
var form = null;
this.deferred.execute(promise => {
self.$steps.addClass("ui-steps");
self.$el.addClass("ui-steps");
if (step.step_type == "tool") {
step.postchange = function(process, form) {
var current_state = {
@@ -458,17 +401,14 @@ var View = Backbone.View.extend({
}
}
self.forms[step.index] = form;
self._append(self.$steps, form.$el);
self._append(self.$el, form.$el);
if (step.needs_refresh) {
self._refreshStep(step);
}
form.portlet[!self.show_progress ? "enable" : "disable"]();
if (self.show_progress) {
self.execute_btn.model.set({
wait: true,
wait_text: "Preparing...",
percentage: ((step.index + 1) * 100.0) / self.steps.length
});
const percentage = ((step.index + 1) * 100.0) / self.steps.length;
this.setRunButtonStatus(false, "Preparing...", percentage);
}
Galaxy.emit.debug("tool-form-composite::initialize()", `${step.index} : Workflow step state ready.`, step);
window.setTimeout(() => {
@@ -528,34 +468,14 @@ var View = Backbone.View.extend({
}
},
/** Refresh the history after job submission while form is shown */
_refreshHistory: function() {
const Galaxy = getGalaxyInstance();
var self = this;
var history = Galaxy && Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model;
if (this._refresh_history) {
window.clearTimeout(this._refresh_history);
}
if (history) {
history.refresh().success(() => {
if (history.numOfUnfinishedShownContents() === 0) {
self._refresh_history = window.setTimeout(() => {
self._refreshHistory();
}, history.UPDATE_DELAY);
}
});
}
},
/** Build remaining steps */
_execute: function() {
var self = this;
execute: function() {
this.show_progress = true;
this._enabled(false);
this.deferred.execute(promise => {
window.setTimeout(() => {
promise.resolve();
self._submit();
this._submit();
}, 0);
});
},
@@ -628,22 +548,13 @@ var View = Backbone.View.extend({
type: "POST",
url: `${getAppRoot()}api/workflows/${this.model.id}/invocations`,
data: job_def,
success: function(response) {
success: response => {
Galaxy.emit.debug("tool-form-composite::submit", "Submission successful.", response);
self.$el.children().hide();
self.$el.append(self._templateSuccess(response));
mountWorkflowInvocationState();
// Show Webhook if job is running
if ($.isArray(response) && response.length > 0) {
self.$el.append($("<div/>", { id: "webhook-view" }));
new Webhooks.WebhookView({
type: "workflow",
toolId: job_def.tool_id,
toolVersion: job_def.tool_version
});
this.handleInvocations(response);
} else {
this.submissionErrorModal(job_def, response);
}
self._refreshHistory();
},
error: function(response) {
Galaxy.emit.debug("tool-form-composite::submit", "Submission failed.", response);
@@ -663,15 +574,7 @@ var View = Backbone.View.extend({
}
}
if (!input_found) {
self.modal.show({
title: _l("Workflow submission failed"),
body: self._templateError(job_def, response && response.err_msg),
buttons: {
Close: function() {
self.modal.hide();
}
}
});
this.submissionErrorModal(job_def, response);
}
},
complete: function() {
@@ -688,11 +591,7 @@ var View = Backbone.View.extend({
/** Set enabled/disabled state */
_enabled: function(enabled) {
this.execute_btn.model.set({
wait: !enabled,
wait_text: "Sending...",
percentage: -1
});
this.setRunButtonStatus(enabled, "Sending...", -1);
if (this.wp_form) {
this.wp_form.portlet[enabled ? "enable" : "disable"]();
}
@@ -718,52 +617,19 @@ var View = Backbone.View.extend({
return true;
},
/** Templates */
_templateSuccess: function(response) {
const Galaxy = getGalaxyInstance();
if ($.isArray(response) && response.length > 0) {
let timesExecuted = "";
// Default destination blurb, used for a single execution, same history.
let destinationBlurb =
"You can check the status of queued jobs and view the resulting data by refreshing the History pane, if this has not already happened automatically.";
const newHistoryTarget =
(response[0].history_id &&
Galaxy.currHistoryPanel &&
Galaxy.currHistoryPanel.model.id != response[0].history_id) ||
false;
if (response.length > 1) {
// Executed more than one time, build blurb but skip history link.
timesExecuted = `<em> - ${response.length} times</em>`;
if (newHistoryTarget) {
destinationBlurb = `This workflow will generate results in multiple histories. You can observe progress in the <a href="${getAppRoot()}history/view_multiple">history multi-view</a>.`;
submissionErrorModal: function(job_def, response) {
this.modal.show({
title: _l("Workflow submission failed"),
body: this._templateError(job_def, response && response.err_msg),
buttons: {
Close: () => {
this.modal.hide();
}
} else if (newHistoryTarget) {
// Single execution, with a destination other than the
// current history. Present a link.
destinationBlurb = `This workflow will generate results in a new history. <a href="${getAppRoot()}history/switch_to_history?hist_id=${
response[0].history_id
}">Switch to that history now</a>.`;
}
let success = `
<div>
<div class="donemessagelarge">
<p>
Successfully invoked workflow <b>${Utils.sanitize(this.model.get("name"))}</b>${timesExecuted}.
</p>
<p>
${destinationBlurb}
</p>
</div>`;
for (const invocation of response) {
success += `<div class="workflow-invocation-state" workflow_invocation_id="${invocation.id}"></div>`;
}
success += "</div>";
return $(success);
} else {
return this._templateError(response, "Invalid success response. No invocations found.");
}
});
},
/** Templates */
_templateError: function(response, err_msg) {
return $("<div/>")
.addClass("errormessagelarge")