Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[24.1] Handle error when workflow is unowned in Invocation view #18730

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/src/api/invocations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type WorkflowInvocationElementView = components["schemas"]["WorkflowInvoc
export type WorkflowInvocationCollectionView = components["schemas"]["WorkflowInvocationCollectionView"];
export type InvocationJobsSummary = components["schemas"]["InvocationJobsResponse"];
export type InvocationStep = components["schemas"]["InvocationStep"];
export type InvocationMessage = components["schemas"]["InvocationMessageResponseUnion"];

export type StepJobSummary =
| components["schemas"]["InvocationStepJobsResponseStepModel"]
Expand Down
26 changes: 13 additions & 13 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8049,6 +8049,18 @@ export interface components {
[key: string]: number | undefined;
};
};
InvocationMessageResponseUnion:
| components["schemas"]["InvocationCancellationReviewFailedResponse"]
| components["schemas"]["InvocationCancellationHistoryDeletedResponse"]
| components["schemas"]["InvocationCancellationUserRequestResponse"]
| components["schemas"]["InvocationFailureDatasetFailedResponse"]
| components["schemas"]["InvocationFailureCollectionFailedResponse"]
| components["schemas"]["InvocationFailureJobFailedResponse"]
| components["schemas"]["InvocationFailureOutputNotFoundResponse"]
| components["schemas"]["InvocationFailureExpressionEvaluationFailedResponse"]
| components["schemas"]["InvocationFailureWhenNotBooleanResponse"]
| components["schemas"]["InvocationUnexpectedFailureResponse"]
| components["schemas"]["InvocationEvaluationWarningWorkflowOutputNotFoundResponse"];
/** InvocationOutput */
InvocationOutput: {
/**
Expand Down Expand Up @@ -13316,19 +13328,7 @@ export interface components {
* Messages
* @description A list of messages about why the invocation did not succeed.
*/
messages: (
| components["schemas"]["InvocationCancellationReviewFailedResponse"]
| components["schemas"]["InvocationCancellationHistoryDeletedResponse"]
| components["schemas"]["InvocationCancellationUserRequestResponse"]
| components["schemas"]["InvocationFailureDatasetFailedResponse"]
| components["schemas"]["InvocationFailureCollectionFailedResponse"]
| components["schemas"]["InvocationFailureJobFailedResponse"]
| components["schemas"]["InvocationFailureOutputNotFoundResponse"]
| components["schemas"]["InvocationFailureExpressionEvaluationFailedResponse"]
| components["schemas"]["InvocationFailureWhenNotBooleanResponse"]
| components["schemas"]["InvocationUnexpectedFailureResponse"]
| components["schemas"]["InvocationEvaluationWarningWorkflowOutputNotFoundResponse"]
)[];
messages: components["schemas"]["InvocationMessageResponseUnion"][];
/**
* Model class
* @description The name of the database model class.
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Help/terms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ galaxy:
invocations:
states:
scheduled: |
This state indicates the workflow invocation has had all of its job scheduled. This means all the
This state indicates the workflow invocation has had all of its jobs scheduled. This means all the
datasets are likely created and Galaxy has created the stubs for the jobs in the workflow. *The jobs
themselves might not have been queued or running.*

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<script setup lang="ts">
import { computed } from "vue";

import type { InvocationMessage } from "@/api/invocations";
import { useWorkflowInstance } from "@/composables/useWorkflowInstance";

import type { InvocationMessageResponseModel } from "./invocationMessageModel";

import GenericHistoryItem from "@/components/History/Content/GenericItem.vue";
import JobInformation from "@/components/JobInformation/JobInformation.vue";
import WorkflowInvocationStep from "@/components/WorkflowInvocationState/WorkflowInvocationStep.vue";
Expand Down Expand Up @@ -49,7 +48,7 @@ interface Invocation {
}

interface InvocationMessageProps {
invocationMessage: InvocationMessageResponseModel;
invocationMessage: InvocationMessage;
invocation: Invocation;
}

Expand Down Expand Up @@ -79,7 +78,7 @@ const workflowStep = computed(() => {
const dependentWorkflowStep = computed(() => {
if ("dependent_workflow_step_id" in props.invocationMessage && workflow.value) {
const stepId = props.invocationMessage["dependent_workflow_step_id"];
if (stepId !== undefined) {
if (stepId !== undefined && stepId !== null) {
return workflow.value.steps[stepId];
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script setup lang="ts">
import { faClock } from "@fortawesome/free-regular-svg-icons";
import { faArrowLeft, faEdit, faHdd, faSitemap } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BButton, BButtonGroup } from "bootstrap-vue";

import type { WorkflowInvocationElementView } from "@/api/invocations";
import { useWorkflowInstance } from "@/composables/useWorkflowInstance";
import localize from "@/utils/localization";

import Heading from "../Common/Heading.vue";
import SwitchToHistoryLink from "../History/SwitchToHistoryLink.vue";
import UtcDate from "../UtcDate.vue";
import WorkflowInvocationsCount from "../Workflow/WorkflowInvocationsCount.vue";
import WorkflowRunButton from "../Workflow/WorkflowRunButton.vue";

interface Props {
invocation: WorkflowInvocationElementView;
fromPanel?: boolean;
}

const props = defineProps<Props>();

const { workflow } = useWorkflowInstance(props.invocation.workflow_id);

function getWorkflowName(): string {
return workflow.value?.name || "...";
}
</script>

<template>
<div>
<div class="d-flex flex-gapx-1">
<Heading h1 separator inline truncate size="xl" class="flex-grow-1">
Invoked Workflow: "{{ getWorkflowName() }}"
</Heading>

<div v-if="!props.fromPanel">
<BButton
v-b-tooltip.hover.noninteractive
:title="localize('Return to Invocations List')"
class="text-nowrap"
size="sm"
variant="outline-primary"
to="/workflows/invocations">
<FontAwesomeIcon :icon="faArrowLeft" class="mr-1" />
<span v-localize>Invocations List</span>
</BButton>
</div>
</div>
<div class="py-2 pl-3 d-flex justify-content-between align-items-center">
<div>
<i>
<FontAwesomeIcon :icon="faClock" class="mr-1" />invoked
<UtcDate :date="props.invocation.update_time" mode="elapsed" />
</i>
<span class="d-flex flex-gapx-1 align-items-center">
<FontAwesomeIcon :icon="faHdd" />History:
<SwitchToHistoryLink :history-id="props.invocation.history_id" />
</span>
</div>
<div v-if="workflow" class="d-flex flex-gapx-1 align-items-center">
<div class="d-flex flex-column align-items-end mr-2">
<span v-if="workflow.version !== undefined" class="mb-1">
<FontAwesomeIcon :icon="faSitemap" />
Workflow Version: {{ workflow.version + 1 }}
</span>
<WorkflowInvocationsCount class="float-right" :workflow="workflow" />
</div>
<BButtonGroup vertical>
<BButton
v-b-tooltip.hover.noninteractive.html
:title="
!workflow.deleted
? `<b>Edit</b><br>${getWorkflowName()}`
: 'This workflow has been deleted.'
"
size="sm"
variant="secondary"
:disabled="workflow.deleted"
:to="`/workflows/edit?id=${workflow.id}&version=${workflow.version}`">
<FontAwesomeIcon :icon="faEdit" />
<span v-localize>Edit</span>
</BButton>
<WorkflowRunButton
:id="workflow.id || ''"
:title="
!workflow.deleted
? `<b>Rerun</b><br>${getWorkflowName()}`
: 'This workflow has been deleted.'
"
:disabled="workflow.deleted"
full
:version="workflow.version" />
</BButtonGroup>
</div>
</div>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@
import { createTestingPinia } from "@pinia/testing";
import { shallowMount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { getLocalVue } from "tests/jest/helpers";

import invocationData from "../Workflow/test/json/invocation.json";
import WorkflowInvocationOverview from "./WorkflowInvocationOverview";

const localVue = getLocalVue();

// Constants
const workflowData = {
id: "workflow-id",
name: "Test Workflow",
version: 0,
};
const selectors = {
bAlertStub: "balert-stub",
};
const alertMessages = {
unOwned: "Workflow is neither importable, nor owned by or shared with current user",
nonExistent: "No workflow found for this invocation.",
};

// Mock the workflow store to return the expected workflow data given the stored workflow ID
jest.mock("@/stores/workflowStore", () => {
const originalModule = jest.requireActual("@/stores/workflowStore");
return {
...originalModule,
useWorkflowStore: () => ({
...originalModule.useWorkflowStore(),
getStoredWorkflowByInstanceId: jest.fn().mockImplementation((workflowId) => {
if (["unowned-workflow", "nonexistant-workflow"].includes(workflowId)) {
return undefined;
}
return workflowData;
}),
fetchWorkflowForInstanceId: jest.fn().mockImplementation((workflowId) => {
if (workflowId === "unowned-workflow") {
throw new Error(alertMessages.unOwned);
}
}),
}),
};
});

describe("WorkflowInvocationOverview.vue with terminal invocation", () => {
let wrapper;
let propsData;
Expand Down Expand Up @@ -61,3 +98,39 @@ describe("WorkflowInvocationOverview.vue with invocation scheduling running", ()
expect(wrapper.find(".cancel-workflow-scheduling").exists()).toBeTruthy();
});
});

describe("WorkflowInvocationOverview.vue for a valid/invalid workflow", () => {
async function loadWrapper(invocationData) {
const propsData = {
invocation: invocationData,
invocationAndJobTerminal: true,
invocationSchedulingTerminal: true,
jobStatesSummary: {},
};
const wrapper = shallowMount(WorkflowInvocationOverview, {
propsData,
localVue,
});
await flushPromises();
return wrapper;
}

it("displays the workflow invocation graph for a valid workflow", async () => {
const wrapper = await loadWrapper(invocationData);
expect(wrapper.find("[data-description='workflow invocation graph']").exists()).toBeTruthy();
});

it("displays an alert for an unowned workflow", async () => {
const wrapper = await loadWrapper({ ...invocationData, workflow_id: "unowned-workflow" });
expect(wrapper.find("[data-description='workflow invocation graph']").exists()).toBeFalsy();
const alert = wrapper.find(selectors.bAlertStub);
expect(alert.text()).toContain(alertMessages.unOwned);
});

it("displays an alert for a nonexistant workflow", async () => {
const wrapper = await loadWrapper({ ...invocationData, workflow_id: "nonexistant-workflow" });
expect(wrapper.find("[data-description='workflow invocation graph']").exists()).toBeFalsy();
const alert = wrapper.find(selectors.bAlertStub);
expect(alert.text()).toContain(alertMessages.nonExistent);
});
});
Loading
Loading