Skip to content

Commit 7a3c08c

Browse files
authored
Merge pull request #20145 from ahmedhamidawan/show_info_on_job_success
Show job ids on job success
2 parents 507719b + bb25979 commit 7a3c08c

9 files changed

Lines changed: 198 additions & 96 deletions

File tree

client/src/api/jobs.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,25 @@ export type JobDetails = components["schemas"]["ShowFullJobResponse"] | componen
77
export type JobInputSummary = components["schemas"]["JobInputSummary"];
88
export type JobDisplayParametersSummary = components["schemas"]["JobDisplayParametersSummary"];
99
export type JobMetric = components["schemas"]["JobMetric"];
10+
11+
interface JobDef {
12+
tool_id: string;
13+
}
14+
export interface JobResponse {
15+
produces_entry_points: boolean;
16+
jobs: Array<JobBaseModel | ShowFullJobResponse>;
17+
outputs: {
18+
hid: number;
19+
name: string;
20+
}[]; // TODO: This is temporary, adjust when API response is typed
21+
output_collections: {
22+
hid: number;
23+
name: string;
24+
}[]; // TODO: This is temporary, adjust when API response is typed
25+
// implicit_collections // TODO: Add when API response is typed
26+
}
27+
export interface ResponseVal {
28+
jobDef: JobDef;
29+
jobResponse: JobResponse;
30+
toolName: string;
31+
}

