Files
galaxy/client/src/components/Workflow/Run/WorkflowRunForm.vue
T
mvdbeek 125e253eba Paginate workflow run-form data dropdowns via /api/histories/{id}/contents
Switch the simplified and legacy workflow run forms from a JSON-encoded
``options_pagination`` round-trip on ``/api/workflows/{id}/download?style=run``
to a direct ``GET /api/histories/{history_id}/contents`` call per scroll /
search. Workflow ``data`` / ``data_collection`` inputs cannot use the
Python-only filter knobs that tool ``data`` parameters can (``options_filter_attribute``,
``data_destination``), so SQL-level filtering on extension + state + name/hid
is sufficient — no need for the workflow API to mediate every page fetch.

The expanded "acceptable extensions" set (formats ∪ implicit-conversion
sources, already computed by ``BaseDataToolParameter._acceptable_extensions``)
is shipped once in the initial run spec; the client passes it to the contents
API as ``q=extension-in&qv=ext1,ext2,...``.

Add SQL extension filtering to ``History.paginated_active_dataset_collections``
and to ``HistoryContentsFilters`` for HDCAs: a correlated subquery walks up to
three levels of nesting (``list``, ``paired``, ``list:paired``, ``list:list``,
``list:list:paired``) and rejects collections that contain any leaf HDA whose
extension is outside the acceptable set — matching the canonical
``SummaryDatasetCollectionMatcher`` semantics without a schema migration.
2026-05-21 08:31:21 +02:00

350 lines
14 KiB
Vue

