diff --git a/app/src/components/ProgramList.tsx b/app/src/components/ProgramList.tsx index fc04bc4..06a76f2 100644 --- a/app/src/components/ProgramList.tsx +++ b/app/src/components/ProgramList.tsx @@ -1,18 +1,49 @@ import { useMemo, useState } from "react" import type { ProgramSummary } from "@/types" import { ProgramCard } from "./ProgramCard" +import { kindLabel } from "@/lib/labels" +import { cn } from "@/lib/utils" interface ProgramListProps { programs: ProgramSummary[] linkBase?: string // where each card links (default "/programs") showDeployments?: boolean // list each program's deployments on the card (default true) + filterable?: boolean // show deployment-kind filter chips (default false) } -export function ProgramList({ programs, linkBase, showDeployments }: ProgramListProps) { +const KIND_ORDER = ["service", "job", "tool", "static", "reference"] + +// Active chip color per kind — mirrors KindBadge so the filter reads as the badge. +const KIND_ACTIVE: Record = { + service: "bg-green-700 text-white border-green-600", + job: "bg-purple-700 text-white border-purple-600", + tool: "bg-blue-700 text-white border-blue-600", + static: "bg-cyan-700 text-white border-cyan-600", + reference: "bg-gray-600 text-gray-200 border-gray-500", +} + +export function ProgramList({ programs, linkBase, showDeployments, filterable }: ProgramListProps) { const [search, setSearch] = useState("") + // Filter by a *deployment* kind: a program matches if it has a deployment of + // this kind (so a tool-and-job program shows under both Tool and Job). + const [kind, setKind] = useState(null) + + // Count programs per deployment kind (a program counts once toward each kind + // it deploys as). + const counts = useMemo(() => { + const c: Record = {} + for (const p of programs) { + for (const k of new Set(p.deployments.map((d) => d.kind))) { + c[k] = (c[k] ?? 0) + 1 + } + } + return c + }, [programs]) + const kindsPresent = KIND_ORDER.filter((k) => counts[k]) const filtered = useMemo(() => { let base = [...programs].sort((a, b) => a.id.localeCompare(b.id)) + if (kind) base = base.filter((p) => p.deployments.some((d) => d.kind === kind)) if (search) { const q = search.toLowerCase() base = base.filter( @@ -22,17 +53,36 @@ export function ProgramList({ programs, linkBase, showDeployments }: ProgramList ) } return base - }, [programs, search]) + }, [programs, search, kind]) return (
-
+
setSearch(e.target.value)} placeholder="Filter programs..." className="bg-black/30 border border-[var(--border)] rounded px-3 py-1.5 text-sm focus:outline-none focus:border-[var(--primary)] w-56" /> + {filterable && kindsPresent.length > 0 && ( +
+ setKind(null)} + /> + {kindsPresent.map((k) => ( + setKind(kind === k ? null : k)} + /> + ))} +
+ )}
{filtered.length === 0 ? ( @@ -52,3 +102,29 @@ export function ProgramList({ programs, linkBase, showDeployments }: ProgramList
) } + +function Chip({ + label, + active, + activeClass, + onClick, +}: { + label: string + active: boolean + activeClass: string + onClick: () => void +}) { + return ( + + ) +} diff --git a/app/src/pages/Programs.tsx b/app/src/pages/Programs.tsx index 92fa16a..2450311 100644 --- a/app/src/pages/Programs.tsx +++ b/app/src/pages/Programs.tsx @@ -12,7 +12,7 @@ export function Programs() { {isLoading ? (

Loading...

) : programs && programs.length > 0 ? ( - + ) : (

No programs yet.

)}