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 fad7719f0e83693f706d9a2b89f90f82652d149c Author: yasithdev <[email protected]> AuthorDate: Fri Apr 24 22:43:40 2026 -0400 feat(launcher): InputList + OutputList + FileOutputRow --- .../js/components/launch/FileOutputRow.vue | 43 ++++++++++++++++++++ .../js/components/launch/InputList.vue | 33 ++++++++++++++++ .../js/components/launch/OutputList.vue | 25 ++++++++++++ .../tests/unit/components/launch/InputList.spec.ts | 46 ++++++++++++++++++++++ 4 files changed, 147 insertions(+) diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileOutputRow.vue b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileOutputRow.vue new file mode 100644 index 000000000..338add1f0 --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileOutputRow.vue @@ -0,0 +1,43 @@ +<template> + <div class="d-flex align-items-center gap-2 border rounded p-2 mb-1"> + <div class="flex-shrink-0" style="width: 140px;"> + <code>{{ descriptor.name }}</code> + <span class="text-muted small ms-1">{{ descriptor.type }}</span> + </div> + <select + class="form-select form-select-sm" + style="width: 160px;" + :value="modelValue?.storage_id ?? ''" + @change="onStorage(($event.target as HTMLSelectElement).value)" + > + <option value="" disabled>Storageā¦</option> + <option v-for="s in storages" :key="s.storage_id" :value="s.storage_id">{{ s.name }}</option> + </select> + <input + class="form-control form-control-sm flex-grow-1" + :data-test="`file-out-path-${descriptor.name}`" + :value="modelValue?.path ?? ''" + placeholder="/path/to/output" + @input="onPath(($event.target as HTMLInputElement).value)" + /> + <span class="badge bg-info text-dark">stage-out</span> + </div> +</template> + +<script setup lang="ts"> +import type { IODescriptor, StorageRef, UserStorage } from "django-airavata-common-ui/js/stores/launch-types"; + +const props = defineProps<{ + descriptor: IODescriptor; + modelValue: StorageRef | null; + storages: UserStorage[]; +}>(); +const emit = defineEmits<{ "update:modelValue": [v: StorageRef] }>(); + +function onStorage(id: string) { + emit("update:modelValue", { storage_id: id, path: props.modelValue?.path ?? "" }); +} +function onPath(p: string) { + emit("update:modelValue", { storage_id: props.modelValue?.storage_id ?? "", path: p }); +} +</script> diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/InputList.vue b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/InputList.vue new file mode 100644 index 000000000..8768a604d --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/InputList.vue @@ -0,0 +1,33 @@ +<template> + <div v-if="store.pickedInterface"> + <div + v-for="io in store.pickedInterface.inputs" + :key="io.name" + data-test="input-row" + > + <FileInputRow + v-if="io.type === 'file' || io.type === 'dir'" + :descriptor="io" + :model-value="(store.draft.inputs[io.name] as { storage_id: string; path: string } | null) ?? null" + :storages="storages" + @update:model-value="store.setInput(io.name, $event)" + /> + <ScalarInputRow + v-else + :descriptor="io" + :model-value="(store.draft.inputs[io.name] as string | number | boolean | null) ?? null" + @update:model-value="store.setInput(io.name, $event)" + /> + </div> + </div> +</template> + +<script setup lang="ts"> +import type { UserStorage } from "django-airavata-common-ui/js/stores/launch-types"; +import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch"; +import FileInputRow from "./FileInputRow.vue"; +import ScalarInputRow from "./ScalarInputRow.vue"; + +defineProps<{ storages: UserStorage[] }>(); +const store = useLaunchStore(); +</script> diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/OutputList.vue b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/OutputList.vue new file mode 100644 index 000000000..a74e31d28 --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/OutputList.vue @@ -0,0 +1,25 @@ +<template> + <div v-if="store.pickedInterface"> + <FileOutputRow + v-for="io in fileOutputs" + :key="io.name" + :descriptor="io" + :model-value="store.draft.outputs[io.name] ?? null" + :storages="storages" + @update:model-value="store.setOutput(io.name, $event)" + /> + </div> +</template> + +<script setup lang="ts"> +import { computed } from "vue"; +import type { UserStorage } from "django-airavata-common-ui/js/stores/launch-types"; +import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch"; +import FileOutputRow from "./FileOutputRow.vue"; + +defineProps<{ storages: UserStorage[] }>(); +const store = useLaunchStore(); +const fileOutputs = computed(() => + (store.pickedInterface?.outputs ?? []).filter((o) => o.type === "file" || o.type === "dir"), +); +</script> diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/InputList.spec.ts b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/InputList.spec.ts new file mode 100644 index 000000000..2354fe1be --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/InputList.spec.ts @@ -0,0 +1,46 @@ +import { mount } from "@vue/test-utils"; +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, describe, expect, it } from "vitest"; +import InputList from "../../../../js/components/launch/InputList.vue"; +import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch"; + +const APP = { + app_id: "namd", name: "NAMD", category: "MD", + content: { kind: "github" as const, url: "g" }, + interfaces: [{ + name: "run", + inputs: [ + { name: "sim_dir", type: "dir" as const, required: true }, + { name: "steps", type: "int" as const, required: true }, + ], + outputs: [], + }], +}; + +const STORAGES = [{ storage_id: "my-home", name: "My Home", is_primary: true }]; + +describe("InputList", () => { + beforeEach(() => { + setActivePinia(createPinia()); + const s = useLaunchStore(); + s.pickApp(APP); + s.pickInterface("run"); + }); + + it("renders one row per declared input", () => { + const w = mount(InputList, { props: { storages: STORAGES } }); + expect(w.findAll("[data-test='input-row']")).toHaveLength(2); + }); + + it("scalar input writes through to the store", async () => { + const w = mount(InputList, { props: { storages: STORAGES } }); + await w.find("input[data-test='scalar-steps']").setValue("5000"); + expect(useLaunchStore().draft.inputs.steps).toBe(5000); + }); + + it("file input writes through to the store", async () => { + const w = mount(InputList, { props: { storages: STORAGES } }); + await w.find("input[data-test='file-path-sim_dir']").setValue("/data"); + expect(useLaunchStore().draft.inputs.sim_dir).toEqual({ storage_id: "", path: "/data" }); + }); +});
