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