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"}