client/src/components/Markdown/Sections/Elements/JobMetrics.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ const toolId = computed(() => {
3333
});
3434
const toolVersion = computed(() => {
3535
if (targetJobId.value) {
36-
return jobStore.getJob(targetJobId.value)?.tool_version;
36+
// TODO: `ShowFullJobResponse` does not have a `tool_version` property
37+
return (jobStore.getJob(targetJobId.value) as any)?.tool_version;
3738
}
3839
return undefined;
3940
});
@@ -44,7 +45,7 @@ const { selectJobOptions, selectedJob, targetJobId } = useMappingJobs(jobIdRef,
4445
4546
async function init() {
4647
if (targetJobId.value) {
47-
jobStore.fetchJob(targetJobId.value);
48+
jobStore.fetchJob({ id: targetJobId.value });
4849
}
4950
}
5051

client/src/components/Markdown/Sections/Elements/JobParameters.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ const toolId = computed(() => {
3535
});
3636
const toolVersion = computed(() => {
3737
if (targetJobId.value) {
38-
return jobStore.getJob(targetJobId.value)?.tool_version;
38+
// TODO: `ShowFullJobResponse` does not have a `tool_version` property
39+
return (jobStore.getJob(targetJobId.value) as any)?.tool_version;
3940
}
4041
return undefined;
4142
});
@@ -46,7 +47,7 @@ const { selectJobOptions, selectedJob, targetJobId } = useMappingJobs(jobIdRef,
4647
4748
async function init() {
4849
if (targetJobId.value) {
49-
jobStore.fetchJob(targetJobId.value);
50+
jobStore.fetchJob({ id: targetJobId.value });
5051
}
5152
}
5253

client/src/components/Markdown/Sections/Elements/ToolStd.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const { selectJobOptions, selectedJob, targetJobId } = useMappingJobs(jobIdRef,
2727
2828
async function init() {
2929
if (targetJobId.value) {
30-
jobStore.fetchJob(targetJobId.value);
30+
jobStore.fetchJob({ id: targetJobId.value });
3131
}
3232
}
3333
@@ -40,7 +40,7 @@ watch(
4040
);
4141
4242
const jobContent = computed(() => {
43-
let content: string | undefined;
43+
let content: string | null | undefined;
4444
if (targetJobId.value) {
4545
const job = jobStore.getJob(targetJobId.value);
4646
content = job && job[props.name];

client/src/components/Tool/ToolCard.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const showHelpForum = computed(() => isConfigLoaded.value && config.value.enable
119119
120120
<template>
121121
<FormCardSticky
122-
:error-message="errorText"
122+
:error-message="errorText || ''"
123123
:description="props.description"
124124
:name="props.title"
125125
:version="props.version">

client/src/components/Tool/ToolSuccess.vue

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { BAlert } from "bootstrap-vue";
3+
import { storeToRefs } from "pinia";
34
import { computed } from "vue";
45
import { useRouter } from "vue-router/composables";
56
@@ -14,16 +15,16 @@ import ToolEntryPoints from "@/components/ToolEntryPoints/ToolEntryPoints.vue";
1415
1516
const { config } = useConfig(true);
1617
const jobStore = useJobStore();
18+
const { latestResponse } = storeToRefs(jobStore);
1719
const router = useRouter();
1820
19-
const jobDef = computed(() => responseVal.value.jobDef);
20-
const jobResponse = computed(() => responseVal.value.jobResponse);
21-
const responseVal = computed(() => jobStore.getLatestResponse);
21+
const jobDef = computed(() => latestResponse.value?.jobDef);
22+
const jobResponse = computed(() => latestResponse.value?.jobResponse);
2223
const showRecommendation = computed(() => config.value.enable_tool_recommendations);
23-
const toolName = computed(() => responseVal.value.toolName);
24+
const toolName = computed(() => latestResponse.value?.toolName);
2425
2526
// no data means that no tool was run in this session i.e. no data in the store
26-
if (Object.keys(responseVal.value).length === 0) {
27+
if (!latestResponse.value || Object.keys(latestResponse.value).length === 0) {
2728
router.push(`/`);
2829
}
2930
</script>
@@ -37,9 +38,9 @@ if (Object.keys(responseVal.value).length === 0) {
3738
<div v-if="jobResponse?.produces_entry_points">
3839
<ToolEntryPoints v-for="job in jobResponse.jobs" :key="job.id" :job-id="job.id" />
3940
</div>
40-
<ToolSuccessMessage :job-response="jobResponse" :tool-name="toolName" />
41-
<Webhook type="tool" :tool-id="jobDef.tool_id" />
42-
<ToolRecommendation v-if="showRecommendation" :tool-id="jobDef.tool_id" />
41+
<ToolSuccessMessage :job-response="jobResponse" :tool-name="toolName || '...'" />
42+
<Webhook v-if="jobDef" type="tool" :tool-id="jobDef.tool_id" />
43+
<ToolRecommendation v-if="showRecommendation && jobDef" :tool-id="jobDef.tool_id" />
4344
</div>
4445
</section>
4546
</template>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { mount, type Wrapper } from "@vue/test-utils";
2+
3+
import jobInformationResponse from "@/components/JobInformation/testData/jobInformationResponse.json";
4+
5+
import ToolSuccessMessage from "./ToolSuccessMessage.vue";
6+
7+
// Prop constants
8+
const TEST_JOB_RESPONSE = {
9+
produces_entry_points: false,
10+
jobs: [jobInformationResponse],
11+
outputs: [
12+
{
13+
hid: 0,
14+
name: "output1",
15+
},
16+
],
17+
output_collections: [
18+
{
19+
hid: 1,
20+
name: "collection1",
21+
},
22+
],
23+
};
24+
const TEST_TOOL_NAME = "Test Tool";
25+
26+
// Selectors
27+
const SELECTORS = {
28+
SINGULAR_JOB_LINK: "[data-description='singular job link']",
29+
JOB_LINK: "[data-description='job link']",
30+
OUTPUTS_LIST: "[data-description='list of outputs']",
31+
};
32+
33+
describe("ToolSuccessMessage", () => {
34+
let wrapper: Wrapper<Vue>;
35+
36+
beforeEach(async () => {
37+
wrapper = mount(ToolSuccessMessage as object, {
38+
propsData: {
39+
jobResponse: TEST_JOB_RESPONSE,
40+
toolName: TEST_TOOL_NAME,
41+
},
42+
stubs: {
43+
FontAwesomeIcon: true,
44+
},
45+
});
46+
});
47+
48+
it("shows both dataset and collection outputs correctly", async () => {
49+
expect(wrapper.find(SELECTORS.OUTPUTS_LIST).text()).toContain(
50+
`${TEST_JOB_RESPONSE.outputs[0]?.hid}: ${TEST_JOB_RESPONSE.outputs[0]?.name}`
51+
);
52+
expect(wrapper.find(SELECTORS.OUTPUTS_LIST).text()).toContain(
53+
`${TEST_JOB_RESPONSE.output_collections[0]?.hid}: ${TEST_JOB_RESPONSE.output_collections[0]?.name}`
54+
);
55+
});
56+
57+
it("has a link to the singular job", () => {
58+
expect(wrapper.find(SELECTORS.SINGULAR_JOB_LINK).exists()).toBe(true);
59+
});
60+
61+
it("shows the tool name", () => {
62+
expect(wrapper.text()).toContain(`Started tool ${TEST_TOOL_NAME}`);
63+
});
64+
65+
it("has a link to each job when multiple jobs are present", async () => {
66+
await wrapper.setProps({
67+
jobResponse: {
68+
...TEST_JOB_RESPONSE,
69+
jobs: [
70+
jobInformationResponse,
71+
{
72+
...jobInformationResponse,
73+
id: "test_id_2",
74+
},
75+
],
76+
},
77+
});
78+
expect(wrapper.findAll(SELECTORS.JOB_LINK).length).toBe(2);
79+
});
80+
});

client/src/components/Tool/ToolSuccessMessage.vue

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
1+
<script setup lang="ts">
2+
import { computed } from "vue";
3+
import { RouterLink } from "vue-router";
4+
5+
import type { JobResponse } from "@/api/jobs";
6+
7+
import ExternalLink from "../ExternalLink.vue";
8+
9+
const props = defineProps<{
10+
jobResponse: JobResponse;
11+
toolName: string;
12+
}>();
13+
14+
const outputs = computed(() => props.jobResponse.outputs.concat(props.jobResponse.output_collections));
15+
16+
const nJobs = computed(() => (props.jobResponse && props.jobResponse.jobs ? props.jobResponse.jobs.length : 0));
17+
18+
const nJobsText = computed(() => (nJobs.value > 1 ? `${nJobs.value} jobs` : `1 job`));
19+
20+
const nOutputsText = computed(() => (outputs.value.length > 1 ? `${outputs.value.length} outputs` : `this output`));
21+
</script>
22+
123
<template>
224
<div class="donemessagelarge">
325
<p>
4-
Started tool <b>{{ toolName }}</b> and successfully added {{ nJobsText }} to the queue.
26+
Started tool <b>{{ props.toolName }}</b> and successfully added {{ nJobsText }} to the queue.
527
</p>
628
<p>It produces {{ nOutputsText }}:</p>
7-
<ul>
8-
<li v-for="item of jobResponse.outputs" :key="item.hid">
29+
<ul data-description="list of outputs">
30+
<li v-for="item of outputs" :key="item.hid">
931
<b>{{ item.hid }}: {{ item.name }}</b>
1032
</li>
1133
</ul>
@@ -14,34 +36,21 @@
1436
the job has been run the status will change from 'running' to 'finished' if completed successfully or
1537
'error' if problems were encountered.
1638
</p>
39+
<p v-if="nJobs > 1">
40+
Here is a link to each job:
41+
<ExternalLink
42+
v-for="job in jobResponse.jobs"
43+
:key="job.id"
44+
:href="`/jobs/${job.id}/view`"
45+
data-description="job link">
46+
{{ job.id }}
47+
</ExternalLink>
48+
</p>
49+
<p v-else-if="jobResponse.jobs[0]" data-description="singular job link">
50+
Here is a link to the job: {{ jobResponse.jobs[0]?.id }}
51+
<RouterLink :to="`/jobs/${jobResponse.jobs[0].id}/view`">
52+
{{ jobResponse.jobs[0]?.id }}
53+
</RouterLink>
54+
</p>
1755
</div>
1856
</template>
19-
20-
<script>
21-
export default {
22-
props: {
23-
jobResponse: {
24-
type: Object,
25-
required: true,
26-
},
27-
toolName: {
28-
type: String,
29-
required: true,
30-
},
31-
},
32-
computed: {
33-
nOutputs() {
34-
return this.jobResponse && this.jobResponse.outputs ? this.jobResponse.outputs.length : 0;
35-
},
36-
nJobs() {
37-
return this.jobResponse && this.jobResponse.jobs ? this.jobResponse.jobs.length : 0;
38-
},
39-
nJobsText() {
40-
return this.nJobs > 1 ? `${this.nJobs} jobs` : `1 job`;
41-
},
42-
nOutputsText() {
43-
return this.nOutputs > 1 ? `${this.nOutputs} outputs` : `this output`;
44-
},
45-
},
46-
};
47-
</script>

client/src/stores/jobStore.ts

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,57 +3,45 @@
33
* Requests response generated by a tool run
44
*/
55

6-
import axios from "axios";
76
import { defineStore } from "pinia";
8-
import Vue from "vue";
7+
import { ref } from "vue";
98

10-
import { getAppRoot } from "@/onload/loadConfig";
9+
import { GalaxyApi } from "@/api";
10+
import type { ResponseVal, ShowFullJobResponse } from "@/api/jobs";
11+
import { type FetchParams, useKeyedCache } from "@/composables/keyedCache";
12+
import { rethrowSimple } from "@/utils/simple-error";
1113

12-
/* interfaces */
13-
interface Job {
14-
id: string;
15-
tool_id: string;
16-
tool_version: string;
17-
tool_stdout: string;
18-
tool_stderr: string;
19-
}
20-
interface JobDef {
21-
tool_id: string;
22-
}
23-
interface JobResponse {
24-
produces_entry_points: boolean;
25-
jobs: Array<Job>;
26-
}
27-
interface ResponseVal {
28-
jobDef: JobDef;
29-
jobResponse: JobResponse;
30-
toolName: string;
31-
}
14+
export const useJobStore = defineStore("jobStore", () => {
15+
const latestResponse = ref<ResponseVal | null>(null);
3216

33-
export const useJobStore = defineStore("jobStore", {
34-
state: () => ({
35-
jobs: {} as { [index: string]: Job },
36-
response: {} as ResponseVal,
37-
}),
38-
getters: {
39-
getJob: (state) => {
40-
return (jobId: string) => state.jobs[jobId];
41-
},
42-
getLatestResponse: (state) => {
43-
return state.response;
44-
},
45-
},
46-
actions: {
47-
async fetchJob(jobId: string) {
48-
const { data } = await axios.get(`${getAppRoot()}api/jobs/${jobId}?full=true`);
49-
this.saveJobForJobId(jobId, data);
50-
},
51-
// Setters
52-
saveJobForJobId(jobId: string, job: Job) {
53-
Vue.set(this.jobs, jobId, job);
54-
},
55-
saveLatestResponse(response: ResponseVal) {
56-
this.response = response;
57-
},
58-
},
17+
async function fetchJobById(params: FetchParams): Promise<ShowFullJobResponse> {
18+
const { data, error } = await GalaxyApi().GET("/api/jobs/{job_id}", {
19+
params: { path: { job_id: params.id } },
20+
query: { full: true },
21+
});
22+
if (error) {
23+
rethrowSimple(error);
24+
}
25+
return data;
26+
}
27+
28+
function saveLatestResponse(newResponse: ResponseVal) {
29+
latestResponse.value = newResponse;
30+
}
31+
32+
const {
33+
fetchItemById: fetchJob,
34+
getItemById: getJob,
35+
getItemLoadError: getJobLoadError,
36+
isLoadingItem: isLoadingJob,
37+
} = useKeyedCache<ShowFullJobResponse>(fetchJobById);
38+
39+
return {
40+
fetchJob,
41+
saveLatestResponse,
42+
getJob,
43+
getJobLoadError,
44+
isLoadingJob,
45+
latestResponse,
46+
};
5947
});

0 commit comments

Comments
 (0)