From 3b88a41afda842eb3d493b7d77a825595164cb94 Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Wed, 3 Jun 2026 17:47:59 -0500 Subject: [PATCH] fix(dashboard): guard setMetricsById against same-value calls to end loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Even with primitive useEffect deps, TanStack Query or React concurrent rendering can cause onMetrics to fire with identical values mid-render-cycle. Add a ref-based equality check in Dashboard.onMetrics: if all five metric values are unchanged, skip setMetricsById entirely — no state update, no Dashboard re-render, loop terminates. Also fixes vi.fn() typing in server-workspace-rows.test.tsx to satisfy tsc (Vitest 4 single-type-arg signature). --- .../web/src/components/dashboard/Dashboard.tsx | 14 +++++++++++++- .../web/src/test/server-workspace-rows.test.tsx | 9 +++++---- 2 files changed, 18 insertions(+), 5 deletions(-) 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)