diff --git a/packages/web/src/components/dashboard/Dashboard.tsx b/packages/web/src/components/dashboard/Dashboard.tsx index b4d9bed..74f3a0d 100644 --- a/packages/web/src/components/dashboard/Dashboard.tsx +++ b/packages/web/src/components/dashboard/Dashboard.tsx @@ -1,7 +1,7 @@ import { Link, useNavigate } from "@tanstack/react-router"; import { motion } from "framer-motion"; import { Boxes, LayoutDashboard, Network, Settings as SettingsIcon } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { computeFleetAggregates, DEFAULT_ROW_METRICS, @@ -28,6 +28,7 @@ export function Dashboard() { const navigate = useNavigate(); const [serverFilter, setServerFilter] = useState(ALL_SERVERS); const [metricsById, setMetricsById] = useState>({}); + const lastMetrics = useRef>({}); useEffect(() => { if (serverFilter !== ALL_SERVERS && !instances.find((i) => i.id === serverFilter)) { @@ -36,6 +37,17 @@ export function Dashboard() { }, [instances, serverFilter]); const onMetrics = useCallback((id: string, m: FleetRowMetrics) => { + const prev = lastMetrics.current[id]; + if ( + prev && + prev.workspaceCount === m.workspaceCount && + prev.conclusionCount === m.conclusionCount && + prev.queueActive === m.queueActive && + prev.queuePending === m.queuePending && + prev.health === m.health + ) + return; + lastMetrics.current = { ...lastMetrics.current, [id]: m }; setMetricsById((prev) => ({ ...prev, [id]: m })); }, []); diff --git a/packages/web/src/test/server-workspace-rows.test.tsx b/packages/web/src/test/server-workspace-rows.test.tsx index a131d02..11b0f83 100644 --- a/packages/web/src/test/server-workspace-rows.test.tsx +++ b/packages/web/src/test/server-workspace-rows.test.tsx @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, render, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { ServerWorkspaceRows } from "@/components/dashboard/ServerWorkspaceRows"; +import type { FleetRowMetrics } from "@/components/fleet/fleetAggregates"; import { DemoProvider } from "@/context/DemoContext"; import type { Instance } from "@/lib/config"; @@ -29,7 +30,7 @@ function makeQc() { return new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } }); } -function renderRows(instance: Instance, onMetrics: ReturnType) { +function renderRows(instance: Instance, onMetrics: (id: string, metrics: FleetRowMetrics) => void) { const qc = makeQc(); return render( @@ -52,7 +53,7 @@ describe("ServerWorkspaceRows — onMetrics stability", () => { afterEach(() => localStorage.clear()); it("calls onMetrics with health:ok after data loads", async () => { - const onMetrics = vi.fn(); + const onMetrics = vi.fn<(id: string, m: FleetRowMetrics) => void>(); renderRows(neo, onMetrics); await waitFor(() => expect(onMetrics).toHaveBeenCalledWith( @@ -63,7 +64,7 @@ describe("ServerWorkspaceRows — onMetrics stability", () => { }); it("does not call onMetrics again when values have not changed", async () => { - const onMetrics = vi.fn(); + const onMetrics = vi.fn<(id: string, m: FleetRowMetrics) => void>(); renderRows(neo, onMetrics); // Wait until we have at least one call with stable state @@ -83,7 +84,7 @@ describe("ServerWorkspaceRows — onMetrics stability", () => { }); it("calls onMetrics when health transitions from loading to ok", async () => { - const onMetrics = vi.fn(); + const onMetrics = vi.fn<(id: string, m: FleetRowMetrics) => void>(); renderRows(neo, onMetrics); // Must eventually report ok (not just loading)