import { useQueries } from "@tanstack/react-query"; import { motion } from "framer-motion"; import { ChevronRight, CircleDot } from "lucide-react"; import { useEffect, useMemo } 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; /** * 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"; // Dep array uses primitives only — an object dep (e.g. the metrics shape) would // create a new reference on each render even when values are unchanged, causing // onMetrics → setMetricsById → re-render → new object → onMetrics … loop. useEffect(() => { onMetrics(instance.id, { workspaceCount: totalWorkspaces, conclusionCount, queueActive, queuePending, lastSeen: null, health, }); }, [instance.id, totalWorkspaces, conclusionCount, queueActive, queuePending, health, 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"}
); })} ); }