diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/page.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.test.tsx index 148a916c867..5a6fd9cc68f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/page.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.test.tsx @@ -18,12 +18,11 @@ vi.mock("./_components/LensView", () => ({ describe("Lens navigation", () => { beforeEach(() => auth.mockReturnValue({ accessToken: "test-token", userRole: "Admin", isViewOnly: false })); - it("opens investigations and pauses trace polling when returning from traces", async () => { + it("opens traces by default and pauses polling while viewing investigations", async () => { const user = userEvent.setup(); const onUrlUpdate = vi.fn(); renderWithProviders(, { onUrlUpdate }); - expect(screen.getByRole("tab", { name: "Investigations" })).toHaveAttribute("aria-selected", "true"); - await user.click(screen.getByRole("tab", { name: "Traces" })); + expect(screen.getByRole("tab", { name: "Traces" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByText("Trace polling active")).toBeVisible(); await user.click(screen.getByRole("tab", { name: "Investigations" })); expect(screen.getByText("Manage investigations")).toBeVisible(); @@ -39,6 +38,12 @@ describe("Lens navigation", () => { expect(screen.getByText("Manage investigations")).toBeVisible(); }); + it("honors an explicit traces tab even when a saved investigation is in the URL", () => { + renderWithProviders(, { searchParams: "?tab=traces&lens=saved-lens" }); + expect(screen.getByRole("tab", { name: "Traces" })).toHaveAttribute("aria-selected", "true"); + expect(screen.getByText("Trace polling active")).toBeVisible(); + }); + it.each(["Internal User", "Internal Viewer", "Org Admin"])( "preserves trace access without granting investigations to %s", async (userRole) => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx index 01f5126e3ae..b29e498600f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx @@ -15,7 +15,7 @@ export default function LensPage() { parseAsStringLiteral(["traces", "investigations"]).withOptions({ history: "push" }), ); const [lensId] = useQueryState("lens", parseAsString); - const defaultTab = isProxyAdminTierRole(userRole ?? "") || lensId ? "investigations" : "traces"; + const defaultTab = lensId ? "investigations" : "traces"; const activeTab = tab ?? defaultTab; if (!accessToken) return null; return ( diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/SpanTree.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/SpanTree.tsx index 27a3d7fbfe0..3f7c798a0a0 100644 --- a/ui/litellm-dashboard/src/components/view_logs/TraceView/SpanTree.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/SpanTree.tsx @@ -1,8 +1,9 @@ "use client"; -import { BarChart3, Clock, Coins, ListTree, MoreHorizontal, Timer } from "lucide-react"; +import { BarChart3, Clock, Coins, ListTree, MoreHorizontal, PanelRightOpen, Timer } from "lucide-react"; import { useEffect, useMemo, useRef } from "react"; +import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/cva.config"; @@ -24,6 +25,7 @@ interface SpanTreeProps { onToggleSpan: (id: string) => void; onToggleGroup: (id: string) => void; onLoadMore: (groupId: string) => void; + onOpenDetails?: () => void; /** Inside the side drawer J/K switch runs, so spans move with the arrow keys. */ embedded?: boolean; } @@ -308,6 +310,7 @@ export function SpanTree({ onToggleSpan, onToggleGroup, onLoadMore, + onOpenDetails, embedded = false, }: SpanTreeProps) { const scrollRef = useRef(null); @@ -333,6 +336,12 @@ export function SpanTree({ aria-label="Hide framework spans" /> + {onOpenDetails && ( + + )}
diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceDrawer.test.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceDrawer.test.tsx index da3fce0d14b..50de602fa31 100644 --- a/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceDrawer.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceDrawer.test.tsx @@ -135,6 +135,21 @@ describe("RunView", () => { expect(screen.queryByTestId("detail-pane")).not.toBeInTheDocument(); }); + it.each(["button", "Escape"])("reopens the selected step after closing details with %s", async (method) => { + const user = userEvent.setup(); + renderRun(research); + await screen.findByTestId("detail-pane"); + await user.keyboard("j"); + const selectedId = screen.getByTestId("detail-pane").getAttribute("data-row-id"); + expect(selectedId).not.toBe(rootSpanId(research)); + if (method === "button") await user.click(screen.getByRole("button", { name: "close detail" })); + else await user.keyboard("{Escape}"); + expect(screen.queryByTestId("detail-pane")).not.toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: "Show details" })); + expect(screen.getByTestId("detail-pane")).toHaveAttribute("data-row-id", selectedId); + expect(screen.queryByRole("button", { name: "Show details" })).not.toBeInTheDocument(); + }); + it("keeps a way back to the runs table when a run fails to load", async () => { const user = userEvent.setup(); const onBack = vi.fn(); diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceDrawer.tsx index 0aeea4e4cc7..a991c771a56 100644 --- a/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceDrawer.tsx @@ -253,6 +253,7 @@ function RunBody({ trace, accessToken, initialSpanId, embedded }: RunBodyProps) onToggleSpan={toggleSpan} onToggleGroup={toggleGroup} onLoadMore={loadMore} + onOpenDetails={detailOpen ? undefined : () => setDetailOpen(true)} embedded={embedded} /> {detailOpen && (