You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ya...@apache.org on 2026/04/25 05:16:36 UTC
(airavata-portals) 19/34: feat(launcher): ScalarInputRow + FileInputRow
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 <ya...@icloud.com>
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);
+ });
+});