Skip to content

Commit

Permalink
Merge pull request #17203 from davelopez/migrate_invocation_store_pinia
Browse files Browse the repository at this point in the history
Migrate workflow invocation store to Pinia
  • Loading branch information
jmchilton authored Dec 22, 2023
2 parents 4bb1c4b + 635fe41 commit fc8e287
Show file tree
Hide file tree
Showing 12 changed files with 171 additions and 181 deletions.
42 changes: 42 additions & 0 deletions client/src/api/invocations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import axios from "axios";

import { getAppRoot } from "@/onload";

import { ApiResponse } from "./schema";

// TODO: Replace these interfaces with real schema models after https://github.com/galaxyproject/galaxy/pull/16707 is merged
export interface WorkflowInvocation {
id: string;
}

export interface WorkflowInvocationJobsSummary {
id: string;
}

export interface WorkflowInvocationStep {
id: string;
}

// TODO: Replace these provisional functions with fetchers after https://github.com/galaxyproject/galaxy/pull/16707 is merged
export async function fetchInvocationDetails(params: { id: string }): Promise<ApiResponse<WorkflowInvocation>> {
const { data } = await axios.get(`${getAppRoot()}api/invocations/${params.id}`);
return {
data,
} as ApiResponse<WorkflowInvocation>;
}

export async function fetchInvocationJobsSummary(params: {
id: string;
}): Promise<ApiResponse<WorkflowInvocationJobsSummary>> {
const { data } = await axios.get(`${getAppRoot()}api/invocations/${params.id}/jobs_summary`);
return {
data,
} as ApiResponse<WorkflowInvocationJobsSummary>;
}

