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");
+  });
+});

Reply via email to