From 3cd22643cc4809396b9fe922f1a445e68957be34 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Sun, 4 Oct 2026 19:26:36 -0700 Subject: [PATCH] fix(ui): keep Lens investigation preview rows in place while matches reload Co-Authored-By: Claude Opus 5.5 --- .../InvestigationSetup.integration.test.tsx | 24 +++++++++++++++++++ .../lens/setup/MatchingActivityPreview.tsx | 8 ++++--- .../lens/setup/useMatchingActivity.ts | 8 +++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/ui/litellm-dashboard/src/components/lens/setup/InvestigationSetup.integration.test.tsx b/ui/litellm-dashboard/src/components/lens/setup/InvestigationSetup.integration.test.tsx index 50bc39cd2c1..19c94bea45a 100644 --- a/ui/litellm-dashboard/src/components/lens/setup/InvestigationSetup.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/lens/setup/InvestigationSetup.integration.test.tsx @@ -430,6 +430,30 @@ it("fetches one preview for two keystrokes inside the debounce window", async () } }); +it("shows skeleton rows on first load and keeps the previous rows while a new search loads", async () => { + const pending = new Map void>(); + proxy.post.mockImplementation( + (_path, options) => + new Promise((resolve) => { + pending.set((options?.body as { selection: Settings }).selection.q, resolve); + }), + ); + const user = userEvent.setup(); + renderWithProviders(); + const preview = within(screen.getByRole("region", { name: "Matching activity" })); + expect((await preview.findAllByTestId("runs-placeholder")).length).toBeGreaterThan(0); + act(() => pending.get("")?.({ eligible: 1, selected: 1, executions: [run("Old run", 1)] })); + expect(await preview.findByText("Old run")).toBeVisible(); + expect(preview.queryByTestId("runs-placeholder")).not.toBeInTheDocument(); + await user.type(screen.getByRole("combobox", { name: "Search runs" }), "new"); + await waitFor(() => expect(pending.has("new")).toBe(true)); + expect(preview.getByText("Old run")).toBeVisible(); + expect(preview.queryByTestId("runs-placeholder")).not.toBeInTheDocument(); + act(() => pending.get("new")?.({ eligible: 1, selected: 1, executions: [run("New run", 2)] })); + expect(await preview.findByText("New run")).toBeVisible(); + expect(preview.queryByText("Old run")).not.toBeInTheDocument(); +}); + it.each(["empty", "error"])("blocks a new investigation when its preview is %s", async (state) => { if (state === "error") proxy.post.mockRejectedValue(new Error("Storage unavailable")); else proxy.post.mockResolvedValue({ eligible: 0, selected: 0, executions: [] }); diff --git a/ui/litellm-dashboard/src/components/lens/setup/MatchingActivityPreview.tsx b/ui/litellm-dashboard/src/components/lens/setup/MatchingActivityPreview.tsx index c5427612e12..490db86cf95 100644 --- a/ui/litellm-dashboard/src/components/lens/setup/MatchingActivityPreview.tsx +++ b/ui/litellm-dashboard/src/components/lens/setup/MatchingActivityPreview.tsx @@ -60,6 +60,7 @@ export function MatchingActivityPreview({ ...props }: MatchingActivityPreviewProps) { const runs = page.executions.flatMap((run) => (run.summary ? [run.summary] : [])); + const shown = status.ready || status.stale; return (
)} - {status.ready && runs.length > 0 && ( + {(status.loading || (shown && !status.error && runs.length > 0)) && (
{}} picks={selection ? runPicks(selection) : undefined} />
)} - {status.ready && } + {shown && }
); } diff --git a/ui/litellm-dashboard/src/components/lens/setup/useMatchingActivity.ts b/ui/litellm-dashboard/src/components/lens/setup/useMatchingActivity.ts index feeb3e2cac8..2ea6e2e1ede 100644 --- a/ui/litellm-dashboard/src/components/lens/setup/useMatchingActivity.ts +++ b/ui/litellm-dashboard/src/components/lens/setup/useMatchingActivity.ts @@ -29,6 +29,10 @@ export interface PreviewStatus { readonly title: string; readonly windowLabel: string; readonly ready: boolean; + /** No results yet for any search: the table shows skeleton rows. */ + readonly loading: boolean; + /** The rows belong to the previous search while this one loads. */ + readonly stale: boolean; readonly error: Error | null; readonly refresh: () => void; } @@ -144,6 +148,8 @@ export function useMatchingActivity(): MatchingActivity { }, [empty, valid, asOf, refresh]); const pending = settling || preview.isLoading || preview.isPlaceholderData; const ready = !pending && valid; + const loading = valid && pending && !firstPage; + const stale = valid && pending && !!firstPage; const setExecutionIds = (next: readonly string[]) => setValue("selection.execution_ids", [...next], { shouldValidate: true }); const picked = manualSelection ? manualPicks(selection, setExecutionIds) : null; @@ -156,6 +162,8 @@ export function useMatchingActivity(): MatchingActivity { title: previewTitle({ pending, validWindow: windowValid, valid }, firstPage), windowLabel: windowLabel(selection), ready, + loading, + stale, error: preview.error, refresh, },