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 fbb34a3f2e2c0d58dce4734891d3a85beb6d9dbf Author: yasithdev <[email protected]> AuthorDate: Fri Apr 24 22:38:06 2026 -0400 feat(launcher): AppPicker (categories + search + tile grid) --- .../js/components/launch/AppPicker.vue | 80 ++++++++++++++++++++++ .../tests/unit/components/launch/AppPicker.spec.ts | 41 +++++++++++ 2 files changed, 121 insertions(+) diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/AppPicker.vue b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/AppPicker.vue new file mode 100644 index 000000000..7df9ef1ad --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/AppPicker.vue @@ -0,0 +1,80 @@ +<template> + <div> + <div class="d-flex flex-wrap gap-1 mb-2"> + <button + v-for="cat in categories" + :key="cat" + type="button" + class="btn btn-sm" + :class="cat === activeCat ? 'btn-primary' : 'btn-light'" + :data-test="`cat-${cat}`" + @click="activeCat = cat" + > + {{ cat }}<span class="ms-1 opacity-50">{{ countByCat[cat] }}</span> + </button> + </div> + <input + v-model="search" + type="search" + class="form-control mb-2" + data-test="app-search" + placeholder="Filter…" + /> + <div class="row g-2"> + <div + v-for="a in filtered" + :key="a.app_id" + class="col-6 col-md-3" + data-test="app-tile" + > + <button + type="button" + class="card w-100 text-start p-2" + :class="{ 'border-primary': store.draft.app_id === a.app_id }" + :data-test="`app-tile-${a.app_id}`" + @click="pick(a)" + > + <strong>{{ a.name }}</strong> + <span class="text-muted small d-block">{{ a.content.url }}</span> + </button> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { computed, ref } from "vue"; +import type { Application } from "django-airavata-common-ui/js/stores/launch-types"; +import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch"; + +const props = defineProps<{ applications: Application[] }>(); +const store = useLaunchStore(); +const activeCat = ref<string>("All"); +const search = ref(""); + +const categories = computed(() => { + const set = new Set<string>(["All"]); + for (const a of props.applications) set.add(a.category); + return Array.from(set); +}); + +const countByCat = computed<Record<string, number>>(() => { + const out: Record<string, number> = { All: props.applications.length }; + for (const a of props.applications) out[a.category] = (out[a.category] ?? 0) + 1; + return out; +}); + +const filtered = computed(() => { + let xs = props.applications; + if (activeCat.value !== "All") xs = xs.filter((a) => a.category === activeCat.value); + if (search.value) { + const n = search.value.toLowerCase(); + xs = xs.filter((a) => a.name.toLowerCase().includes(n)); + } + return xs; +}); + +function pick(a: Application) { + store.pickApp(a); +} +</script> diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/AppPicker.spec.ts b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/AppPicker.spec.ts new file mode 100644 index 000000000..3bf80e2e1 --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/AppPicker.spec.ts @@ -0,0 +1,41 @@ +import { mount } from "@vue/test-utils"; +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, describe, expect, it } from "vitest"; +import AppPicker from "../../../../js/components/launch/AppPicker.vue"; +import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch"; + +const APPS = [ + { app_id: "namd", name: "NAMD", category: "Molecular Dynamics", + content: { kind: "github" as const, url: "g" }, interfaces: [] }, + { app_id: "gromacs", name: "GROMACS", category: "Molecular Dynamics", + content: { kind: "tarball" as const, url: "t" }, interfaces: [] }, + { app_id: "alphafold", name: "AlphaFold", category: "ML / AI", + content: { kind: "github" as const, url: "g" }, interfaces: [] }, +]; + +describe("AppPicker", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("renders all apps when category=All", () => { + const w = mount(AppPicker, { props: { applications: APPS } }); + expect(w.findAll("[data-test='app-tile']")).toHaveLength(3); + }); + + it("filters by category chip", async () => { + const w = mount(AppPicker, { props: { applications: APPS } }); + await w.find("[data-test='cat-ML / AI']").trigger("click"); + expect(w.findAll("[data-test='app-tile']")).toHaveLength(1); + }); + + it("filters by search text within current category", async () => { + const w = mount(AppPicker, { props: { applications: APPS } }); + await w.find("input[data-test='app-search']").setValue("NAMD"); + expect(w.findAll("[data-test='app-tile']")).toHaveLength(1); + }); + + it("clicking a tile picks the app via the store", async () => { + const w = mount(AppPicker, { props: { applications: APPS } }); + await w.find("[data-test='app-tile-namd']").trigger("click"); + expect(useLaunchStore().draft.app_id).toBe("namd"); + }); +});