export async function fetchInvocationStep(params: { id: string }): Promise<ApiResponse<WorkflowInvocationStep>> {
const { data } = await axios.get(`${getAppRoot()}api/invocations//any/steps/${params.id}`);
return {
data,
} as ApiResponse<WorkflowInvocationStep>;
}
2 changes: 2 additions & 0 deletions client/src/components/Workflow/InvocationsList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ describe("InvocationsList.vue", () => {

beforeEach(async () => {
axiosMock = new MockAdapter(axios);
axiosMock.onGet(`api/invocations/${mockInvocationData.id}`).reply(200, mockInvocationData);
axiosMock.onGet(`api/invocations/${mockInvocationData.id}/jobs_summary`).reply(200, {});
});

afterEach(() => {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { createTestingPinia } from "@pinia/testing";
import { shallowMount, Wrapper } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { setActivePinia } from "pinia";
import { getLocalVue } from "tests/jest/helpers";

import type { WorkflowInvocation } from "@/api/invocations";
import JOB_STATES_MODEL from "@/utils/job-states-model";

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

import WorkflowInvocationState from "./WorkflowInvocationState.vue";

const localVue = getLocalVue();

const selectors = {
invocationSummary: ".invocation-summary",
};

const invocationJobsSummaryById = {
id: "d9833097445452b0",
model: "WorkflowInvocation",
states: {},
populated_state: "ok",
};

async function mountWorkflowInvocationState(invocation: WorkflowInvocation | null) {
const pinia = createTestingPinia();
setActivePinia(pinia);

const wrapper = shallowMount(WorkflowInvocationState, {
propsData: {
invocationId: invocationData.id,
},
computed: {
invocation: () => invocation,
jobStatesSummary: () => new JOB_STATES_MODEL.JobStatesSummary(invocationJobsSummaryById),
},
pinia,
localVue,
});
await flushPromises();
return wrapper;
}

describe("WorkflowInvocationState.vue", () => {
it("determines that invocation and job states are terminal with terminal invocation", async () => {
const wrapper = await mountWorkflowInvocationState(invocationData);
expect(isInvocationAndJobTerminal(wrapper)).toBe(true);
});

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

it("determines that invocation and job states are not terminal with non-terminal invocation", async () => {
const invocation = {
...invocationData,
state: "new",
};
const wrapper = await mountWorkflowInvocationState(invocation);
expect(isInvocationAndJobTerminal(wrapper)).toBe(false);
});
});

function isInvocationAndJobTerminal(wrapper: Wrapper<Vue>): boolean {
const invocationSummary = wrapper.find(selectors.invocationSummary);
// This is a somewhat hacky way to determine if the invocation and job states are terminal without
// exposing the internals of the component. This is just to restore the previous behavior of the test
// but it would be better to test this in a more appropriate way.
return invocationSummary.exists() && invocationSummary.html().includes('invocationandjobterminal="true"');
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<b-tabs v-if="invocation">
<b-tab title="Summary" active>
<WorkflowInvocationSummary
class="invocation-summary"
:invocation="invocation"
:index="index"
:invocation-and-job-terminal="invocationAndJobTerminal"
Expand All @@ -10,9 +11,7 @@
@invocation-cancelled="cancelWorkflowScheduling" />
</b-tab>
<b-tab title="Details">
<WorkflowInvocationDetails
:invocation="invocation"
:invocation-and-job-terminal="invocationAndJobTerminal" />
<WorkflowInvocationDetails :invocation="invocation" />
</b-tab>
<!-- <b-tab title="Workflow Overview">
<p>TODO: Insert readonly version of workflow editor here</p>
Expand All @@ -34,7 +33,8 @@
import mixin from "components/JobStates/mixin";
import LoadingSpan from "components/LoadingSpan";
import JOB_STATES_MODEL from "utils/job-states-model";
import { mapActions, mapGetters } from "vuex";
import { useInvocationStore } from "@/stores/invocationStore";
import { cancelWorkflowScheduling } from "./services";
Expand All @@ -61,16 +61,21 @@ export default {
default: null,
},
},
setup() {
const invocationStore = useInvocationStore();
return {
invocationStore,
};
},
data() {
return {
stepStatesInterval: null,
jobStatesInterval: null,
};
},
computed: {
...mapGetters(["getInvocationById", "getInvocationJobsSummaryById"]),
invocation: function () {
return this.getInvocationById(this.invocationId);
return this.invocationStore.getInvocationById(this.invocationId);
},
invocationState: function () {
return this.invocation?.state || "new";
Expand All @@ -93,7 +98,7 @@ export default {
return this.jobStatesSummary && this.jobStatesSummary.terminal();
},
jobStatesSummary() {
const jobsSummary = this.getInvocationJobsSummaryById(this.invocationId);
const jobsSummary = this.invocationStore.getInvocationJobsSummaryById(this.invocationId);
return !jobsSummary ? null : new JOB_STATES_MODEL.JobStatesSummary(jobsSummary);
},
},
Expand All @@ -106,19 +111,16 @@ export default {
clearTimeout(this.stepStatesInterval);
},
methods: {
...mapActions(["fetchInvocationForId", "fetchInvocationJobsSummaryForId"]),
pollStepStatesUntilTerminal: function () {
pollStepStatesUntilTerminal: async function () {
if (!this.invocation || !this.invocationSchedulingTerminal) {
this.fetchInvocationForId(this.invocationId).then((response) => {
this.stepStatesInterval = setTimeout(this.pollStepStatesUntilTerminal, 3000);
});
await this.invocationStore.fetchInvocationForId({ id: this.invocationId });
this.stepStatesInterval = setTimeout(this.pollStepStatesUntilTerminal, 3000);
}
},
pollJobStatesUntilTerminal: function () {
pollJobStatesUntilTerminal: async function () {
if (!this.jobStatesTerminal) {
this.fetchInvocationJobsSummaryForId(this.invocationId).then((response) => {
this.jobStatesInterval = setTimeout(this.pollJobStatesUntilTerminal, 3000);
});
await this.invocationStore.fetchInvocationJobsSummaryForId({ id: this.invocationId });
this.jobStatesInterval = setTimeout(this.pollJobStatesUntilTerminal, 3000);
}
},
onError: function (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ import WorkflowIcons from "components/Workflow/icons";
import { mapActions, mapState } from "pinia";
import { useToolStore } from "stores/toolStore";
import { useWorkflowStore } from "stores/workflowStore";
import { mapActions as vuexMapActions, mapGetters } from "vuex";
import JobStep from "./JobStep";
import ParameterStep from "./ParameterStep";
Expand Down Expand Up @@ -126,7 +125,6 @@ export default {
computed: {
...mapState(useWorkflowStore, ["getStoredWorkflowByInstanceId"]),
...mapState(useToolStore, ["getToolForId", "getToolNameById"]),
...mapGetters(["getInvocationStepById"]),
isReady() {
return this.invocation.steps.length > 0;
},
Expand Down Expand Up @@ -156,7 +154,6 @@ export default {
methods: {
...mapActions(useWorkflowStore, ["fetchWorkflowForInstanceId"]),
...mapActions(useToolStore, ["fetchToolForId"]),
...vuexMapActions(["fetchInvocationStepById"]),
fetchTool() {
if (this.workflowStep.tool_id && !this.getToolForId(this.workflowStep.tool_id)) {
this.fetchToolForId(this.workflowStep.tool_id);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
import { shallowMount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";
import Vuex from "vuex";

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

const invocationJobsSummaryById = {
id: "d9833097445452b0",
model: "WorkflowInvocation",
states: {},
populated_state: "ok",
};

const mockComputed = {
getInvocationJobsSummaryById: () => () => invocationJobsSummaryById,
};

const localVue = getLocalVue();

describe("WorkflowInvocationSummary.vue with terminal invocation", () => {
Expand All @@ -30,7 +18,6 @@ describe("WorkflowInvocationSummary.vue with terminal invocation", () => {
};
wrapper = shallowMount(WorkflowInvocationSummary, {
propsData,
computed: mockComputed,
localVue,
});
});
Expand All @@ -49,16 +36,8 @@ describe("WorkflowInvocationSummary.vue with invocation scheduling running", ()
let wrapper;
let propsData;
let store;
let actions;

beforeEach(async () => {
actions = {
fetchInvocationForId: jest.fn(),
fetchInvocationJobsSummaryForId: jest.fn(),
};
store = new Vuex.Store({
actions,
});
propsData = {
invocation: invocationData,
invocationAndJobTerminal: false,
Expand All @@ -67,7 +46,6 @@ describe("WorkflowInvocationSummary.vue with invocation scheduling running", ()
wrapper = shallowMount(WorkflowInvocationSummary, {
store,
propsData,
computed: mockComputed,
localVue,
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ import mixin from "components/JobStates/mixin";
import LoadingSpan from "components/LoadingSpan";
import ProgressBar from "components/ProgressBar";
import { getRootFromIndexLink } from "onload";
import { mapGetters } from "vuex";
import InvocationMessage from "@/components/WorkflowInvocationState/InvocationMessage.vue";
Expand Down Expand Up @@ -122,7 +121,6 @@ export default {
};
},
computed: {
...mapGetters(["getInvocationById", "getInvocationJobsSummaryById"]),
invocationId() {
return this.invocation?.id;
},
Expand Down
Loading

0 comments on commit fc8e287

Please sign in to comment.