Merge branch 'release_24.2' into dev

This commit is contained in:
mvdbeek
2025-04-15 18:27:11 +02:00
6 changed files with 68 additions and 47 deletions
@@ -83,11 +83,15 @@ if (props.instance) {
}
function handleInvocations(incomingInvocations: any) {
invocations.value = incomingInvocations;
// make sure any new histories are added to historyStore
invocations.value.forEach((invocation: any) => {
historyStore.getHistoryById(invocation.history_id);
});
if (incomingInvocations.length === 1) {
router.push(`/workflows/invocations/${incomingInvocations[0].id}?success=true`);
} else {
invocations.value = incomingInvocations;
// make sure any new histories are added to historyStore
invocations.value.forEach((invocation: any) => {
historyStore.getHistoryById(invocation.history_id);
});
}
}
function handleSubmissionError(error: string) {
@@ -2,7 +2,6 @@
import { onMounted } from "vue";
import type { WorkflowInvocation } from "@/api/invocations";
import { useHistoryStore } from "@/stores/historyStore";
import Webhooks from "@/utils/webhooks";
import { startWatchingHistory } from "@/watch/watchHistoryProvided";
@@ -23,18 +22,12 @@ onMounted(() => {
startWatchingHistory();
});
const historyStore = useHistoryStore();
const targetHistories = props.invocations.reduce((histories, invocation) => {
if (invocation.history_id && !histories.includes(invocation.history_id)) {
histories.push(invocation.history_id);
}
return histories;
}, [] as string[]);
const wasNewHistoryTarget =
props.invocations.length > 0 &&
!!props.invocations[0]?.history_id &&
historyStore.currentHistoryId !== props.invocations[0].history_id;
</script>
<template>
@@ -51,7 +44,6 @@ const wasNewHistoryTarget =
<WorkflowInvocationState
v-else-if="props.invocations.length === 1 && props.invocations[0]"
:invocation-id="props.invocations[0].id"
:new-history-target="wasNewHistoryTarget"
is-full-page
success />
<div id="webhook-view"></div>
@@ -22,7 +22,6 @@ interface Props {
invocationUpdateTime?: string;
historyId: string;
showDetails?: boolean;
newHistoryTarget?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
@@ -71,12 +70,12 @@ const { renderMarkdown } = useMarkdown({
<FontAwesomeIcon :icon="faHdd" />History:
<SwitchToHistoryLink :history-id="props.historyId" />
<BBadge
v-if="props.newHistoryTarget && useHistoryStore().currentHistoryId !== props.historyId"
v-if="useHistoryStore().currentHistoryId !== props.historyId"
v-b-tooltip.hover.noninteractive
data-description="new history badge"
role="button"
variant="info"
title="Results generated in a new history. Click on history name to switch to that history.">
title="Results generated in a different history. Click on history name to switch to that history.">
<FontAwesomeIcon :icon="faExclamation" />
</BBadge>
</span>
@@ -33,6 +33,10 @@ const invocationById = {
...invocationData,
id: "non-terminal-jobs",
},
"non-terminal-populated-state": {
...invocationData,
id: "non-terminal-populated-state",
},
};
// Jobs summary constants
@@ -51,6 +55,10 @@ const invocationJobsSummaryById = {
running: 1,
},
},
"non-terminal-populated-state": {
...invocationDataJobsSummary,
populated_state: "new",
},
};
// Mock the invocation store to return the expected invocation data given the invocation ID
@@ -163,6 +171,15 @@ describe("WorkflowInvocationState check invocation and job terminal states", ()
assertJobsSummaryFetched(1);
});
it("determines that job states are not terminal with non-terminal populated state for summary", async () => {
const wrapper = await mountWorkflowInvocationState("non-terminal-populated-state");
expect(isInvocationAndJobTerminal(wrapper)).toBe(false);
// 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);
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BBadge, BButton, BTab, BTabs } from "bootstrap-vue";
import { computed, onUnmounted, ref, watch } from "vue";
import { type InvocationJobsSummary, type InvocationStep, type WorkflowInvocationElementView } from "@/api/invocations";
import { type InvocationStep, type WorkflowInvocationElementView } from "@/api/invocations";
import { useAnimationFrameResizeObserver } from "@/composables/sensors/animationFrameResizeObserver";
import { useInvocationStore } from "@/stores/invocationStore";
import { useWorkflowStore } from "@/stores/workflowStore";
@@ -36,7 +36,6 @@ interface Props {
isSubworkflow?: boolean;
isFullPage?: boolean;
success?: boolean;
newHistoryTarget?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
@@ -80,8 +79,8 @@ const disabledTabTooltip = computed(() => {
const state = invocationState.value;
if (state != "scheduled") {
return `This workflow is not currently scheduled. The current state is ${state}. Once the workflow is fully scheduled and jobs have complete any disabled tabs will become available.`;
} else if (runningCount.value != 0) {
return `The workflow invocation still contains ${runningCount.value} running job(s). Once these jobs have completed any disabled tabs will become available.`;
} else if (stateCounts.value && stateCounts.value.runningCount != 0) {
return `The workflow invocation still contains ${stateCounts.value.runningCount} running job(s). Once these jobs have completed any disabled tabs will become available.`;
} else {
return "Steps for this workflow are still running. Any disabled tabs will be available once complete.";
}
@@ -109,18 +108,23 @@ const invocationSchedulingTerminal = computed(() => {
);
});
const jobStatesTerminal = computed(() => {
// If the job states summary is null, we haven't fetched it yet
// If the `populated_state` for the summary is `new`, we haven't finished scheduling all jobs
if (jobStatesSummary.value === null || jobStatesSummary.value.populated_state === "new") {
return false;
}
if (invocationSchedulingTerminal.value && jobCount.value === 0) {
// no jobs for this invocation (think subworkflow or just inputs)
// no jobs for this invocation (think it has just subworkflows/inputs)
return true;
}
return !!jobStatesSummary.value && isTerminal(jobStatesSummary.value as InvocationJobsSummary);
});
const jobStatesSummary = computed(() => {
const jobsSummary = invocationStore.getInvocationJobsSummaryById(props.invocationId);
return (!jobsSummary ? null : jobsSummary) as InvocationJobsSummary;
return isTerminal(jobStatesSummary.value);
});
const jobStatesSummary = computed(() => invocationStore.getInvocationJobsSummaryById(props.invocationId));
const invocationStateSuccess = computed(() => {
return invocationState.value == "scheduled" && runningCount.value === 0 && invocationAndJobTerminal.value;
return (
invocationState.value == "scheduled" && stateCounts.value?.runningCount === 0 && invocationAndJobTerminal.value
);
});
type StepStateType = { [state: string]: number };
@@ -151,27 +155,31 @@ const stepStatesStr = computed<string>(() => {
return `${stepStates.value?.scheduled || 0} of ${stepCount.value} steps successfully scheduled.`;
});
const okCount = computed<number>(() => {
return jobStatesSummaryOkCount(jobStatesSummary.value);
});
const errorCount = computed<number>(() => {
return jobStatesSummaryErrorCount(jobStatesSummary.value);
});
const runningCount = computed<number>(() => {
return jobStatesSummaryRunningCount(jobStatesSummary.value);
const stateCounts = computed<{
okCount: number;
errorCount: number;
runningCount: number;
newCount: number;
} | null>(() => {
if (jobStatesSummary.value === null) {
return null;
}
const okCount = jobStatesSummaryOkCount(jobStatesSummary.value);
const errorCount = jobStatesSummaryErrorCount(jobStatesSummary.value);
const runningCount = jobStatesSummaryRunningCount(jobStatesSummary.value);
const newCount = jobCount.value - okCount - runningCount - errorCount;
return { okCount, errorCount, runningCount, newCount };
});
const jobCount = computed<number>(() => {
return jobStatesSummaryJobCount(jobStatesSummary.value);
});
const newCount = computed<number>(() => {
return jobCount.value - okCount.value - runningCount.value - errorCount.value;
});
const jobStatesStr = computed(() => {
if (jobStatesSummary.value === null) {
return "No jobs summary available yet.";
}
let jobStr = `${numTerminal(jobStatesSummary.value) || 0} of ${jobCount.value} jobs complete`;
if (!invocationSchedulingTerminal.value) {
jobStr += " (total number of jobs will change until all steps fully scheduled)";
@@ -274,8 +282,7 @@ async function onCancel() {
v-if="props.isFullPage"
:workflow-id="invocation.workflow_id"
:invocation-update-time="invocation.update_time"
:history-id="invocation.history_id"
:new-history-target="props.newHistoryTarget">
:history-id="invocation.history_id">
<template v-slot:middle-content>
<div class="progress-bars mx-1">
<ProgressBar
@@ -301,12 +308,13 @@ async function onCancel() {
:loading="!invocationSchedulingTerminal"
class="steps-progress" />
<ProgressBar
v-if="stateCounts"
:note="jobStatesStr"
:total="jobCount"
:ok-count="okCount"
:running-count="runningCount"
:new-count="newCount"
:error-count="errorCount"
:ok-count="stateCounts.okCount"
:running-count="stateCounts.runningCount"
:new-count="stateCounts.newCount"
:error-count="stateCounts.errorCount"
:loading="!invocationAndJobTerminal"
class="jobs-progress" />
</div>
+1
View File
@@ -655,6 +655,7 @@ export function getRouter(Galaxy) {
props: (route) => ({
invocationId: route.params.invocationId,
isFullPage: true,
success: route.query.success,
}),
},
{