import { useQueries } from "@tanstack/react-query"; import { motion } from "framer-motion"; import { ChevronRight, CircleDot } from "lucide-react"; import { useEffect, useMemo, useRef } from "react"; import { scopedConclusionsCountOptions, scopedQueueStatusOptions, useScopedWorkspaces, } from "@/api/compareQueries"; import type { components } from "@/api/schema.d.ts"; import type { FleetRowMetrics } from "@/components/fleet/fleetAggregates"; import { useDemo } from "@/hooks/useDemo"; import type { Instance } from "@/lib/config"; import { COLOR } from "@/lib/constants"; import { formatCount } from "@/lib/utils"; type Workspace = components["schemas"]["Workspace"]; type QueueStatus = components["schemas"]["QueueStatus"]; type ConclusionPage = components["schemas"]["Page_Conclusion_"]; interface Props { instance: Instance; /** Open a workspace's drill-down (activates the instance first if needed). */ onOpenWorkspace: (instance: Instance, workspaceId: string) => void; /** Report this server's summed metrics up for the aggregate header. */ onMetrics: (id: string, metrics: FleetRowMetrics) => void; } const WORKSPACE_PAGE_SIZE = 100; function metricsEqual(a: FleetRowMetrics, b: FleetRowMetrics): boolean { return ( a.workspaceCount === b.workspaceCount && a.conclusionCount === b.conclusionCount && a.queueActive === b.queueActive && a.queuePending === b.queuePending && a.health === b.health ); } /** * Renders one `` per workspace on a single server (instance), labelled * ` ()`, and reports the server's summed metrics up so the * Dashboard header can aggregate across servers. Per-instance data fetching lives * here (not in a parent loop) to satisfy the rules of hooks — one child per server. */ export function ServerWorkspaceRows({ instance, onOpenWorkspace, onMetrics }: Props) { const { mask } = useDemo(); const workspacesQ = useScopedWorkspaces(instance, 1, WORKSPACE_PAGE_SIZE); const workspaces: Workspace[] = useMemo( () => (workspacesQ.data as { items?: Workspace[] } | undefined)?.items ?? [], [workspacesQ.data], ); const totalWorkspaces = (workspacesQ.data as { total?: number } | undefined)?.total ?? workspaces.length; const queueResults = useQueries({ queries: workspaces.map((ws) => scopedQueueStatusOptions(instance, ws.id)), }); const conclusionsResults = useQueries({ queries: workspaces.map((ws) => scopedConclusionsCountOptions(instance, ws.id)), }); const queueActive = queueResults.reduce( (s, q) => s + ((q.data as QueueStatus | undefined)?.in_progress_work_units ?? 0), 0, ); const queuePending = queueResults.reduce( (s, q) => s + ((q.data as QueueStatus | undefined)?.pending_work_units ?? 0), 0, ); const conclusionCount = conclusionsResults.reduce( (s, c) => s + ((c.data as ConclusionPage | undefined)?.total ?? 0), 0, ); const health: FleetRowMetrics["health"] = workspacesQ.isError ? "unreachable" : workspacesQ.isSuccess ? "ok" : "loading"; const metrics: FleetRowMetrics = useMemo( () => ({ workspaceCount: totalWorkspaces, conclusionCount, queueActive, queuePending, lastSeen: null, health, }), [totalWorkspaces, conclusionCount, queueActive, queuePending, health], ); const lastReported = useRef(null); useEffect(() => { if (lastReported.current && metricsEqual(lastReported.current, metrics)) return; lastReported.current = metrics; onMetrics(instance.id, metrics); }, [instance.id, metrics, onMetrics]); if (workspacesQ.isError) { return ( {instance.name} — unreachable ); } if (workspaces.length === 0) { return ( {instance.name} — {workspacesQ.isLoading ? "loading…" : "no workspaces"} ); } return ( <> {workspaces.map((ws, i) => { const queue = queueResults[i]?.data as QueueStatus | undefined; const active = queue?.in_progress_work_units ?? 0; const pending = queue?.pending_work_units ?? 0; const isActive = active > 0 || pending > 0; const conclusions = (conclusionsResults[i]?.data as ConclusionPage | undefined)?.total; return ( {conclusions === undefined ? "—" : formatCount(conclusions)}
{isActive ? ( ) : ( )} {isActive ? `${formatCount(active)}/${formatCount(pending)}` : "idle"}
); })} ); }