diff --git a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationStates.tsx b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationStates.tsx index aa313cd6e37..ead2e0789c1 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationStates.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationStates.tsx @@ -1,7 +1,8 @@ "use client"; -import { ArrowUpRight, Loader2, SearchX, TriangleAlert } from "lucide-react"; +import { ArrowUpRight, SearchX, TriangleAlert } from "lucide-react"; import { Button, buttonVariants } from "@/components/ui/button"; +import { LoadingState } from "../ui/LoadingState"; import { StateMessage } from "../ui/StateMessage"; import { ApiError } from "@/lib/http/client"; @@ -63,9 +64,7 @@ export function InvestigationError({ message, refresh }: { message: string; refr export function InvestigationsLoading() { return ( - } + diff --git a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.integration.test.tsx b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.integration.test.tsx index 039e884c793..0619df164fa 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.integration.test.tsx @@ -311,6 +311,26 @@ it("runs with saved settings from Run now without opening setup, then accepts an expect(sentBody(proxy.post, "/lens/lens/runs")).toEqual([{ agent_name: "billing", lookback_hours: 24 }]); }); +it("shows a loading state until the investigation list arrives", async () => { + window.history.replaceState({}, "", "/lens/"); + testQueryClient.clear(); + let resolveList: (list: unknown) => void = () => {}; + proxy.get.mockImplementation((path) => { + if (path === "/lens") + return new Promise((resolve) => { + resolveList = resolve; + }); + return Promise.resolve({ traces: true, requests: false, data: [] }); + }); + renderWithProviders(withPreview(, vi.fn())); + expect(await screen.findByRole("status", { name: "Loading investigations…" })).toBeVisible(); + expect(screen.queryByRole("region", { name: "Get Lens running" })).not.toBeInTheDocument(); + + act(() => resolveList({ lenses: [], workers: [], tracing_enabled: true })); + expect(await screen.findByRole("region", { name: "Get Lens running" })).toBeVisible(); + expect(screen.queryByRole("status", { name: "Loading investigations…" })).not.toBeInTheDocument(); +}); + it("offers the interactive demo without starting an investigation", async () => { window.history.replaceState({}, "", "/lens/"); testQueryClient.clear(); diff --git a/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.test.tsx b/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.test.tsx index 9136d5e1cf9..d26d2048a58 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.test.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.test.tsx @@ -144,6 +144,25 @@ describe("RunView", () => { expect(screen.getByLabelText("Keyboard shortcuts")).toHaveTextContent("↑/↓ step←/→ foldEsc close"); }); + it("shows a loading state until the first trace arrives", async () => { + let resolveTrace: (trace: Trace) => void = () => {}; + vi.mocked(agentTraceCall).mockImplementation( + () => + new Promise((resolve) => { + resolveTrace = resolve; + }), + ); + renderWithProviders( + , + ); + expect(screen.getByRole("status", { name: "Loading trace…" })).toBeVisible(); + expect(screen.queryByTestId("run-view")).not.toBeInTheDocument(); + + act(() => resolveTrace(research)); + expect(await screen.findByRole("heading", { level: 1 })).toHaveTextContent(traceDisplayName(research.summary)); + expect(screen.queryByRole("status", { name: "Loading trace…" })).not.toBeInTheDocument(); + }); + it("keeps the current run on screen, inert, while an unvisited run loads in the drawer", async () => { const user = userEvent.setup(); let resolveSwarm: (trace: Trace) => void = () => {}; @@ -161,7 +180,7 @@ describe("RunView", () => { rerender(); await waitFor(() => expect(screen.getByTestId("run-view")).toHaveAttribute("aria-busy", "true")); - expect(screen.queryByRole("status", { name: "Loading trace" })).not.toBeInTheDocument(); + expect(screen.queryByRole("status", { name: "Loading trace…" })).not.toBeInTheDocument(); expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(traceDisplayName(research.summary)); await user.keyboard("{ArrowDown}"); expect(screen.getByTestId("detail-pane")).toHaveAttribute("data-row-id", root); diff --git a/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.tsx b/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.tsx index 3680cf8073d..b20ed28306b 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.tsx @@ -7,10 +7,10 @@ import { ErrorBoundary } from "react-error-boundary"; import { Button } from "@/components/ui/button"; import { Tabs } from "@/components/ui/tabs"; -import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { cn } from "@/lib/cva.config"; import { useTracesApi } from "../../api"; +import { LoadingState } from "../../../ui/LoadingState"; import { classifyTraceReadFailure, traceReadRetry, traceReadRetryDelay } from "../../list/traceReadFailure"; import { type RunSelection, traceKey } from "../../routing"; import type { Trace } from "../../types"; @@ -32,22 +32,8 @@ function selectedSpanMissing(trace: Trace, spanId: string | null): boolean { return Boolean(spanId && !trace.spans.some((span) => span.span_id === spanId)); } -function RunLoading({ embedded }: { embedded: boolean }) { - return ( -
- {embedded ? ( - [72, 48, 88, 60, 80].map((w) => ( -
- )) - ) : ( - - )} -
- ); +function RunLoading() { + return ; } function RunLoadError({ error, onBack, onRetry }: { error: unknown; onBack: () => void; onRetry: () => void }) { @@ -85,7 +71,7 @@ export function RunView(props: RunViewProps) { )} > - }> + }> diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx index dff968334f7..946244ab1bb 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx @@ -51,6 +51,26 @@ describe("AgentTracesTable empty state", () => { }); }); +describe("AgentTracesTable loading state", () => { + it("announces the first page load instead of an empty table", () => { + render( + inList( + , + ), + ); + expect(screen.getByRole("status", { name: "Loading runs…" })).toBeVisible(); + expect(screen.queryByText(/No runs/)).not.toBeInTheDocument(); + }); +}); + describe("AgentTracesTable virtualization", () => { const template = (traceList as TracePage).data[0] as TraceSummary; const manyRuns: TraceSummary[] = Array.from({ length: 500 }, (_, i) => ({ diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx index 8ba8bdfa2e7..fd0aeb0d350 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx @@ -12,6 +12,7 @@ import { formatActivityTimestamp, formatRunTimestamp, localTimeZoneAbbreviation import { SpanIcon } from "../ui/SpanIcon"; import { StatusMark } from "../ui/StatusMark"; import { FrameworkLogo, traceFramework } from "../ui/TraceFramework"; +import { LoadingState } from "../../ui/LoadingState"; import type { TraceSummary } from "../types"; import { traceRefOf } from "../routing"; import { fmtMs, previewText, traceDisplayName, traceAgentNames } from "../utils"; @@ -215,7 +216,7 @@ export function AgentTracesTable({ }; const table = useReactTable(tableOptions); return ( - + @@ -232,7 +233,7 @@ export function AgentTracesTable({ )} - {isLoading &&
Loading runs…
} + {isLoading && } {error && (
diff --git a/ui/litellm-dashboard/src/components/lens/ui/LoadingState.tsx b/ui/litellm-dashboard/src/components/lens/ui/LoadingState.tsx new file mode 100644 index 00000000000..e4881602ca9 --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/ui/LoadingState.tsx @@ -0,0 +1,17 @@ +import { Loader2 } from "lucide-react"; + +import { StateMessage, type StateMessageProps } from "./StateMessage"; + +type LoadingStateProps = Omit; + +export function LoadingState({ title, ...props }: LoadingStateProps) { + return ( + } + title={title} + {...props} + /> + ); +}