Skip to content

Commit

Permalink
Merge pull request #18730 from ahmedhamidawan/invocation_view_unowned…
Browse files Browse the repository at this point in the history
…_workflow

[24.1] Handle error when workflow is unowned in Invocation view
  • Loading branch information
mvdbeek authored Aug 28, 2024
2 parents 99c1a92 + 91e7ce2 commit c7097a0
Show file tree
Hide file tree
Showing 14 changed files with 330 additions and 459 deletions.
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 @@ -8192,6 +8192,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 @@ -13459,19 +13471,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

0 comments on commit c7097a0

Please sign in to comment.