From 845800a3f1cb55a1c4e55ff0bad2a318e0752909 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Sat, 12 Sep 2026 17:18:38 -0700 Subject: [PATCH 01/75] break-all wrap in toasts --- client/packages/ui/src/components/GToast.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/client/packages/ui/src/components/GToast.vue b/client/packages/ui/src/components/GToast.vue index 9c87d2b40a8..a0e8c21ac1b 100644 --- a/client/packages/ui/src/components/GToast.vue +++ b/client/packages/ui/src/components/GToast.vue @@ -146,6 +146,7 @@ const isActiveHost = computed(() => props.host === activeToastHost.value); .g-toast-body { margin-top: 0.25rem; + word-break: break-all; } } From fb6cf7826def19cd00675a50a6b34cb5c272dca1 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Mon, 14 Sep 2026 17:21:57 -0700 Subject: [PATCH 02/75] modernize `JobInformation` in the form of a card --- .../src/components/JobInformation/CodeRow.vue | 144 ++++++-- .../components/JobInformation/JobDetails.vue | 32 +- .../components/JobInformation/JobHeader.vue | 48 +++ .../JobInformation/JobInformation.test.js | 9 +- .../JobInformation/JobInformation.vue | 342 +++++++++++------- client/src/components/JobStates/JobState.vue | 11 +- client/src/entry/analysis/router.js | 5 +- client/src/stores/toolStore.ts | 4 +- 8 files changed, 398 insertions(+), 197 deletions(-) create mode 100644 client/src/components/JobInformation/JobHeader.vue diff --git a/client/src/components/JobInformation/CodeRow.vue b/client/src/components/JobInformation/CodeRow.vue index cd39cfd301d..e07ccd1b463 100644 --- a/client/src/components/JobInformation/CodeRow.vue +++ b/client/src/components/JobInformation/CodeRow.vue @@ -1,43 +1,35 @@ - diff --git a/client/src/components/JobInformation/JobDetails.vue b/client/src/components/JobInformation/JobDetails.vue index 88a87636898..228ed04d169 100644 --- a/client/src/components/JobInformation/JobDetails.vue +++ b/client/src/components/JobInformation/JobDetails.vue @@ -13,39 +13,25 @@ const props = defineProps<{ job?: JobBaseModel; jobId?: string; invocationId?: string; + showHeader?: boolean; }>(); const id = computed(() => props.job?.id || props.jobId); -// Curious as to why we're trying to access traceback, info and remote_host like this, when they don't exist on -// `JobBaseModel`? Possibly historical reasons? (leaving as is for now) -const traceback = computed(() => (props.job && "traceback" in props.job ? (props.job?.traceback as string) : null)); +// Curious as to why we're trying to access info and remote_host like this, when they don't exist on +// `JobBaseModel`? Maybe in the admin view we fetch more properties? (leaving as is for now) const info = computed(() => (props.job && "info" in props.job ? (props.job?.info as string) : null)); const remoteHost = computed(() => (props.job && "remote_host" in props.job ? (props.job.remote_host as string) : null)); diff --git a/client/src/components/Tool/ToolSuccessMessage.test.ts b/client/src/components/Tool/ToolSuccessMessage.test.ts deleted file mode 100644 index 58c11ef754c..00000000000 --- a/client/src/components/Tool/ToolSuccessMessage.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { getLocalVue, injectTestRouter } from "@tests/vitest/helpers"; -import { mount, type VueWrapper } from "@vue/test-utils"; -import flushPromises from "flush-promises"; -import { beforeEach, describe, expect, it } from "vitest"; - -import jobInformationResponse from "@/components/JobInformation/testData/jobInformationResponse.json"; - -import ToolSuccessMessage from "./ToolSuccessMessage.vue"; - -const localVue = getLocalVue(); -const router = injectTestRouter(localVue); - -// Prop constants -const TEST_JOB_RESPONSE = { - produces_entry_points: false, - jobs: [jobInformationResponse], - outputs: [ - { - hid: 0, - name: "output1", - }, - ], - output_collections: [ - { - hid: 1, - name: "collection1", - }, - ], -}; -const TEST_TOOL_NAME = "Test Tool"; - -// Selectors -const SELECTORS = { - SINGULAR_JOB_LINK: "[data-description='singular job link']", - JOB_LINK: "[data-description='job link']", - OUTPUTS_LIST: "[data-description='list of outputs']", -}; - -describe("ToolSuccessMessage", () => { - let wrapper: VueWrapper; - - beforeEach(async () => { - wrapper = mount(ToolSuccessMessage as object, { - global: localVue, - router, - props: { - jobResponse: TEST_JOB_RESPONSE, - toolName: TEST_TOOL_NAME, - }, - stubs: { - FontAwesomeIcon: true, - }, - }); - }); - - it("shows both dataset and collection outputs correctly", async () => { - expect(wrapper.find(SELECTORS.OUTPUTS_LIST).text()).toContain( - `${TEST_JOB_RESPONSE.outputs[0]?.hid}: ${TEST_JOB_RESPONSE.outputs[0]?.name}`, - ); - expect(wrapper.find(SELECTORS.OUTPUTS_LIST).text()).toContain( - `${TEST_JOB_RESPONSE.output_collections[0]?.hid}: ${TEST_JOB_RESPONSE.output_collections[0]?.name}`, - ); - }); - - it("has a link to the singular job", () => { - expect(wrapper.find(SELECTORS.SINGULAR_JOB_LINK).exists()).toBe(true); - }); - - it("shows the tool name", () => { - expect(wrapper.text()).toContain(`Started tool ${TEST_TOOL_NAME}`); - }); - - it("has a link to each job when multiple jobs are present", async () => { - // Remount with two-job props instead of setProps -- VTU v2 + Vue 3 doesn't - // reliably re-render when a top-level prop object is replaced wholesale. - wrapper = mount(ToolSuccessMessage as object, { - global: localVue, - router, - props: { - jobResponse: { - ...TEST_JOB_RESPONSE, - jobs: [ - jobInformationResponse, - { - ...jobInformationResponse, - id: "test_id_2", - }, - ], - }, - toolName: TEST_TOOL_NAME, - }, - stubs: { - FontAwesomeIcon: true, - }, - }); - await flushPromises(); - expect(wrapper.findAll(SELECTORS.JOB_LINK).length).toBe(2); - }); -}); diff --git a/client/src/components/Tool/ToolSuccessMessage.vue b/client/src/components/Tool/ToolSuccessMessage.vue deleted file mode 100644 index 50885f7c287..00000000000 --- a/client/src/components/Tool/ToolSuccessMessage.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - From 36151863ae658fd8cd0b14f0482b9cbc4ca1a046 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Wed, 16 Sep 2026 17:09:48 -0700 Subject: [PATCH 17/75] add outputs to the tool success view --- client/src/api/jobs.ts | 4 ++ .../src/components/Tool/ToolSuccess.test.ts | 25 +++++++++++- client/src/components/Tool/ToolSuccess.vue | 15 +++---- .../components/Tool/ToolSuccessOutputs.vue | 39 +++++++++++++++++++ 4 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 client/src/components/Tool/ToolSuccessOutputs.vue diff --git a/client/src/api/jobs.ts b/client/src/api/jobs.ts index 24cae77d4b2..e2f856b7775 100644 --- a/client/src/api/jobs.ts +++ b/client/src/api/jobs.ts @@ -44,10 +44,14 @@ export interface JobResponse { outputs: { hid: number; name: string; + history_content_type: "dataset"; + id: string; }[]; // TODO: This is temporary, adjust when API response is typed output_collections: { hid: number; name: string; + history_content_type: "dataset_collection"; + id: string; }[]; // TODO: This is temporary, adjust when API response is typed // implicit_collections // TODO: Add when API response is typed errors?: any; diff --git a/client/src/components/Tool/ToolSuccess.test.ts b/client/src/components/Tool/ToolSuccess.test.ts index c98c92d4d82..cd188e08d2a 100644 --- a/client/src/components/Tool/ToolSuccess.test.ts +++ b/client/src/components/Tool/ToolSuccess.test.ts @@ -12,6 +12,7 @@ import jobInformationResponse from "@/components/JobInformation/testData/jobInfo import { useConfig } from "@/composables/config"; import ToolSuccess from "./ToolSuccess.vue"; +import ToolSuccessOutputs from "./ToolSuccessOutputs.vue"; import JobHeader from "@/components/JobInformation/JobHeader.vue"; import JobInformation from "@/components/JobInformation/JobInformation.vue"; import ToolRecommendation from "@/components/ToolRecommendation.vue"; @@ -91,6 +92,7 @@ async function mountToolSuccess(latestResponse: Record | null, JobHeader: true, Webhook: true, ToolRecommendation: true, + ToolSuccessOutputs: true, }, }) as VueWrapper; @@ -150,9 +152,28 @@ describe("ToolSuccess", () => { it("does not show the multi-job header or job count badge", () => { expect(wrapper.find(SELECTORS.JOB_COUNT_BADGE).exists()).toBe(false); }); + }); - // TODO: Add testing for rendering outputs - // it("shows both dataset and collection outputs correctly", async () => { + describe("with outputs", () => { + const TEST_OUTPUT = { id: "output_id", hid: 1, name: "output1" }; + const TEST_OUTPUT_COLLECTION = { id: "collection_id", hid: 2, name: "collection1" }; + + it("passes both dataset and collection outputs through to ToolSuccessOutputs", async () => { + const wrapper = await mountToolSuccess({ + jobDef: TEST_JOB_DEF, + jobResponse: { + ...TEST_JOB_RESPONSE, + outputs: [TEST_OUTPUT], + output_collections: [TEST_OUTPUT_COLLECTION], + }, + toolName: TEST_TOOL_NAME, + }); + + const outputs = wrapper.findComponent(ToolSuccessOutputs); + expect(outputs.exists()).toBe(true); + expect(outputs.props("jobResponse").outputs).toEqual([TEST_OUTPUT]); + expect(outputs.props("jobResponse").output_collections).toEqual([TEST_OUTPUT_COLLECTION]); + }); }); describe("with multiple jobs", () => { diff --git a/client/src/components/Tool/ToolSuccess.vue b/client/src/components/Tool/ToolSuccess.vue index be27fa7bf4f..851b6b0e477 100644 --- a/client/src/components/Tool/ToolSuccess.vue +++ b/client/src/components/Tool/ToolSuccess.vue @@ -27,6 +27,7 @@ import Heading from "@/components/Common/Heading.vue"; import Webhook from "@/components/Common/Webhook.vue"; import JobHeader from "@/components/JobInformation/JobHeader.vue"; import JobInformation from "@/components/JobInformation/JobInformation.vue"; +import ToolSuccessOutputs from "@/components/Tool/ToolSuccessOutputs.vue"; import ToolEntryPoints from "@/components/ToolEntryPoints/ToolEntryPoints.vue"; const { config } = useConfig(); @@ -117,15 +118,15 @@ const webhookId = computed(() => webhook.value?.webhookId ?? null);
- + + + diff --git a/client/src/components/Tool/ToolSuccessOutputs.vue b/client/src/components/Tool/ToolSuccessOutputs.vue new file mode 100644 index 00000000000..fa3e2fb6cf0 --- /dev/null +++ b/client/src/components/Tool/ToolSuccessOutputs.vue @@ -0,0 +1,39 @@ + + + + + From e565e1e66914feb8013ada6cf64b9930bab00b4e Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Wed, 16 Sep 2026 17:38:50 -0700 Subject: [PATCH 18/75] remove `useJobBasic` and use `jobDetails` instead; make `JobState` consume id Now `JobState` is reactive to the job's state changing. --- .../History/Graph/HistoryGraphNodeBody.vue | 6 +-- .../History/Graph/ToolExecutionJobs.vue | 11 +----- .../components/JobInformation/JobHeader.vue | 8 ++-- .../src/components/JobStates/JobState.test.ts | 18 +++++++-- client/src/components/JobStates/JobState.vue | 38 ++++++++++++------- .../WorkflowInvocationState/JobStep.test.ts | 31 ++++++++++++++- .../WorkflowInvocationState/JobStepJobs.vue | 2 +- client/src/composables/useJobBasic.ts | 14 ------- 8 files changed, 77 insertions(+), 51 deletions(-) delete mode 100644 client/src/composables/useJobBasic.ts diff --git a/client/src/components/History/Graph/HistoryGraphNodeBody.vue b/client/src/components/History/Graph/HistoryGraphNodeBody.vue index 2b00b00ff2c..4c9b11c208c 100644 --- a/client/src/components/History/Graph/HistoryGraphNodeBody.vue +++ b/client/src/components/History/Graph/HistoryGraphNodeBody.vue @@ -2,7 +2,6 @@ import { computed } from "vue"; import { useCreatingJob } from "@/composables/useCreatingJob"; -import { useJobBasic } from "@/composables/useJobBasic"; import type { HistoryGraphNode } from "./historyGraphMapper"; @@ -32,7 +31,6 @@ const infoIcon = computed(() => props.node?.icon); // For dataset/collection nodes, resolve the creating job and fetch its basic // details for the JobState badge / RerunJobButton in the GTabs nav-end. const { jobId: creatingJobId, loading: lookupLoading, error: lookupError } = useCreatingJob(itemId, nodeSrc); -const { job } = useJobBasic(creatingJobId);