Skip to content

Commit

Permalink
Merge branch 'release_24.1' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdbeek committed Aug 26, 2024
2 parents e9c6a07 + 5d5f537 commit 160725a
Show file tree
Hide file tree
Showing 23 changed files with 342 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
faUserLock,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import axios from "axios";
import {
BButton,
BButtonGroup,
Expand All @@ -35,9 +36,12 @@ import { computed, ref } from "vue";
import { canMutateHistory, type HistorySummary } from "@/api";
import { iframeRedirect } from "@/components/plugins/legacyNavigation";
import { useToast } from "@/composables/toast";
import { getAppRoot } from "@/onload/loadConfig";
import { useHistoryStore } from "@/stores/historyStore";
import { useUserStore } from "@/stores/userStore";
import localize from "@/utils/localization";
import { rethrowSimple } from "@/utils/simple-error";
import CopyModal from "@/components/History/Modals/CopyModal.vue";
import SelectorModal from "@/components/History/Modals/SelectorModal.vue";
Expand Down Expand Up @@ -81,6 +85,8 @@ const showCopyModal = ref(false);
const purgeHistory = ref(false);
const toast = useToast();
const userStore = useUserStore();
const historyStore = useHistoryStore();
Expand Down Expand Up @@ -122,6 +128,16 @@ function userTitle(title: string) {
return localize(title);
}
}
async function resumePausedJobs() {
const url = `${getAppRoot()}history/resume_paused_jobs?current=True`;
try {
const response = await axios.get(url);
toast.success(response.data.message);
} catch (e) {
rethrowSimple(e);
}
}
</script>

