This is an automated email from the ASF dual-hosted git repository. yasithdev pushed a commit to branch feat/generic-experiment-launcher in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
commit dc6303902c043f45021188fd05b05eebca3607d8 Author: yasithdev <[email protected]> AuthorDate: Fri Apr 24 22:58:38 2026 -0400 feat(launcher): Tab3ReviewLaunch preview fetch + launch flow --- .../js/components/launch/Tab3ReviewLaunch.vue | 76 +++++++++++++++++++++- .../components/launch/Tab3ReviewLaunch.spec.ts | 59 +++++++++++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/Tab3ReviewLaunch.vue b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/Tab3ReviewLaunch.vue index 25a445eed..9fe0c97c7 100644 --- a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/Tab3ReviewLaunch.vue +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/Tab3ReviewLaunch.vue @@ -1,2 +1,74 @@ -<template><div /></template> -<script setup lang="ts"></script> +<template> + <div> + <div v-if="loading" class="text-muted">Generating preview…</div> + <div v-else-if="error" class="alert alert-danger"> + <div>{{ error }}</div> + <button class="btn btn-sm btn-outline-light mt-2" @click="refresh">Retry</button> + </div> + <div v-else-if="store.preview"> + <ul v-if="store.preview.warnings.length" class="alert alert-warning small" data-test="warnings"> + <li v-for="w in store.preview.warnings" :key="w">{{ w }}</li> + </ul> + <InvocationCommand :command="store.preview.invocation_command" /> + <ScriptPreview :script="store.preview.script_contents" /> + <div v-if="launchError" class="alert alert-danger mt-2"> + {{ launchError }} + <button class="btn btn-sm btn-outline-light ms-2" @click="onLaunch">Try again</button> + </div> + </div> + <div class="d-flex justify-content-end mt-3"> + <button + class="btn btn-primary" + data-test="launch" + :disabled="!store.preview || loading" + @click="onLaunch" + > + Launch experiment + </button> + </div> + </div> +</template> + +<script setup lang="ts"> +import { onMounted, ref } from "vue"; +import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch"; +import { launcherService } from "django-airavata-common-ui/js/services/launcherService"; +import InvocationCommand from "./InvocationCommand.vue"; +import ScriptPreview from "./ScriptPreview.vue"; + +const store = useLaunchStore(); +const loading = ref(false); +const error = ref<string | null>(null); +const launchError = ref<string | null>(null); +let abort: AbortController | null = null; + +async function refresh() { + if (store.preview && store.lastPreviewedHash === store.draftHash) return; + loading.value = true; + error.value = null; + abort?.abort(); + abort = new AbortController(); + try { + const r = await launcherService.generatePreview(store.draft, abort.signal); + store.preview = r; + store.lastPreviewedHash = store.draftHash; + } catch (e) { + error.value = (e as Error).message; + store.preview = null; + } finally { + loading.value = false; + } +} + +async function onLaunch() { + launchError.value = null; + try { + const { experiment_id } = await launcherService.launchExperiment(store.draft); + window.location.href = `/workspace/experiments/${experiment_id}`; + } catch (e) { + launchError.value = (e as Error).message; + } +} + +onMounted(() => { void refresh(); }); +</script> diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/Tab3ReviewLaunch.spec.ts b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/Tab3ReviewLaunch.spec.ts new file mode 100644 index 000000000..cbd6fae19 --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/Tab3ReviewLaunch.spec.ts @@ -0,0 +1,59 @@ +import { mount, flushPromises } from "@vue/test-utils"; +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import Tab3ReviewLaunch from "../../../../js/components/launch/Tab3ReviewLaunch.vue"; +import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch"; + +vi.mock("django-airavata-common-ui/js/services/launcherService", () => ({ + launcherService: { + generatePreview: vi.fn(), + launchExperiment: vi.fn(), + }, +})); + +import { launcherService } from "django-airavata-common-ui/js/services/launcherService"; + +const NAV = vi.fn(); +Object.defineProperty(window, "location", { value: { href: "/", assign: NAV }, writable: true }); + +describe("Tab3ReviewLaunch", () => { + beforeEach(() => { + setActivePinia(createPinia()); + vi.mocked(launcherService.generatePreview).mockResolvedValue({ + invocation_command: "sbatch /tmp/run.sh", + script_contents: "#!/bin/bash\necho hi\n", + warnings: ["check walltime"], + }); + vi.mocked(launcherService.launchExperiment).mockResolvedValue({ experiment_id: "exp-42" }); + NAV.mockClear(); + window.location.href = "/"; + }); + + it("renders the preview after fetch", async () => { + const w = mount(Tab3ReviewLaunch); + await flushPromises(); + expect(w.text()).toContain("sbatch /tmp/run.sh"); + expect(w.text()).toContain("echo hi"); + }); + + it("renders warnings as a list", async () => { + const w = mount(Tab3ReviewLaunch); + await flushPromises(); + expect(w.find("[data-test='warnings']").text()).toContain("check walltime"); + }); + + it("disables launch when preview failed", async () => { + vi.mocked(launcherService.generatePreview).mockRejectedValueOnce(new Error("nope")); + const w = mount(Tab3ReviewLaunch); + await flushPromises(); + expect(w.find("button[data-test='launch']").attributes("disabled")).toBeDefined(); + }); + + it("redirects on successful launch", async () => { + const w = mount(Tab3ReviewLaunch); + await flushPromises(); + await w.find("button[data-test='launch']").trigger("click"); + await flushPromises(); + expect(window.location.href).toBe("/workspace/experiments/exp-42"); + }); +});
