From ab10ca74fe08b5f4b4c7d41a1191b0339c52e524 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Tue, 30 Jun 2026 23:02:33 -0700 Subject: [PATCH] Move Gateway up in nav; render jobs and programs as cards Reorder the sidebar so Gateway sits right under Overview. Replace the Scheduled and Program tables with card grids matching Services: add JobCard and ProgramCard, convert ScheduledSection to a JobCard grid, and turn the former ProgramTable into a filterable ProgramCard grid (renamed ProgramList; kept the search box, dropped the sortable table). Remove the now-orphaned SortHeader component. --- app/src/components/JobCard.tsx | 96 +++++++++++++++ app/src/components/Layout.tsx | 2 +- app/src/components/ProgramCard.tsx | 42 +++++++ app/src/components/ProgramList.tsx | 45 +++++++ app/src/components/ProgramTable.tsx | 157 ------------------------ app/src/components/ScheduledSection.tsx | 97 ++------------- app/src/components/SortHeader.tsx | 48 -------- app/src/pages/Programs.tsx | 4 +- 8 files changed, 193 insertions(+), 298 deletions(-) create mode 100644 app/src/components/JobCard.tsx create mode 100644 app/src/components/ProgramCard.tsx create mode 100644 app/src/components/ProgramList.tsx delete mode 100644 app/src/components/ProgramTable.tsx delete mode 100644 app/src/components/SortHeader.tsx diff --git a/app/src/components/JobCard.tsx b/app/src/components/JobCard.tsx new file mode 100644 index 0000000..ee67cd4 --- /dev/null +++ b/app/src/components/JobCard.tsx @@ -0,0 +1,96 @@ +import { Clock, Play, RefreshCw, Square, Terminal } from "lucide-react" +import { Link } from "react-router-dom" +import type { JobSummary, HealthStatus } from "@/types" +import { useServiceAction } from "@/services/api/hooks" +import { runnerLabel } from "@/lib/labels" +import { StackBadge } from "./StackBadge" + +interface JobCardProps { + job: JobSummary + health?: HealthStatus +} + +export function JobCard({ job, health }: JobCardProps) { + const { mutate, isPending } = useServiceAction() + const isDown = health?.status === "down" + + const doAction = (action: string) => { + mutate({ name: job.id, action }) + } + + return ( +
+
+ + {job.id} + + {health && ( + + {health.status} + + )} +
+ +
+ +
+ + {job.description && ( +

{job.description}

+ )} + +
+
+ {job.schedule && ( + + + {job.schedule} + + )} + {job.runner && ( + + + {runnerLabel(job.runner)} + + )} +
+ + {job.managed && ( +
+ {isDown && ( + + )} + + {!isDown && ( + + )} +
+ )} +
+
+ ) +} diff --git a/app/src/components/Layout.tsx b/app/src/components/Layout.tsx index f37a83e..ebc97bd 100644 --- a/app/src/components/Layout.tsx +++ b/app/src/components/Layout.tsx @@ -17,10 +17,10 @@ import { useEventStream } from "@/services/api/hooks" const NAV = [ { to: "/", label: "Overview", icon: LayoutDashboard, end: true }, + { to: "/gateway", label: "Gateway", icon: Globe, end: false }, { to: "/services", label: "Services", icon: Server, end: false }, { to: "/scheduled", label: "Scheduled", icon: Clock, end: false }, { to: "/programs", label: "Programs", icon: Package, end: false }, - { to: "/gateway", label: "Gateway", icon: Globe, end: false }, { to: "/mesh", label: "Mesh", icon: Share2, end: false }, ] diff --git a/app/src/components/ProgramCard.tsx b/app/src/components/ProgramCard.tsx new file mode 100644 index 0000000..8ca64cf --- /dev/null +++ b/app/src/components/ProgramCard.tsx @@ -0,0 +1,42 @@ +import { Link } from "react-router-dom" +import type { ProgramSummary } from "@/types" +import { BehaviorBadge } from "./BehaviorBadge" +import { StackBadge } from "./StackBadge" +import { ProgramActions } from "./ProgramActions" + +interface ProgramCardProps { + program: ProgramSummary +} + +export function ProgramCard({ program }: ProgramCardProps) { + return ( +
+
+ + {program.id} + +
+ +
+ + +
+ + {program.description && ( +

{program.description}

+ )} + + +
+ ) +} diff --git a/app/src/components/ProgramList.tsx b/app/src/components/ProgramList.tsx new file mode 100644 index 0000000..81fbbdb --- /dev/null +++ b/app/src/components/ProgramList.tsx @@ -0,0 +1,45 @@ +import { useMemo, useState } from "react" +import type { ProgramSummary } from "@/types" +import { ProgramCard } from "./ProgramCard" + +interface ProgramListProps { + programs: ProgramSummary[] +} + +export function ProgramList({ programs }: ProgramListProps) { + const [search, setSearch] = useState("") + + const filtered = useMemo(() => { + const base = [...programs].sort((a, b) => a.id.localeCompare(b.id)) + if (!search) return base + const q = search.toLowerCase() + return base.filter( + (c) => + c.id.toLowerCase().includes(q) || + (c.description?.toLowerCase().includes(q) ?? false), + ) + }, [programs, search]) + + 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" + /> +
+ + {filtered.length === 0 ? ( +

No programs match.

+ ) : ( +
+ {filtered.map((program) => ( + + ))} +
+ )} +
+ ) +} diff --git a/app/src/components/ProgramTable.tsx b/app/src/components/ProgramTable.tsx deleted file mode 100644 index 8e3b0c9..0000000 --- a/app/src/components/ProgramTable.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { useMemo, useState } from "react" -import { Link } from "react-router-dom" -import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react" -import type { ProgramSummary } from "@/types" -import { BehaviorBadge } from "./BehaviorBadge" -import { StackBadge } from "./StackBadge" -import { ProgramActions } from "./ProgramActions" - -interface ProgramTableProps { - programs: ProgramSummary[] -} - -type SortKey = "id" | "stack" | "behavior" -type SortDir = "asc" | "desc" - -export function ProgramTable({ programs }: ProgramTableProps) { - const [search, setSearch] = useState("") - const [sortKey, setSortKey] = useState("id") - const [sortDir, setSortDir] = useState("asc") - - const toggleSort = (key: SortKey) => { - if (sortKey === key) { - setSortDir((d) => (d === "asc" ? "desc" : "asc")) - } else { - setSortKey(key) - setSortDir("asc") - } - } - - const filtered = useMemo(() => { - if (!search) return programs - const q = search.toLowerCase() - return programs.filter( - (c) => - c.id.toLowerCase().includes(q) || - (c.description?.toLowerCase().includes(q) ?? false), - ) - }, [programs, search]) - - const sorted = useMemo(() => { - const dir = sortDir === "asc" ? 1 : -1 - return [...filtered].sort((a, b) => { - switch (sortKey) { - case "id": - return dir * a.id.localeCompare(b.id) - case "stack": - return dir * (a.stack ?? "").localeCompare(b.stack ?? "") - case "behavior": - return dir * (a.behavior ?? "").localeCompare(b.behavior ?? "") - default: - return 0 - } - }) - }, [filtered, sortKey, sortDir]) - - 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" - /> -
- -
- - - - - - - - - - - {sorted.map((comp) => ( - - ))} - {sorted.length === 0 && ( - - - - )} - -
Actions
- No programs match. -
-
-
- ) -} - -function SortHeader({ - label, - sortKey, - current, - dir, - onSort, -}: { - label: string - sortKey: SortKey - current: SortKey - dir: SortDir - onSort: (key: SortKey) => void -}) { - const active = current === sortKey - const Icon = active ? (dir === "asc" ? ArrowUp : ArrowDown) : ArrowUpDown - return ( - - - - ) -} - -function ProgramRow({ program }: { program: ProgramSummary }) { - return ( - - - - {program.id} - - {program.description && ( -

- {program.description} -

- )} - - - - - - - - - - - - ) -} diff --git a/app/src/components/ScheduledSection.tsx b/app/src/components/ScheduledSection.tsx index 89ae043..901316e 100644 --- a/app/src/components/ScheduledSection.tsx +++ b/app/src/components/ScheduledSection.tsx @@ -1,8 +1,6 @@ -import { Link } from "react-router-dom" -import { Play, RefreshCw, Square } from "lucide-react" +import { useMemo } from "react" import type { JobSummary, HealthStatus } from "@/types" -import { useServiceAction } from "@/services/api/hooks" -import { StackBadge } from "./StackBadge" +import { JobCard } from "./JobCard" interface ScheduledSectionProps { jobs: JobSummary[] @@ -10,94 +8,13 @@ interface ScheduledSectionProps { } export function ScheduledSection({ jobs, statuses }: ScheduledSectionProps) { - const statusMap = new Map(statuses.map((s) => [s.id, s])) + const statusMap = useMemo(() => new Map(statuses.map((s) => [s.id, s])), [statuses]) return ( -
- - - - - - - - - - - - {jobs.map((job) => ( - - ))} - -
NameScheduleStackStatusActions
+
+ {jobs.map((job) => ( + + ))}
) } - -function ScheduledRow({ job, health }: { job: JobSummary; health?: HealthStatus }) { - const { mutate, isPending } = useServiceAction() - const isDown = health?.status === "down" - - return ( - - - - {job.id} - - {job.description && ( -

- {job.description} -

- )} - - {job.schedule} - - - - - {health ? ( - - {health.status} - - ) : ( - - )} - - -
- {isDown && ( - - )} - - {!isDown && ( - - )} -
- - - ) -} diff --git a/app/src/components/SortHeader.tsx b/app/src/components/SortHeader.tsx deleted file mode 100644 index abebcc9..0000000 --- a/app/src/components/SortHeader.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useState } from "react" -import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react" - -export type SortDir = "asc" | "desc" - -export function SortHeader({ - label, - sortKey, - current, - dir, - onSort, -}: { - label: string - sortKey: K - current: K - dir: SortDir - onSort: (key: K) => void -}) { - const active = current === sortKey - const Icon = active ? (dir === "asc" ? ArrowUp : ArrowDown) : ArrowUpDown - return ( - - - - ) -} - -export function useSort(defaultKey: K, defaultDir: SortDir = "asc") { - const [sortKey, setSortKey] = useState(defaultKey) - const [sortDir, setSortDir] = useState(defaultDir) - - const toggleSort = (key: K) => { - if (sortKey === key) { - setSortDir((d) => (d === "asc" ? "desc" : "asc")) - } else { - setSortKey(key) - setSortDir("asc") - } - } - - return { sortKey, sortDir, toggleSort } as const -} diff --git a/app/src/pages/Programs.tsx b/app/src/pages/Programs.tsx index 8e8f697..92fa16a 100644 --- a/app/src/pages/Programs.tsx +++ b/app/src/pages/Programs.tsx @@ -1,5 +1,5 @@ import { usePrograms } from "@/services/api/hooks" -import { ProgramTable } from "@/components/ProgramTable" +import { ProgramList } from "@/components/ProgramList" import { PageHeader } from "@/components/PageHeader" export function Programs() { @@ -12,7 +12,7 @@ export function Programs() { {isLoading ? (

Loading...

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

No programs yet.

)}