diff --git a/client/src/api/invocations.ts b/client/src/api/invocations.ts index c604a40998b3..f4aaf3cf798e 100644 --- a/client/src/api/invocations.ts +++ b/client/src/api/invocations.ts @@ -19,6 +19,15 @@ export interface WorkflowInvocationStep { id: string; } +export async function invocationForJob(params: { jobId: string }): Promise { + const { data } = await axios.get(`${getAppRoot()}api/invocations?job_id=${params.jobId}`); + if (data.length > 0) { + return data[0] as WorkflowInvocation; + } else { + return null; + } +} + // 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> { const { data } = await axios.get(`${getAppRoot()}api/invocations/${params.id}`); diff --git a/client/src/components/JobInformation/JobInformation.test.js b/client/src/components/JobInformation/JobInformation.test.js index 0eab9f09f0e0..f90dd1385492 100644 --- a/client/src/components/JobInformation/JobInformation.test.js +++ b/client/src/components/JobInformation/JobInformation.test.js @@ -22,6 +22,7 @@ describe("JobInformation/JobInformation.vue", () => { axiosMock = new MockAdapter(axios); axiosMock.onGet(new RegExp(`api/configuration/decode/*`)).reply(200, { decoded_id: 123 }); axiosMock.onGet("/api/jobs/test_id?full=True").reply(200, jobResponse); + axiosMock.onGet("/api/invocations?job_id=test_id").reply(200, []); }); afterEach(() => { diff --git a/client/src/components/JobInformation/JobInformation.vue b/client/src/components/JobInformation/JobInformation.vue index 8ee844248c71..95fe2d8cb7bf 100644 --- a/client/src/components/JobInformation/JobInformation.vue +++ b/client/src/components/JobInformation/JobInformation.vue @@ -89,6 +89,12 @@ {{ job.copied_from_job_id }} + + Workflow Invocation + + {{ invocationId }} + + @@ -103,6 +109,8 @@ import { formatDuration, intervalToDuration } from "date-fns"; import { getAppRoot } from "onload/loadConfig"; import JOB_STATES_MODEL from "utils/job-states-model"; +import { invocationForJob } from "@/api/invocations"; + import DecodedId from "../DecodedId.vue"; import CodeRow from "./CodeRow.vue"; @@ -128,6 +136,7 @@ export default { data() { return { job: null, + invocationId: null, }; }, computed: { @@ -139,6 +148,9 @@ export default { jobIsTerminal() { return this.job && !JOB_STATES_MODEL.NON_TERMINAL_STATES.includes(this.job.state); }, + routeToInvocation() { + return `/workflows/invocations/${this.invocationId}`; + }, }, methods: { getAppRoot() { @@ -146,6 +158,17 @@ export default { }, updateJob(job) { this.job = job; + if (job) { + this.fetchInvocation(job.id); + } + }, + async fetchInvocation(jobId) { + if (jobId) { + const invocation = await invocationForJob({ jobId: jobId }); + if (invocation) { + this.invocationId = invocation.id; + } + } }, }, }; diff --git a/client/src/components/Workflow/InvocationsList.vue b/client/src/components/Workflow/InvocationsList.vue index 69419752fefd..3b882f535cd0 100644 --- a/client/src/components/Workflow/InvocationsList.vue +++ b/client/src/components/Workflow/InvocationsList.vue @@ -24,7 +24,8 @@ Last updated: ; Invocation ID: {{ row.item.id }}Invocation ID: + {{ row.item.id }} @@ -222,6 +223,9 @@ export default { swapRowDetails(row) { row.toggleDetails(); }, + invocationLink(item) { + return `/workflows/invocations/${item.id}`; + }, switchHistory(historyId) { const Galaxy = getGalaxyInstance(); Galaxy.currHistoryPanel.switchToHistory(historyId); diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index de818e3d34a5..d5d6690122f5 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -71,6 +71,7 @@ import Sharing from "@/components/Sharing/SharingPage.vue"; import HistoryStorageOverview from "@/components/User/DiskUsage/Visualizations/HistoryStorageOverview.vue"; import UserDatasetPermissions from "@/components/User/UserDatasetPermissions.vue"; import WorkflowPublished from "@/components/Workflow/Published/WorkflowPublished.vue"; +import WorkflowInvocationState from "@/components/WorkflowInvocationState/WorkflowInvocationState.vue"; Vue.use(VueRouter); @@ -527,6 +528,11 @@ export function getRouter(Galaxy) { invocationId: route.query.id, }), }, + { + path: "workflows/invocations/:invocationId", + component: WorkflowInvocationState, + props: true, + }, { path: "workflows/list", component: WorkflowList, diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index bad2c03b7ea3..47e8d47ca971 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -272,6 +272,7 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs): webapp.add_client_route("/workflows/trs_import") webapp.add_client_route("/workflows/trs_search") webapp.add_client_route("/workflows/invocations") + webapp.add_client_route("/workflows/invocations/{invocation_id}") webapp.add_client_route("/workflows/sharing") webapp.add_client_route("/workflows/{stored_workflow_id}/invocations") webapp.add_client_route("/workflows/invocations/report")