<template>
Expand Down Expand Up @@ -205,7 +221,7 @@ function userTitle(title: string) {
<BDropdownItem
:disabled="!canEditHistory"
:title="localize('Resume all Paused Jobs in this History')"
@click="iframeRedirect('/history/resume_paused_jobs?current=True')">
@click="resumePausedJobs()">
<FontAwesomeIcon fixed-width :icon="faPlay" class="mr-1" />
<span v-localize>Resume Paused Jobs</span>
</BDropdownItem>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Workflow/Editor/NodeOutput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ const outputDetails = computed(() => {
const outputType =
collectionType && collectionType.isCollection && collectionType.collectionType
? `output is ${collectionTypeToDescription(collectionType)}`
: `output is dataset`;
: `output is ${terminal.value.type || "dataset"}`;
if (isMultiple.value) {
if (!collectionType) {
collectionType = NULL_COLLECTION_TYPE_DESCRIPTION;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ const invocationJobsSummaryById = {
// Mock the invocation store to return the expected invocation data given the invocation ID
jest.mock("@/stores/invocationStore", () => {
const originalModule = jest.requireActual("@/stores/invocationStore");
const mockFetchInvocationForId = jest.fn();
const mockFetchInvocationForId = jest.fn().mockImplementation((fetchParams) => {
if (fetchParams.id === "error-invocation") {
throw new Error("User does not own specified item.");
}
});
const mockFetchInvocationJobsSummaryForId = jest.fn();
return {
...originalModule,
Expand Down Expand Up @@ -106,37 +110,57 @@ describe("WorkflowInvocationState check invocation and job terminal states", ()
const wrapper = await mountWorkflowInvocationState(invocationData.id);
expect(isInvocationAndJobTerminal(wrapper)).toBe(true);

// Neither the invocation nor the jobs summary should be fetched for terminal invocations
assertInvocationFetched(0);
// Invocation is fetched once and the jobs summary isn't fetched at all for terminal invocations
assertInvocationFetched(1);
assertJobsSummaryFetched(0);
});

it("determines that invocation and job states are not terminal with no fetched invocation", async () => {
const wrapper = await mountWorkflowInvocationState("not-fetched-invocation");
expect(isInvocationAndJobTerminal(wrapper)).toBe(false);

// Both, the invocation and jobs summary should be fetched once if the invocation is not in the store
// Invocation is fetched once and the jobs summary is then never fetched if the invocation is not in the store
assertInvocationFetched(1);
assertJobsSummaryFetched(1);
assertJobsSummaryFetched(0);

// expect there to be an alert for the missing invocation
const alert = wrapper.find("balert-stub");
expect(alert.attributes("variant")).toBe("info");
const span = alert.find("span");
expect(span.text()).toBe("Invocation not found.");
});

it("determines that invocation is not terminal with non-terminal state", async () => {
const wrapper = await mountWorkflowInvocationState("non-terminal-id");
expect(isInvocationAndJobTerminal(wrapper)).toBe(false);

// Only the invocation should be fetched for non-terminal invocations
assertInvocationFetched(1);
// Only the invocation is fetched for non-terminal invocations; once for the initial fetch and then for the polling
assertInvocationFetched(2);
assertJobsSummaryFetched(0);
});

it("determines that job states are not terminal with non-terminal jobs but scheduled invocation", async () => {
const wrapper = await mountWorkflowInvocationState("non-terminal-jobs");
expect(isInvocationAndJobTerminal(wrapper)).toBe(false);

// Only the jobs summary should be fetched, not the invocation since it is in scheduled/terminal state
assertInvocationFetched(0);
// Only the jobs summary should be polled, the invocation is initially fetched only since it is in scheduled/terminal state
assertInvocationFetched(1);
assertJobsSummaryFetched(1);
});

it("determines that errored invocation fetches are handled correctly", async () => {
const wrapper = await mountWorkflowInvocationState("error-invocation");
expect(isInvocationAndJobTerminal(wrapper)).toBe(false);

// Invocation is fetched once and the jobs summary isn't fetched at all for errored invocations
assertInvocationFetched(1);
assertJobsSummaryFetched(0);

// expect there to be an alert for the handled error
const alert = wrapper.find("balert-stub");
expect(alert.attributes("variant")).toBe("danger");
expect(alert.text()).toBe("User does not own specified item.");
});
});

describe("WorkflowInvocationState check 'Report' tab disabled state", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useAnimationFrameResizeObserver } from "@/composables/sensors/animation
import { useInvocationStore } from "@/stores/invocationStore";
import { useWorkflowStore } from "@/stores/workflowStore";
import localize from "@/utils/localization";
import { errorMessageAsString } from "@/utils/simple-error";
import { cancelWorkflowScheduling } from "./services";
import { isTerminal, jobCount, runningCount } from "./util";
Expand Down Expand Up @@ -49,6 +50,8 @@ const invocationStore = useInvocationStore();
const stepStatesInterval = ref<any>(undefined);
const jobStatesInterval = ref<any>(undefined);
const initialLoading = ref(true);
const errorMessage = ref<string | null>(null);
// after the report tab is first activated, no longer lazy-render it from then on
const reportActive = ref(false);
Expand All @@ -69,8 +72,10 @@ useAnimationFrameResizeObserver(scrollableDiv, ({ clientSize, scrollSize }) => {
isScrollable.value = scrollSize.height >= clientSize.height + 1;
});
const invocation = computed(
() => invocationStore.getInvocationById(props.invocationId) as WorkflowInvocationElementView
const invocation = computed(() =>
!initialLoading.value && !errorMessage.value
? (invocationStore.getInvocationById(props.invocationId) as WorkflowInvocationElementView)
: null
);
const invocationState = computed(() => invocation.value?.state || "new");
const invocationAndJobTerminal = computed(() => invocationSchedulingTerminal.value && jobStatesTerminal.value);
Expand Down Expand Up @@ -105,9 +110,19 @@ const workflowStore = useWorkflowStore();
const isDeletedWorkflow = computed(() => getWorkflow()?.deleted === true);
const workflowVersion = computed(() => getWorkflow()?.version);
onMounted(() => {
pollStepStatesUntilTerminal();
pollJobStatesUntilTerminal();
onMounted(async () => {
try {
await invocationStore.fetchInvocationForId({ id: props.invocationId });
initialLoading.value = false;
if (invocation.value) {
await pollStepStatesUntilTerminal();
await pollJobStatesUntilTerminal();
}
} catch (e) {
errorMessage.value = errorMessageAsString(e);
} finally {
initialLoading.value = false;
}
});
onUnmounted(() => {
Expand All @@ -116,7 +131,7 @@ onUnmounted(() => {
});
async function pollStepStatesUntilTerminal() {
if (!invocation.value || !invocationSchedulingTerminal.value) {
if (!invocationSchedulingTerminal.value) {
await invocationStore.fetchInvocationForId({ id: props.invocationId });
stepStatesInterval.value = setTimeout(pollStepStatesUntilTerminal, 3000);
}
Expand Down Expand Up @@ -152,7 +167,7 @@ function getWorkflowId() {
}
function getWorkflowName() {
return workflowStore.getStoredWorkflowNameByInstanceId(invocation.value?.workflow_id);
return workflowStore.getStoredWorkflowNameByInstanceId(invocation.value?.workflow_id || "");
}
</script>

Expand Down Expand Up @@ -261,7 +276,13 @@ function getWorkflowName() {
</BTab>
</BTabs>
</div>
<BAlert v-else variant="info" show>
<BAlert v-else-if="initialLoading" variant="info" show>
<LoadingSpan message="Loading invocation" />
</BAlert>
<BAlert v-else-if="errorMessage" variant="danger" show>
{{ errorMessage }}
</BAlert>
<BAlert v-else variant="info" show>
<span v-localize>Invocation not found.</span>
</BAlert>
</template>
10 changes: 7 additions & 3 deletions lib/galaxy/managers/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)

import sqlalchemy
import yaml
from gxformat2 import (
from_galaxy_native,
ImporterGalaxyInterface,
Expand Down Expand Up @@ -635,9 +636,12 @@ def normalize_workflow_format(self, trans, as_dict):
galaxy_interface = Format2ConverterGalaxyInterface()
import_options = ImportOptions()
import_options.deduplicate_subworkflows = True
as_dict = python_to_workflow(
as_dict, galaxy_interface, workflow_directory=workflow_directory, import_options=import_options
)
try:
as_dict = python_to_workflow(
as_dict, galaxy_interface, workflow_directory=workflow_directory, import_options=import_options
)
except yaml.scanner.ScannerError as e:
raise exceptions.MalformedContents(str(e))

return RawWorkflowDescription(as_dict, workflow_path)

Expand Down
60 changes: 57 additions & 3 deletions lib/galaxy/tools/cross_product_flat.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,66 @@
Synopsis
========
@CROSS_PRODUCT_INTRO@
====================
How to use this tool
====================
===========
Description
===========
@GALAXY_DOT_PRODUCT_SEMANTICS@
Running input lists through this tool produces new dataset lists (described in detail below) that when using
the same natural element-wise matching "map over" semantics described above produce every combination of the
elements of the two lists compared against each other. Running a tool with these two outputs instead of the inital
two input produces a list of the comparison of each combination of pairs from the respective inputs.
.. image:: ${static_path}/images/tools/collection_ops/flat_crossproduct_output.png
:alt: The Flat Cartesian Product of Two Collections
:width: 500
The result of running a subsequent tool with the outputs produced by this tool will be a much larger list
whose element identifiers are the concatenation of the combinations of the elements identifiers from the
two input lists.
.. image:: ${static_path}/images/tools/collection_ops/flat_crossproduct_separator.png
:alt: Flat Cross Product Identifier Separator
:width: 500
============================================
What this tool does (technical details)
============================================
This tool consumes two lists - we will call them ``input_a`` and ``input_b``. If ``input_a``
has length ``n`` and dataset elements identified as ``a1``, ``a2``, ... ``an`` and ``input_b``
has length ``m`` and dataset elements identified as ``b1``, ``b2``, ... ``bm``, then this tool
produces a pair of larger lists - each of size ``n*m``.
Both output lists will be the same length and contain the same set of element identifiers in the
same order. If the kth input can be described as ``(i-1)*n + (j-1)`` where ``1 <= i <= m`` and ``1 <= j <= n``
then the element identifier for this kth element is the concatenation of the element identifier for
the ith item of ``input_a`` and the jth item of ``input_b``.
In the first output list, this kth element will be the ith element of ``input_a``. In the second
output list, the kth element will be the jth element of ``input_b``.
.. image:: ${static_path}/images/tools/collection_ops/flat_cross_product_outputs.png
:alt: Flat Cross Product Outputs
:width: 500
These list structures might appear to be a little odd, but they have the very useful property
that if you match up corresponding elements of the lists the result is each combination of
elements in ``input_a`` and ``input_b`` are matched up once.
.. image:: ${static_path}/images/tools/collection_ops/flat_cross_product_matched.png
:alt: Flat Cross Product Matching Datasets
:width: 500
Running a downstream comparison tool that compares two datasets with these two lists produces a
new list with every combination of comparisons.
.. image:: ${static_path}/images/tools/collection_ops/flat_cross_product_downstream.png
:alt: Flat Cross Product All-vs-All Result
:width: 500
----
Expand Down
63 changes: 60 additions & 3 deletions lib/galaxy/tools/cross_product_nested.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,69 @@
Synopsis
========
@CROSS_PRODUCT_INTRO@
====================
How to use this tool
====================
===========
Description
===========
@GALAXY_DOT_PRODUCT_SEMANTICS@
Running input lists through this tool produces new list structures (described in detail below) that when using
the same natural element-wise matching "map over" semantics described above produce every combination of the
elements of the two lists compared against each other. Running a tool with these two outputs instead of the inital
two input produces a nested list structure where the jth element of the inner list of the ith element of the outer
list is a comparison of the ith element of the first list to the jth element of the second list.
Put more simply, the result is a nested list where the identifiers of an element describe which inputs were
matched to produce the comparison output found at that element.
.. image:: ${static_path}/images/tools/collection_ops/nested_crossproduct_output.png
:alt: The Cartesian Product of Two Collections
:width: 500
============================================
What this tool does (technical details)
============================================
This tool consumes two flat lists. We will call the input collections ``input_a`` and ``input_b``. If ``input_a``
has length ``n`` and dataset elements identified as ``a1``, ``a2``, ... ``an`` and ``input_b``
has length ``m`` and dataset elements identified as ``b1``, ``b2``, ... ``bm``, then this tool
produces a pair of output nested lists (specifically of the ``list:list`` collection type) where
the outer list is of length ``n`` and each inner list has a length of ``m`` (a ``n X m`` nested list). The jth element
inside the outer list's ith element is a pseudo copy of the ith dataset of ``inputa``. One
way to think about the output nested lists is as matrices. Here is a diagram of the first output
showing the element identifiers of the outer and inner lists along with the what dataset is being
"copied" into this new collection.
.. image:: ${static_path}/images/tools/collection_ops/nested_cross_product_out_1.png
:alt: Nested Cross Product First Output
:width: 500
The second output is a nested list of pseudo copies of the elements of ``input_b`` instead of
``input_a``. In particular the outer list is again of length ``n`` and each inner list is again
of lenth ``m`` but this time the jth element inside the outer list's ith element is a pseudo copy
of the jth dataset of ``inputb``. Here is the matrix of these outputs.
.. image:: ${static_path}/images/tools/collection_ops/nested_cross_product_out_2.png
:alt: Nested Cross Product Second Output
:width: 500
These nested list structures might appear to be a little odd, but they have the very useful property
that if you match up corresponding elements of the nested lists the result is each combination of
elements in ``input_a`` and ``input_b`` are matched up once. The following diagram describes these matching
datasets.
.. image:: ${static_path}/images/tools/collection_ops/nested_cross_product_matching.png
:alt: Matching Inputs
:width: 500
Running a tool that compares two datasets with these two nested lists produces a new nested list
as described above. The following diagram shows the structure of this output and how the element
identifiers are preserved and indicate what comparison was performed.
.. image:: ${static_path}/images/tools/collection_ops/nested_cross_product_output.png
:alt: Matching Inputs
:width: 500
----
Expand Down
Loading

0 comments on commit 160725a

Please sign in to comment.