feat(ui): save default session log sort preference

This commit is contained in:
haiyangbg 2026-10-19 15:32:01 +08:00
parent 581b7d1a02
commit 3eee2ac4da
2 changed files with 97 additions and 7 deletions

View file

@ -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") => (
<QueryClientProvider client={queryClient}>
<LogDetailsDrawer open={open} onClose={() => {}} logEntry={null} sessionId="session-1" accessToken="token" />
<LogDetailsDrawer open={open} onClose={() => {}} logEntry={null} sessionId={sessionId} accessToken="token" />
</QueryClientProvider>
);
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));

View file

@ -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<string | null>(null);
const [sessionSortMode, setSessionSortMode] = useState<SessionLogSortMode>("duration");
const [defaultSessionSortMode, setDefaultSessionSortMode] = useState<SessionLogSortMode>("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
</TabsTrigger>
</TabsList>
<Button
variant="ghost"
size="xs"
className="mt-1 w-full text-[11px]"
disabled={sessionSortMode === defaultSessionSortMode}
onClick={saveDefaultSessionSort}
title="Use this order when opening sessions in this browser"
>
{sessionSortMode === defaultSessionSortMode ? "Default in this browser" : "Set as default"}
</Button>
</Tabs>
)}
</div>