fix(dashboard): guard setMetricsById against same-value calls to end loop
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<TFunction>() typing in server-workspace-rows.test.tsx to satisfy tsc (Vitest 4 single-type-arg signature).
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { Link, useNavigate } from "@tanstack/react-router";
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Boxes, LayoutDashboard, Network, Settings as SettingsIcon } from "lucide-react";
|
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 {
|
import {
|
||||||
computeFleetAggregates,
|
computeFleetAggregates,
|
||||||
DEFAULT_ROW_METRICS,
|
DEFAULT_ROW_METRICS,
|
||||||
@@ -28,6 +28,7 @@ export function Dashboard() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [serverFilter, setServerFilter] = useState<string>(ALL_SERVERS);
|
const [serverFilter, setServerFilter] = useState<string>(ALL_SERVERS);
|
||||||
const [metricsById, setMetricsById] = useState<Record<string, FleetRowMetrics>>({});
|
const [metricsById, setMetricsById] = useState<Record<string, FleetRowMetrics>>({});
|
||||||
|
const lastMetrics = useRef<Record<string, FleetRowMetrics>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (serverFilter !== ALL_SERVERS && !instances.find((i) => i.id === serverFilter)) {
|
if (serverFilter !== ALL_SERVERS && !instances.find((i) => i.id === serverFilter)) {
|
||||||
@@ -36,6 +37,17 @@ export function Dashboard() {
|
|||||||
}, [instances, serverFilter]);
|
}, [instances, serverFilter]);
|
||||||
|
|
||||||
const onMetrics = useCallback((id: string, m: FleetRowMetrics) => {
|
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 }));
|
setMetricsById((prev) => ({ ...prev, [id]: m }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|||||||
import { act, render, waitFor } from "@testing-library/react";
|
import { act, render, waitFor } from "@testing-library/react";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { ServerWorkspaceRows } from "@/components/dashboard/ServerWorkspaceRows";
|
import { ServerWorkspaceRows } from "@/components/dashboard/ServerWorkspaceRows";
|
||||||
|
import type { FleetRowMetrics } from "@/components/fleet/fleetAggregates";
|
||||||
import { DemoProvider } from "@/context/DemoContext";
|
import { DemoProvider } from "@/context/DemoContext";
|
||||||
import type { Instance } from "@/lib/config";
|
import type { Instance } from "@/lib/config";
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ function makeQc() {
|
|||||||
return new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } });
|
return new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRows(instance: Instance, onMetrics: ReturnType<typeof vi.fn>) {
|
function renderRows(instance: Instance, onMetrics: (id: string, metrics: FleetRowMetrics) => void) {
|
||||||
const qc = makeQc();
|
const qc = makeQc();
|
||||||
return render(
|
return render(
|
||||||
<QueryClientProvider client={qc}>
|
<QueryClientProvider client={qc}>
|
||||||
@@ -52,7 +53,7 @@ describe("ServerWorkspaceRows — onMetrics stability", () => {
|
|||||||
afterEach(() => localStorage.clear());
|
afterEach(() => localStorage.clear());
|
||||||
|
|
||||||
it("calls onMetrics with health:ok after data loads", async () => {
|
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);
|
renderRows(neo, onMetrics);
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(onMetrics).toHaveBeenCalledWith(
|
expect(onMetrics).toHaveBeenCalledWith(
|
||||||
@@ -63,7 +64,7 @@ describe("ServerWorkspaceRows — onMetrics stability", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not call onMetrics again when values have not changed", async () => {
|
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);
|
renderRows(neo, onMetrics);
|
||||||
|
|
||||||
// Wait until we have at least one call with stable state
|
// 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 () => {
|
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);
|
renderRows(neo, onMetrics);
|
||||||
|
|
||||||
// Must eventually report ok (not just loading)
|
// Must eventually report ok (not just loading)
|
||||||
|
|||||||
Reference in New Issue
Block a user