From 1dbbce1ad853660dbf6ff844da2fa3813c9d9795 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Mon, 27 Jul 2026 18:07:40 -0400 Subject: [PATCH] Extract usePacketDetail and share it across the analyzer and overlay --- src/App.tsx | 21 +++---------- src/features/packets/usePacketDetail.ts | 14 +++++++++ .../features/packets/usePacketDetail.test.tsx | 31 +++++++++++++++++++ 3 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 src/features/packets/usePacketDetail.ts create mode 100644 tests/features/packets/usePacketDetail.test.tsx diff --git a/src/App.tsx b/src/App.tsx index 463e4d3..9c15bc8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback, useRef, lazy, Suspense } from "react"; import { BrowserRouter, useSearchParams } from "react-router-dom"; -import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { RegionProvider, useRegion, useRegionSelection } from "./hooks/useRegion"; import { ALL_REGIONS, @@ -27,7 +27,7 @@ import { RouteTable } from "./features/routes/RouteTable"; import { TraceList } from "./features/traces/TraceList"; import { ChannelList } from "./features/channels/ChannelList"; import { EmptyState } from "./components/EmptyState"; -import { getPacketDetail } from "./api/client"; +import { usePacketDetail } from "./features/packets/usePacketDetail"; import { WsManager } from "./api/ws-manager"; import { WS_URL, ENABLED_TABS } from "./lib/constants"; import type { PacketDetail } from "./types/api"; @@ -145,21 +145,8 @@ function AppInner() { const [pathMapInitialKey, setPathMapInitialKey] = useState(null); const pathLinkHandledRef = useRef(false); - // short staleTime: observations keep accruing, so reopening the analyzer should show them - // instead of a snapshot frozen at first open - const { data: analyzerDetail, isLoading: analyzerLoading } = useQuery({ - queryKey: ["packet-detail", analyzerHash], - queryFn: () => getPacketDetail(analyzerHash!), - enabled: !!analyzerHash, - staleTime: 30_000, - }); - - const { data: overlayPacketDetail, isLoading: overlayPacketLoading } = useQuery({ - queryKey: ["packet-detail", overlayPacketHash], - queryFn: () => getPacketDetail(overlayPacketHash!), - enabled: !!overlayPacketHash, - staleTime: 30_000, - }); + const { data: analyzerDetail, isLoading: analyzerLoading } = usePacketDetail(analyzerHash); + const { data: overlayPacketDetail, isLoading: overlayPacketLoading } = usePacketDetail(overlayPacketHash); // deep link: ?hash opens the analyzer drawer; ?path then opens the path popup once, pre-selected useEffect(() => { diff --git a/src/features/packets/usePacketDetail.ts b/src/features/packets/usePacketDetail.ts new file mode 100644 index 0000000..4e11651 --- /dev/null +++ b/src/features/packets/usePacketDetail.ts @@ -0,0 +1,14 @@ +import { useQuery } from "@tanstack/react-query"; +import { getPacketDetail } from "../../api/client"; +import type { PacketDetail } from "../../types/api"; + +// One query per hash shared by the expanded row, the analyzer drawer and the overlay — TanStack +// dedupes, so a row expanded under an open drawer costs a single request. +export function usePacketDetail(hash: string | null) { + return useQuery({ + queryKey: ["packet-detail", hash], + queryFn: () => getPacketDetail(hash!), + enabled: !!hash, + staleTime: 30_000, + }); +} diff --git a/tests/features/packets/usePacketDetail.test.tsx b/tests/features/packets/usePacketDetail.test.tsx new file mode 100644 index 0000000..808799f --- /dev/null +++ b/tests/features/packets/usePacketDetail.test.tsx @@ -0,0 +1,31 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { usePacketDetail } from "../../../src/features/packets/usePacketDetail"; + +const getPacketDetail = vi.fn(); +vi.mock("../../../src/api/client", () => ({ + getPacketDetail: (hash: string) => getPacketDetail(hash), +})); + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +beforeEach(() => getPacketDetail.mockReset()); + +describe("usePacketDetail", () => { + it("does not fetch when hash is null", () => { + renderHook(() => usePacketDetail(null), { wrapper }); + expect(getPacketDetail).not.toHaveBeenCalled(); + }); + + it("fetches the detail for a hash", async () => { + getPacketDetail.mockResolvedValue({ packetHash: "AA11", observations: [] }); + const { result } = renderHook(() => usePacketDetail("AA11"), { wrapper }); + await waitFor(() => expect(result.current.data?.packetHash).toBe("AA11")); + expect(getPacketDetail).toHaveBeenCalledWith("AA11"); + }); +});