diff --git a/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.test.tsx b/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.test.tsx
index 1321a4a152a..4a0fea12128 100644
--- a/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.test.tsx
+++ b/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.test.tsx
@@ -1,9 +1,11 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
-import { describe, expect, it, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { LogDetailsDrawer } from "./LogDetailsDrawer";
import { sessionSpendLogsCall } from "../../networking";
import { LogEntry } from "../types";
+import { toast } from "@/lib/toast";
+import * as localStorageUtils from "@/utils/localStorageUtils";
import { AutoRouterModelGroupsProvider } from "@/components/shared/table_cells";
vi.mock("../../networking", () => ({
@@ -83,19 +85,78 @@ const sessionLogs = [
const renderSessionDrawer = () => {
vi.mocked(sessionSpendLogsCall).mockResolvedValue({ data: sessionLogs, total: 4, total_pages: 1 });
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
- const drawer = (open: boolean) => (
+ const drawer = (open: boolean, sessionId = "session-1") => (
- {}} logEntry={null} sessionId="session-1" accessToken="token" />
+ {}} logEntry={null} sessionId={sessionId} accessToken="token" />
);
- const { rerender } = render(drawer(true));
- return { rerender, drawer };
+ const { rerender, unmount } = render(drawer(true));
+ return { rerender, drawer, unmount };
};
const sidebarEventNames = () =>
screen.queryAllByText(/^(llm-early|llm-late|tool-early|tool-late)$/).map((el) => el.textContent);
describe("LogDetailsDrawer session sidebar sorting", () => {
+ beforeEach(() => localStorage.removeItem("litellm_session_logs_default_sort"));
+ afterEach(() => vi.restoreAllMocks());
+
+ it("saves a default sort for reopened sessions and a fresh page mount", async () => {
+ const { rerender, drawer, unmount } = renderSessionDrawer();
+ await screen.findByText("llm-early");
+ fireEvent.click(screen.getByRole("tab", { name: "Start time" }));
+ fireEvent.click(screen.getByRole("button", { name: "Set as default" }));
+ expect(screen.getByRole("button", { name: "Default in this browser" })).toBeDisabled();
+
+ rerender(drawer(false));
+ rerender(drawer(true));
+ await waitFor(() => expect(sidebarEventNames()).toEqual(["llm-early", "tool-early", "llm-late", "tool-late"]));
+ unmount();
+ renderSessionDrawer();
+ await waitFor(() => expect(sidebarEventNames()).toEqual(["llm-early", "tool-early", "llm-late", "tool-late"]));
+ });
+
+ it("keeps a temporary sort separate from the default for the next session", async () => {
+ localStorage.setItem("litellm_session_logs_default_sort", "start_time");
+ const { rerender, drawer } = renderSessionDrawer();
+ await waitFor(() => expect(sidebarEventNames()).toEqual(["llm-early", "tool-early", "llm-late", "tool-late"]));
+ fireEvent.click(screen.getByRole("tab", { name: "Duration" }));
+ expect(sidebarEventNames()).toEqual(["tool-early", "llm-late", "llm-early", "tool-late"]);
+ rerender(drawer(true, "session-2"));
+ await waitFor(() => expect(sidebarEventNames()).toEqual(["llm-early", "tool-early", "llm-late", "tool-late"]));
+ });
+
+ it("lets duration become the saved default again", async () => {
+ localStorage.setItem("litellm_session_logs_default_sort", "start_time");
+ const { unmount } = renderSessionDrawer();
+ await screen.findByText("llm-early");
+ fireEvent.click(screen.getByRole("tab", { name: "Duration" }));
+ fireEvent.click(screen.getByRole("button", { name: "Set as default" }));
+ unmount();
+ renderSessionDrawer();
+ await waitFor(() => expect(sidebarEventNames()).toEqual(["tool-early", "llm-late", "llm-early", "tool-late"]));
+ });
+
+ it("falls back to duration when the stored preference is invalid", async () => {
+ localStorage.setItem("litellm_session_logs_default_sort", "unsupported-mode");
+ renderSessionDrawer();
+ await waitFor(() => expect(sidebarEventNames()).toEqual(["tool-early", "llm-late", "llm-early", "tool-late"]));
+ expect(screen.getByRole("tab", { name: "Duration" })).toHaveAttribute("aria-selected", "true");
+ });
+
+ it("reports a failed save without pretending the default changed", async () => {
+ vi.spyOn(localStorageUtils, "setLocalStorageItem").mockImplementation(() => {});
+ const { rerender, drawer } = renderSessionDrawer();
+ await screen.findByText("llm-early");
+ fireEvent.click(screen.getByRole("tab", { name: "Start time" }));
+ fireEvent.click(screen.getByRole("button", { name: "Set as default" }));
+ expect(toast.error).toHaveBeenCalledWith("Couldn't save the default sort in this browser");
+ expect(screen.getByRole("button", { name: "Set as default" })).toBeEnabled();
+ rerender(drawer(false));
+ rerender(drawer(true));
+ await waitFor(() => expect(sidebarEventNames()).toEqual(["tool-early", "llm-late", "llm-early", "tool-late"]));
+ });
+
it("defaults to duration order, longest call first across LLM and MCP calls", async () => {
renderSessionDrawer();
await waitFor(() => expect(sidebarEventNames()).toHaveLength(4));
diff --git a/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.tsx
index 48b89ee8ac1..8215a95da76 100644
--- a/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.tsx
+++ b/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.tsx
@@ -2,6 +2,9 @@ import { useEffect, useMemo, useState } from "react";
import { Bot, Check, Copy, Sparkles, Wrench } from "lucide-react";
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { toast } from "@/lib/toast";
+import { getLocalStorageItem, setLocalStorageItem } from "@/utils/localStorageUtils";
import { LogEntry } from "../types";
import { AutoRouterIcon, useIsAutoRoutedModelGroup } from "@/components/shared/table_cells";
import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "../constants";
@@ -32,6 +35,7 @@ export interface LogDetailsDrawerProps {
}
const SIDEBAR_WIDTH_PX = 224;
+const SESSION_SORT_PREFERENCE_KEY = "litellm_session_logs_default_sort";
// Session logs are fetched page-by-page from the paginated backend and
// accumulated so the drawer can show the whole session. page_size is the
@@ -125,6 +129,7 @@ export function LogDetailsDrawer({
const isSessionMode = Boolean(sessionId);
const [selectedSessionRequestId, setSelectedSessionRequestId] = useState(null);
const [sessionSortMode, setSessionSortMode] = useState("duration");
+ const [defaultSessionSortMode, setDefaultSessionSortMode] = useState("duration");
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [copiedLeftPanelId, setCopiedLeftPanelId] = useState(false);
@@ -222,12 +227,26 @@ export function LogDetailsDrawer({
useEffect(() => {
if (open) {
setIsSidebarCollapsed(false);
+ if (isSessionMode) {
+ const storedMode = getLocalStorageItem(SESSION_SORT_PREFERENCE_KEY);
+ const defaultMode = storedMode === "start_time" ? "start_time" : "duration";
+ setDefaultSessionSortMode(defaultMode);
+ setSessionSortMode(defaultMode);
+ }
} else {
if (isSessionMode) setSelectedSessionRequestId(null);
- setSessionSortMode("duration");
setCopiedLeftPanelId(false);
}
- }, [open, isSessionMode]);
+ }, [open, isSessionMode, sessionId]);
+
+ const saveDefaultSessionSort = () => {
+ setLocalStorageItem(SESSION_SORT_PREFERENCE_KEY, sessionSortMode);
+ if (getLocalStorageItem(SESSION_SORT_PREFERENCE_KEY) !== sessionSortMode) {
+ toast.error("Couldn't save the default sort in this browser");
+ return;
+ }
+ setDefaultSessionSortMode(sessionSortMode);
+ };
// Keyboard navigation
const { selectNextLog, selectPreviousLog } = useKeyboardNavigation({
@@ -402,6 +421,16 @@ export function LogDetailsDrawer({
Start time
+
)}