<template>
<div v-if="currentUser && currentHistoryId" class="workflow-expanded-form">
<BAlert v-if="!canRunOnHistory" variant="warning" show>
<span v-localize>
The workflow cannot run because the current history is immutable. Please select a different history or
send the results to a new one.
</span>
</BAlert>
<div class="h4 clearfix mb-3">
<b>Workflow: {{ model.name }}</b> <i>(version: {{ model.runData.version + 1 }})</i>
<div class="float-right d-flex flex-gapx-1">
<b-button
v-if="!disableSimpleForm"
v-g-tooltip.hover
variant="link"
class="text-decoration-none"
title="Use simplified run form instead"
@click="$emit('showSimple')">
<span class="fas fa-arrow-left" /> Simple Form
</b-button>
<ButtonSpinner
id="run-workflow"
title="Run Workflow"
:tooltip="runButtonTooltip"
:disabled="!canRunOnHistory || hasCredentialErrors"
:wait="showExecuting"
@onClick="onExecute" />
</div>
</div>
<BAlert v-if="disableSimpleFormReason" show variant="warning">
This is the legacy workflow run form.
<span v-if="disableSimpleFormReason === 'hasReplacementParameters'">
This workflow contains parameters in tool steps that require advanced handling. The simplified form does
not support these parameters.
</span>
<span v-else-if="disableSimpleFormReason === 'hasDisconnectedInputs'">
One or more tools in this workflow have required inputs that are not connected to other steps. The
simplified form cannot handle disconnected runtime inputs.
</span>
<span v-else-if="disableSimpleFormReason === 'hasWorkflowResourceParameters'">
This workflow is configured with resource request parameters. The simplified form does not support
workflows with resource options.
</span>
</BAlert>
<WorkflowCredentials v-if="credentialTools.length" :tool-identifiers="credentialTools" />
<FormCard v-if="wpInputsAvailable" title="Workflow Parameters">
<template v-slot:body>
<FormDisplay :inputs="wpInputs" @onChange="onWpInputs" />
</template>
</FormCard>
<FormCard title="History Options">
<template v-slot:body>
<FormDisplay :inputs="historyInputs" @onChange="onHistoryInputs" />
</template>
</FormCard>
<FormCard title="Job re-use Options">
<template v-slot:body>
<FormElement
v-model="useCachedJobs"
title="Attempt to re-use jobs with identical parameters?"
help="This may skip executing jobs that you have already run."
type="boolean" />
</template>
</FormCard>
<OnCompleteActions v-model="onCompleteActions" />
<FormCard v-if="resourceInputsAvailable" title="Workflow Resource Options">
<template v-slot:body>
<FormDisplay :inputs="resourceInputs" @onChange="onResourceInputs" />
</template>
</FormCard>
<div v-for="step in model.steps" :key="step.index">
<WorkflowRunDefaultStep
v-if="step.step_type == 'tool' || step.step_type == 'subworkflow'"
:model="step"
:replace-params="getReplaceParams(step.inputs)"
:validation-scroll-to="getValidationScrollTo(step.index)"
:history-id="currentHistoryId"
@onChange="onToolStepInputs"
@onValidation="onValidation" />
<WorkflowRunInputStep
v-else
:model="step"
:validation-scroll-to="getValidationScrollTo(step.index)"
:history-id="currentHistoryId"
@onChange="onDefaultStepInputs"
@onValidation="onValidation" />
</div>
</div>
</template>
<script>
import { BAlert } from "bootstrap-vue";
import { mapState } from "pinia";
import { useUserMultiToolCredentials } from "@/composables/userMultiToolCredentials";
import { useHistoryStore } from "@/stores/historyStore";
import { useToolsServiceCredentialsDefinitionsStore } from "@/stores/toolsServiceCredentialsDefinitionsStore";
import { useUserStore } from "@/stores/userStore";
import { getReplacements } from "./model";
import { invokeWorkflow } from "./services";
import WorkflowRunDefaultStep from "./WorkflowRunDefaultStep.vue";
import WorkflowRunInputStep from "./WorkflowRunInputStep.vue";
import ButtonSpinner from "@/components/Common/ButtonSpinner.vue";
import FormCard from "@/components/Form/FormCard.vue";
import FormDisplay from "@/components/Form/FormDisplay.vue";
import FormElement from "@/components/Form/FormElement.vue";
import OnCompleteActions from "@/components/Workflow/Run/OnCompleteActions.vue";
import WorkflowCredentials from "@/components/Workflow/Run/WorkflowCredentials.vue";
export default {
components: {
BAlert,
ButtonSpinner,
FormDisplay,
FormCard,
FormElement,
OnCompleteActions,
WorkflowCredentials,
WorkflowRunDefaultStep,
WorkflowRunInputStep,
},
props: {
model: {
type: Object,
required: true,
},
canMutateCurrentHistory: {
type: Boolean,
required: true,
},
disableSimpleForm: {
type: Boolean,
default: false,
},
disableSimpleFormReason: {
type: String,
default: undefined,
},
},
data() {
return {
showExecuting: false,
stepData: {},
stepValidations: {},
stepScrollTo: {},
wpData: {},
inputs: {},
historyData: {},
useCachedJobs: false,
onCompleteActions: [],
historyInputs: [
{
type: "conditional",
name: "new_history",
test_param: {
name: "check",
label: "Send results to a new history",
type: "boolean",
value: "false",
help: "",
},
cases: [
{
value: "true",
inputs: [
{
name: "name",
label: "History name",
type: "text",
value: this.model.name,
},
],
},
{
value: "false",
inputs: [],
},
],
},
],
};
},
computed: {
...mapState(useUserStore, ["currentUser"]),
...mapState(useHistoryStore, ["currentHistoryId"]),
credentialTools() {
return this.model.steps
.filter((step) => step.step_type === "tool" && step.credentials?.length)
.map((step) => {
const { setToolServiceCredentialsDefinitionFor } = useToolsServiceCredentialsDefinitionsStore();
setToolServiceCredentialsDefinitionFor(step.id, step.version, step.credentials);
return {
toolId: step.id,
toolVersion: step.version,
};
});
},
resourceInputsAvailable() {
return this.resourceInputs.length > 0;
},
resourceInputs() {
return this.toArray(this.model.workflowResourceParameters);
},
wpInputsAvailable() {
return this.wpInputs.length > 0;
},
wpInputs() {
return this.toArray(this.model.wpInputs);
},
shouldRunOnNewHistory() {
return Boolean(this.historyData["new_history|name"]);
},
canRunOnHistory() {
return this.shouldRunOnNewHistory || this.canMutateCurrentHistory;
},
hasCredentialErrors() {
if (this.credentialTools.length) {
const { hasUserProvidedAllRequiredToolsServiceCredentials } = useUserMultiToolCredentials(
this.credentialTools,
);
return !hasUserProvidedAllRequiredToolsServiceCredentials.value;
}
return false;
},
runButtonTooltip() {
if (this.hasCredentialErrors) {
return "Please provide all required credentials before running the workflow.";
}
return "Run workflow";
},
},
methods: {
getReplaceParams(inputs) {
return getReplacements(inputs, this.stepData, this.wpData);
},
getValidationScrollTo(stepId) {
if (this.stepScrollTo.stepId == stepId) {
return this.stepScrollTo.stepError;
}
return [];
},
onDefaultStepInputs(stepId, data) {
this.inputs[stepId] = data.input;
},
onToolStepInputs(stepId, data) {
this.stepData[stepId] = data;
},
onHistoryInputs(data) {
this.historyData = data;
},
onResourceInputs(data) {
this.resourceData = data;
},
onWpInputs(data) {
this.wpData = data;
},
onValidation(stepId, validation) {
this.stepValidations[stepId] = validation;
},
onExecute() {
for (const [stepId, stepValidation] of Object.entries(this.stepValidations)) {
if (stepValidation) {
this.stepScrollTo = {
stepId: stepId,
stepError: stepValidation.slice(),
};
return;
}
}
const parameters = {};
Object.entries(this.stepData).forEach(([stepId, stepData]) => {
const stepDataFiltered = {};
Object.entries(stepData).forEach(([inputName, inputValue]) => {
if (!this.model.isConnected(stepId, inputName)) {
stepDataFiltered[inputName] = inputValue;
}
});
parameters[stepId] = stepDataFiltered;
});
const jobDef = {
new_history_name: this.historyData["new_history|name"] ? this.historyData["new_history|name"] : null,
history_id: !this.historyData["new_history|name"] ? this.model.historyId : null,
resource_params: this.resourceData,
replacement_params: this.wpData,
use_cached_job: this.useCachedJobs,
inputs: this.inputs,
parameters: parameters,
// Tool form will submit flat maps for each parameter
// (e.g. "repeat_0|cond|param": "foo" instead of nested
// data structures).
parameters_normalized: true,
// Tool form always wants a list of invocations back
// so that inputs can be batched.
batch: true,
// the user is already warned if tool versions are wrong,
// they can still choose to invoke the workflow anyway.
require_exact_tool_versions: false,
version: this.model.runData.version,
// Completion actions to run when workflow finishes
on_complete: this.onCompleteActions.length > 0 ? this.onCompleteActions : null,
};
console.debug("WorkflowRunForm::onExecute()", "Ready for submission.", jobDef);
this.showExecuting = true;
invokeWorkflow(this.model.workflowId, jobDef)
.then((invocations) => {
console.debug("WorkflowRunForm::onExecute()", "Submission successful.", invocations);
this.showExecuting = false;
this.$emit("submissionSuccess", invocations);
})
.catch((e) => {
console.debug("WorkflowRunForm::onExecute()", "Submission failed.", e);
this.showExecuting = false;
const errorData = e && e.response && e.response.data && e.response.data.err_data;
if (errorData) {
try {
const errorEntries = Object.entries(errorData);
this.stepScrollTo = {
stepId: errorEntries[0][0],
stepError: Object.entries(errorEntries[0][1])[0],
};
} catch (errorFormatting) {
console.debug(
errorFormatting,
"WorkflowRunForm::onExecute()",
"Invalid server error response format.",
errorData,
);
this.$emit("submissionError", e);
}
} else {
this.$emit("submissionError", e);
}
});
},
toArray(obj) {
return obj ? Object.keys(obj).map((k) => obj[k]) : [];
},
},
};
</script>