fix(dashboard): gate home topology live-WS networking (#4596) (#4618)

Integrated into release/v3.8.34 (adapted onto #4606's extracted topology section: default-hidden flip + enabled gate on useLiveDashboard)
This commit is contained in:
KooshaPari
2026-06-22 14:33:45 -07:00
committed by GitHub
parent 6287750164
commit 14a3bb451b
4 changed files with 98 additions and 3 deletions

View File

@@ -196,7 +196,9 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
// Appearance settings for home page pinning
const [pinProviderQuotaToHome, setPinProviderQuotaToHome] = useState(false);
const [showQuickStartOnHome, setShowQuickStartOnHome] = useState(true); // default on
const [showProviderTopologyOnHome, setShowProviderTopologyOnHome] = useState(true); // default on
// #4596: default hidden until appearance settings load, so the live-WS
// topology connection is never opened before we know the user wants it.
const [showProviderTopologyOnHome, setShowProviderTopologyOnHome] = useState(false);
const [autoRefreshProviderQuota, setAutoRefreshProviderQuota] = useState(false);
const [autoRefreshProviderQuotaInterval, setAutoRefreshProviderQuotaInterval] = useState(180);
const [appearanceSettingsLoaded, setAppearanceSettingsLoaded] = useState(false);
@@ -1165,6 +1167,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
providers={topologyProviders}
lastProvider={lastProvider}
errorProvider={errorProvider}
enabled={showProviderTopologyOnHome}
/>
)}

View File

@@ -19,13 +19,17 @@ export function HomeProviderTopologySection({
providers,
lastProvider,
errorProvider,
enabled = true,
}: {
providers: TopologyProvider[];
lastProvider: string;
errorProvider: string;
enabled?: boolean;
}) {
const t = useTranslations("home");
const { activeRequests: liveActiveRequests } = useLiveRequests();
// #4596: gate the live-WS connection so it only opens while the topology
// section is actually shown on the home page.
const { activeRequests: liveActiveRequests } = useLiveRequests({ enabled });
return (
<Card>

View File

@@ -50,6 +50,8 @@ export interface DashboardConnectionState {
export interface UseLiveDashboardOptions {
/** WebSocket URL (default: ws://hostname:20129) */
wsUrl?: string;
/** Whether the WebSocket connection should be active (default: true) */
enabled?: boolean;
/** API key for authentication */
apiKey?: string;
/** Channels to subscribe to */
@@ -66,6 +68,7 @@ export interface UseLiveDashboardOptions {
*/
export function useLiveDashboard({
wsUrl = DEFAULT_WS_URL,
enabled = true,
apiKey,
channels = ["requests", "combo", "credentials"],
autoReconnect = true,
@@ -202,6 +205,23 @@ export function useLiveDashboard({
// Connect on mount and on reconnect trigger
useEffect(() => {
mountedRef.current = true;
if (!enabled) {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
wsRef.current?.close();
wsRef.current = null;
setConnection({
isConnected: false,
isConnecting: false,
error: null,
reconnectAttempt: 0,
});
return;
}
connect();
return () => {
mountedRef.current = false;
@@ -210,7 +230,7 @@ export function useLiveDashboard({
}
wsRef.current?.close();
};
}, [connect]);
}, [connect, enabled]);
// Connect (for manual retry)
const reconnect = useCallback(() => {

View File

@@ -0,0 +1,68 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useLiveRequests } from "../../../src/hooks/useLiveDashboard";
function LiveRequestsHarness({ enabled }: { enabled: boolean }) {
useLiveRequests({ enabled });
return null;
}
describe("home topology hidden networking (#4596)", () => {
const websocketMock = vi.fn();
let root: ReturnType<typeof createRoot> | null = null;
let container: HTMLDivElement | null = null;
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
websocketMock.mockClear();
vi.stubGlobal(
"WebSocket",
class WebSocketMock {
static OPEN = 1;
readyState = 0;
onopen: (() => void) | null = null;
onmessage: ((event: MessageEvent) => void) | null = null;
onclose: (() => void) | null = null;
onerror: (() => void) | null = null;
constructor(url: string) {
websocketMock(url);
}
send() {}
close() {}
}
);
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => {
root?.unmount();
});
container?.remove();
root = null;
container = null;
vi.unstubAllGlobals();
});
it("does NOT open a WebSocket when the topology section is disabled", () => {
act(() => {
root!.render(<LiveRequestsHarness enabled={false} />);
});
expect(websocketMock).not.toHaveBeenCalled();
});
it("opens a WebSocket when the topology section is enabled", () => {
act(() => {
root!.render(<LiveRequestsHarness enabled={true} />);
});
expect(websocketMock).toHaveBeenCalledTimes(1);
});
});