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 1d1e486f72328b84a5896431e77b8cf3435dc58f Author: yasithdev <[email protected]> AuthorDate: Fri Apr 24 22:41:57 2026 -0400 feat(launcher): ScalarInputRow + FileInputRow --- .../js/components/launch/FileInputRow.vue | 43 +++++++++++++++++ .../js/components/launch/ScalarInputRow.vue | 56 ++++++++++++++++++++++ .../unit/components/launch/FileInputRow.spec.ts | 45 +++++++++++++++++ .../unit/components/launch/ScalarInputRow.spec.ts | 31 ++++++++++++ 4 files changed, 175 insertions(+) diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileInputRow.vue b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileInputRow.vue new file mode 100644 index 000000000..df78b521d --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/FileInputRow.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-path-${descriptor.name}`" + :value="modelValue?.path ?? ''" + placeholder="/path/to/data" + @input="onPath(($event.target as HTMLInputElement).value)" + /> + <span data-test="io-badge" class="badge bg-warning text-dark">stage-in</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/ScalarInputRow.vue b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ScalarInputRow.vue new file mode 100644 index 000000000..6b33e2f90 --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ScalarInputRow.vue @@ -0,0 +1,56 @@ +<template> + <div class="row mb-1 align-items-center"> + <label class="col-md-3 col-form-label" :for="`scalar-${descriptor.name}`"> + <code>{{ descriptor.name }}</code> + <span class="text-muted small ms-1">{{ descriptor.type }}</span> + </label> + <div class="col-md-9"> + <select + v-if="descriptor.type === 'enum'" + :id="`scalar-${descriptor.name}`" + :data-test="`scalar-${descriptor.name}`" + class="form-select" + :value="modelValue ?? ''" + @change="emit('update:modelValue', ($event.target as HTMLSelectElement).value)" + > + <option value="" disabled>Choose…</option> + <option v-for="o in descriptor.options ?? []" :key="o" :value="o">{{ o }}</option> + </select> + <input + v-else + :id="`scalar-${descriptor.name}`" + :data-test="`scalar-${descriptor.name}`" + class="form-control" + :type="inputType" + :value="modelValue ?? ''" + @input="onInput(($event.target as HTMLInputElement).value)" + /> + </div> + </div> +</template> + +<script setup lang="ts"> +import { computed } from "vue"; +import type { IODescriptor, ScalarValue } from "django-airavata-common-ui/js/stores/launch-types"; + +const props = defineProps<{ descriptor: IODescriptor; modelValue: ScalarValue | null }>(); +const emit = defineEmits<{ "update:modelValue": [v: ScalarValue | null] }>(); + +const inputType = computed(() => { + switch (props.descriptor.type) { + case "int": + case "float": + return "number"; + case "bool": + return "checkbox"; + default: + return "text"; + } +}); + +function onInput(raw: string) { + if (props.descriptor.type === "int") emit("update:modelValue", raw === "" ? null : Number.parseInt(raw, 10)); + else if (props.descriptor.type === "float") emit("update:modelValue", raw === "" ? null : Number.parseFloat(raw)); + else emit("update:modelValue", raw); +} +</script> diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/FileInputRow.spec.ts b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/FileInputRow.spec.ts new file mode 100644 index 000000000..c16ab713c --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/FileInputRow.spec.ts @@ -0,0 +1,45 @@ +import { mount } from "@vue/test-utils"; +import { describe, expect, it } from "vitest"; +import FileInputRow from "../../../../js/components/launch/FileInputRow.vue"; + +const STORAGES = [ + { storage_id: "my-home", name: "My Home", is_primary: true }, + { storage_id: "scratch", name: "Scratch", is_primary: false }, +]; + +describe("FileInputRow", () => { + it("emits update:modelValue when storage changes", async () => { + const w = mount(FileInputRow, { + props: { + descriptor: { name: "sim_dir", type: "dir", required: true }, + modelValue: { storage_id: "my-home", path: "" }, + storages: STORAGES, + }, + }); + await w.find("select").setValue("scratch"); + expect(w.emitted("update:modelValue")?.at(-1)).toEqual([{ storage_id: "scratch", path: "" }]); + }); + + it("emits update:modelValue when path changes", async () => { + const w = mount(FileInputRow, { + props: { + descriptor: { name: "sim_dir", type: "dir", required: true }, + modelValue: { storage_id: "my-home", path: "" }, + storages: STORAGES, + }, + }); + await w.find("input[data-test='file-path-sim_dir']").setValue("/home/x/sim"); + expect(w.emitted("update:modelValue")?.at(-1)).toEqual([{ storage_id: "my-home", path: "/home/x/sim" }]); + }); + + it("renders a stage-in badge", () => { + const w = mount(FileInputRow, { + props: { + descriptor: { name: "sim_dir", type: "dir", required: true }, + modelValue: null, + storages: STORAGES, + }, + }); + expect(w.find("[data-test='io-badge']").text()).toMatch(/stage-in/i); + }); +}); diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/ScalarInputRow.spec.ts b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/ScalarInputRow.spec.ts new file mode 100644 index 000000000..2d5c92bd6 --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/ScalarInputRow.spec.ts @@ -0,0 +1,31 @@ +import { mount } from "@vue/test-utils"; +import { describe, expect, it } from "vitest"; +import ScalarInputRow from "../../../../js/components/launch/ScalarInputRow.vue"; + +describe("ScalarInputRow", () => { + it("renders a number input for type=int", async () => { + const w = mount(ScalarInputRow, { + props: { descriptor: { name: "steps", type: "int", required: true }, modelValue: null }, + }); + const input = w.find("input[data-test='scalar-steps']"); + expect(input.attributes("type")).toBe("number"); + }); + + it("emits update:modelValue on input", async () => { + const w = mount(ScalarInputRow, { + props: { descriptor: { name: "steps", type: "int", required: true }, modelValue: null }, + }); + await w.find("input").setValue("42"); + expect(w.emitted("update:modelValue")?.at(-1)).toEqual([42]); + }); + + it("renders a select for enum descriptors", () => { + const w = mount(ScalarInputRow, { + props: { + descriptor: { name: "mode", type: "enum", required: true, options: ["a", "b"] }, + modelValue: null, + }, + }); + expect(w.find("select").exists()).toBe(true); + }); +});
