Skip to content

Commit 3f0f537

Browse files
authored
Merge pull request galaxyproject#15076 from guerler/revise_display_application
Refactor display application handling
2 parents d44a8d6 + 576cb6b commit 3f0f537

18 files changed

Lines changed: 705 additions & 390 deletions

File tree

client/src/api/schema/schema.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,26 @@ export interface paths {
10111011
patch?: never;
10121012
trace?: never;
10131013
};
1014+
"/api/display_applications/create_link": {
1015+
parameters: {
1016+
query?: never;
1017+
header?: never;
1018+
path?: never;
1019+
cookie?: never;
1020+
};
1021+
get?: never;
1022+
put?: never;
1023+
/**
1024+
* Creates a link for display applications.
1025+
* @description Creates a link for display applications.
1026+
*/
1027+
post: operations["display_applications_create_link_api_display_applications_create_link_post"];
1028+
delete?: never;
1029+
options?: never;
1030+
head?: never;
1031+
patch?: never;
1032+
trace?: never;
1033+
};
10141034
"/api/display_applications/reload": {
10151035
parameters: {
10161036
query?: never;
@@ -8463,6 +8483,45 @@ export interface components {
84638483
*/
84648484
synopsis: string | null;
84658485
};
8486+
/** CreateLinkFeedback */
8487+
CreateLinkFeedback: {
8488+
/** Messages */
8489+
messages?: [string, string][] | null;
8490+
/** Preparable Steps */
8491+
preparable_steps?: components["schemas"]["CreateLinkStep"][] | null;
8492+
/**
8493+
* Refresh
8494+
* @default false
8495+
*/
8496+
refresh: boolean | null;
8497+
/** Resource */
8498+
resource?: string | null;
8499+
};
8500+
/** CreateLinkIncoming */
8501+
CreateLinkIncoming: {
8502+
/** App Name */
8503+
app_name: string;
8504+
/** Dataset Id */
8505+
dataset_id: string;
8506+
/** Kwd */
8507+
kwd?: {
8508+
[key: string]: string;
8509+
} | null;
8510+
/** Link Name */
8511+
link_name: string;
8512+
};
8513+
/** CreateLinkStep */
8514+
CreateLinkStep: {
8515+
/** Name */
8516+
name: string;
8517+
/**
8518+
* Ready
8519+
* @default false
8520+
*/
8521+
ready: boolean | null;
8522+
/** State */
8523+
state?: string | null;
8524+
};
84668525
/** CreateMetricsPayload */
84678526
CreateMetricsPayload: {
84688527
/**
@@ -27068,6 +27127,51 @@ export interface operations {
2706827127
};
2706927128
};
2707027129
};
27130+
display_applications_create_link_api_display_applications_create_link_post: {
27131+
parameters: {
27132+
query?: never;
27133+
header?: {
27134+
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
27135+
"run-as"?: string | null;
27136+
};
27137+
path?: never;
27138+
cookie?: never;
27139+
};
27140+
requestBody: {
27141+
content: {
27142+
"application/json": components["schemas"]["CreateLinkIncoming"];
27143+
};
27144+
};
27145+
responses: {
27146+
/** @description Successful Response */
27147+
200: {
27148+
headers: {
27149+
[name: string]: unknown;
27150+
};
27151+
content: {
27152+
"application/json": components["schemas"]["CreateLinkFeedback"];
27153+
};
27154+
};
27155+
/** @description Request Error */
27156+
"4XX": {
27157+
headers: {
27158+
[name: string]: unknown;
27159+
};
27160+
content: {
27161+
"application/json": components["schemas"]["MessageExceptionModel"];
27162+
};
27163+
};
27164+
/** @description Server Error */
27165+
"5XX": {
27166+
headers: {
27167+
[name: string]: unknown;
27168+
};
27169+
content: {
27170+
"application/json": components["schemas"]["MessageExceptionModel"];
27171+
};
27172+
};
27173+
};
27174+
};
2707127175
display_applications_reload_api_display_applications_reload_post: {
2707227176
parameters: {
2707327177
query?: never;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { mount } from "@vue/test-utils";
2+
import flushPromises from "flush-promises";
3+
import { getLocalVue } from "tests/jest/helpers";
4+
5+
import { GalaxyApi } from "@/api";
6+
7+
import DisplayApplicationCreateLink from "./DisplayApplication.vue";
8+
9+
jest.mock("@/api", () => {
10+
const post = jest.fn();
11+
return {
12+
GalaxyApi: jest.fn(() => ({
13+
POST: post,
14+
})),
15+
};
16+
});
17+
18+
const localVue = getLocalVue();
19+
20+
beforeEach(() => {
21+
jest.clearAllMocks();
22+
delete window.open;
23+
window.open = jest.fn();
24+
});
25+
26+
it("renders success with resource and opens window", async () => {
27+
GalaxyApi().POST.mockResolvedValueOnce({
28+
data: {
29+
resource: "http://example.com/res",
30+
messages: [],
31+
refresh: false,
32+
},
33+
error: null,
34+
});
35+
const wrapper = mount(DisplayApplicationCreateLink, {
36+
localVue,
37+
propsData: {
38+
appName: "igv",
39+
datasetId: "1",
40+
linkName: "local_default",
41+
},
42+
});
43+
await flushPromises();
44+
expect(window.open).toHaveBeenCalledWith("http://example.com/res", "_blank");
45+
expect(wrapper.text()).toContain("Display application is ready");
46+
expect(wrapper.text()).toContain("http://example.com");
47+
});
48+
49+
it("handles refresh loop once", async () => {
50+
jest.useFakeTimers();
51+
GalaxyApi()
52+
.POST.mockResolvedValueOnce({
53+
data: {
54+
refresh: true,
55+
messages: [["Waiting", "info"]],
56+
resource: null,
57+
},
58+
error: null,
59+
})
60+
.mockResolvedValueOnce({
61+
data: {
62+
resource: "http://example.com/final",
63+
refresh: false,
64+
messages: [],
65+
},
66+
error: null,
67+
});
68+
mount(DisplayApplicationCreateLink, {
69+
localVue,
70+
propsData: {
71+
appName: "igv",
72+
datasetId: "1",
73+
linkName: "local_default",
74+
},
75+
});
76+
await flushPromises();
77+
jest.runOnlyPendingTimers();
78+
await flushPromises();
79+
expect(window.open).toHaveBeenCalledWith("http://example.com/final", "_blank");
80+
});
81+
82+
it("renders error", async () => {
83+
GalaxyApi().POST.mockResolvedValueOnce({
84+
data: null,
85+
error: { err_msg: "Failed" },
86+
});
87+
const wrapper = mount(DisplayApplicationCreateLink, {
88+
localVue,
89+
propsData: {
90+
appName: "igv",
91+
datasetId: "1",
92+
linkName: "local_default",
93+
},
94+
});
95+
await flushPromises();
96+
expect(wrapper.text()).toContain("Failed to create link");
97+
});
98+
99+
it("renders initialization steps", async () => {
100+
GalaxyApi().POST.mockResolvedValueOnce({
101+
data: {
102+
refresh: false,
103+
resource: null,
104+
messages: [["Preparing", "info"]],
105+
preparable_steps: [
106+
{ name: "alpha", ready: false, state: "running" },
107+
{ name: "beta", ready: true, state: "ok" },
108+
],
109+
},
110+
error: null,
111+
});
112+
const wrapper = mount(DisplayApplicationCreateLink, {
113+
localVue,
114+
propsData: {
115+
appName: "igv",
116+
datasetId: "1",
117+
linkName: "local_default",
118+
},
119+
});
120+
await flushPromises();
121+
expect(wrapper.text()).toContain("Display Application Initialization");
122+
expect(wrapper.text()).toContain("alpha");
123+
expect(wrapper.text()).toContain("running");
124+
expect(wrapper.text()).toContain("beta");
125+
expect(wrapper.text()).toContain("ok");
126+
expect(window.open).not.toHaveBeenCalled();
127+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<script setup lang="ts">
2+
import { faCheck, faExternalLinkAlt, faSpinner } from "@fortawesome/free-solid-svg-icons";
3+
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
4+
import { BAlert } from "bootstrap-vue";
5+
import { computed, onMounted, ref } from "vue";
6+
7+
import { type components, GalaxyApi } from "@/api";
8+
import { errorMessageAsString } from "@/utils/simple-error";
9+
10+
import LoadingSpan from "@/components/LoadingSpan.vue";
11+
12+
type CreateLinkFeedback = components["schemas"]["CreateLinkFeedback"];
13+
14+
const TIMEOUT = 2000;
15+
16+
interface Props {
17+
appName: string;
18+
datasetId: string;
19+
linkName: string;
20+
}
21+
22+
const props = defineProps<Props>();
23+
24+
const applicationData = ref<CreateLinkFeedback>({ refresh: false });
25+
const errorMessage = ref("");
26+
const isLoading = ref(true);
27+
28+
const hasData = computed(() => !!applicationData.value);
29+
const hostUrl = computed(() => {
30+
if (applicationData.value?.resource) {
31+
try {
32+
return new URL(applicationData.value.resource).origin;
33+
} catch {
34+
return null;
35+
}
36+
}
37+
return null;
38+
});
39+
40+
async function requestLink() {
41+
const { data, error } = await GalaxyApi().POST("/api/display_applications/create_link", {
42+
body: {
43+
app_name: props.appName,
44+
dataset_id: props.datasetId,
45+
link_name: props.linkName,
46+
},
47+
});
48+
if (error) {
49+
errorMessage.value = `Failed to create link: ${errorMessageAsString(error.err_msg)}`;
50+
} else {
51+
errorMessage.value = "";
52+
applicationData.value = data;
53+
if (applicationData.value.resource) {
54+
window.open(applicationData.value.resource, "_blank");
55+
} else if (applicationData.value.refresh) {
56+
setTimeout(() => requestLink(), TIMEOUT);
57+
}
58+
}
59+
isLoading.value = false;
60+
}
61+
62+
onMounted(() => {
63+
requestLink();
64+
});
65+
</script>
66+
67+
<template>
68+
<div>
69+
<BAlert v-if="errorMessage" variant="danger" show>
70+
{{ errorMessage }}
71+
</BAlert>
72+
<LoadingSpan v-else-if="isLoading" />
73+
<div v-else-if="hasData">
74+
<div v-for="(message, messageIndex) in applicationData.messages" :key="messageIndex">
75+
<BAlert :variant="message[1]" show>
76+
<FontAwesomeIcon v-if="applicationData.refresh" spin :icon="faSpinner" />
77+
<span>{{ message[0] }}</span>
78+
</BAlert>
79+
</div>
80+
<div v-if="applicationData.preparable_steps">
81+
<h4 class="my-2">Display Application Initialization</h4>
82+
<div v-for="(step, stepIndex) in applicationData.preparable_steps" :key="stepIndex" class="my-2">
83+
<FontAwesomeIcon v-if="step.ready" class="text-success" :icon="faCheck" />
84+
<FontAwesomeIcon v-else class="text-primary" spin :icon="faSpinner" />
85+
<span class="ml-1">{{ step.name }}</span>
86+
<span>('{{ step.state }}')</span>
87+
</div>
88+
</div>
89+
<BAlert v-if="applicationData.resource" variant="info" show>
90+
<span>
91+
<span>Display application is ready and can be viewed at</span>
92+
<a :href="applicationData.resource" target="_blank">
93+
<span>{{ hostUrl }}</span>
94+
<FontAwesomeIcon :icon="faExternalLinkAlt" />
95+
</a>
96+
</span>
97+
</BAlert>
98+
</div>
99+
</div>
100+
</template>

0 commit comments

Comments
 (0)