feat(ui): make the audit log detail drawer wider and resizable (#42808)

* feat(ui): make the audit log drawer wider and resizable

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): scope drawer width classes to the sheet side variant

The base SheetContent variant data-[side=right]:sm:max-w-sm beats a plain
sm:max-w-none: the compound data+sm variant sorts later in the Tailwind v4
output and twMerge does not treat them as conflicting, so the sheet stays
capped at max-w-sm. That is also why the old w-[60%] sm:max-w-none on main
rendered at 384px. Expressing every width class under the same
data-[side=right] variant chain lets twMerge dedupe and makes CSS order
deterministic. Also removes the drag listeners on unmount mid-drag.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): keyboard resizing and re-grab guard for the resizable drawer

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(ui): query the sheet by dialog role instead of document.querySelector

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): keep drawer resize controls pinned and honor the 720px floor

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): announce the rendered drawer width when the 720px floor overrides the stored percent

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: mrinal <mrinal@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-09-24 21:10:50 -04:00 • committed by GitHub
parent a76f23ac4f
commit 67d7ac58cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 327 additions and 3 deletions

View file

@ -0,0 +1,103 @@
import { describe, it, expect, beforeEach } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { Sheet, SheetTitle } from "@/components/ui/sheet";
import { ResizableSheetContent } from "./ResizableSheetContent";
function sheetContent(): HTMLElement {
return screen.getByRole("dialog");
}
describe("ResizableSheetContent", () => {
beforeEach(() => {
localStorage.clear();
Object.defineProperty(window, "innerWidth", { value: 2000, configurable: true, writable: true });
});
it("resizes by dragging the handle, clamps to the minimum, and persists on release", () => {
render(
<Sheet open>
<ResizableSheetContent storageKey="k">
<SheetTitle>t</SheetTitle>
</ResizableSheetContent>
</Sheet>,
);
fireEvent.pointerDown(screen.getByRole("separator"));
fireEvent.pointerMove(window, { clientX: window.innerWidth * 0.5 });
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("50%");
fireEvent.pointerMove(window, { clientX: window.innerWidth * 0.9 });
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("40%");
fireEvent.pointerUp(window);
expect(localStorage.getItem("k")).toBe("40");
});
it("resizes from the keyboard and exposes the width via aria-valuenow", () => {
render(
<Sheet open>
<ResizableSheetContent storageKey="k">
<SheetTitle>t</SheetTitle>
</ResizableSheetContent>
</Sheet>,
);
const sep = screen.getByRole("separator");
sep.focus();
fireEvent.keyDown(sep, { key: "ArrowLeft" });
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("80%");
expect(localStorage.getItem("k")).toBe("80");
fireEvent.keyDown(sep, { key: "ArrowRight" });
fireEvent.keyDown(sep, { key: "ArrowRight" });
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("70%");
fireEvent.keyDown(sep, { key: "End" });
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("40%");
expect(sep).toHaveAttribute("aria-valuenow", "40");
});
it("clamps the End key to the 720px floor when it exceeds the percentage minimum", () => {
Object.defineProperty(window, "innerWidth", { value: 1000, configurable: true, writable: true });
render(
<Sheet open>
<ResizableSheetContent storageKey="k">
<SheetTitle>t</SheetTitle>
</ResizableSheetContent>
</Sheet>,
);
const sep = screen.getByRole("separator");
fireEvent.keyDown(sep, { key: "End" });
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("72%");
expect(sep).toHaveAttribute("aria-valuenow", "72");
});
it("announces the rendered width when the 720px floor overrides the stored percentage", () => {
localStorage.setItem("k", "40");
Object.defineProperty(window, "innerWidth", { value: 1000, configurable: true, writable: true });
render(
<Sheet open>
<ResizableSheetContent storageKey="k">
<SheetTitle>t</SheetTitle>
</ResizableSheetContent>
</Sheet>,
);
const sep = screen.getByRole("separator");
expect(sep).toHaveAttribute("aria-valuenow", "72");
expect(sep).toHaveAttribute("aria-valuemin", "72");
Object.defineProperty(window, "innerWidth", { value: 2000, configurable: true, writable: true });
fireEvent(window, new Event("resize"));
expect(sep).toHaveAttribute("aria-valuenow", "40");
expect(sep).toHaveAttribute("aria-valuemin", "40");
Object.defineProperty(window, "innerWidth", { value: 800, configurable: true, writable: true });
fireEvent(window, new Event("resize"));
expect(sep).toHaveAttribute("aria-valuenow", "90");
expect(localStorage.getItem("k")).toBe("40");
});
});

View file

@ -0,0 +1,160 @@
"use client";
import * as React from "react";
import { Maximize2Icon, Minimize2Icon } from "lucide-react";
import { cn } from "@/lib/cva.config";
import { Button } from "@/components/ui/button";
import { SheetContent } from "@/components/ui/sheet";
import { getLocalStorageItem, setLocalStorageItem } from "@/utils/localStorageUtils";
type ResizableSheetContentProps = Omit<React.ComponentProps<typeof SheetContent>, "side" | "style"> & {
storageKey: string;
defaultWidthPercent?: number;
minWidthPercent?: number;
};
const MIN_WIDTH_PX = 720;
function clampWidth(value: number, min: number) {
return Math.min(100, Math.max(min, value));
}
function effectiveMinPercent(minWidthPercent: number, viewportWidth: number) {
if (!viewportWidth) return minWidthPercent;
return Math.min(100, Math.max(minWidthPercent, (MIN_WIDTH_PX / viewportWidth) * 100));
}
function readViewportWidth() {
return typeof window === "undefined" ? 0 : window.innerWidth;
}
function readStoredWidth(storageKey: string, defaultWidthPercent: number, minWidthPercent: number) {
const stored = Number(getLocalStorageItem(storageKey));
if (!Number.isFinite(stored)) return defaultWidthPercent;
if (stored < minWidthPercent || stored > 100) return defaultWidthPercent;
return stored;
}
function ResizableSheetContent({
className,
children,
storageKey,
defaultWidthPercent = 75,
minWidthPercent = 40,
...props
}: ResizableSheetContentProps) {
const [width, setWidth] = React.useState(() => readStoredWidth(storageKey, defaultWidthPercent, minWidthPercent));
const widthRef = React.useRef(width);
const lastNonFullWidthRef = React.useRef(defaultWidthPercent);
const cleanupRef = React.useRef<(() => void) | null>(null);
const [viewportWidth, setViewportWidth] = React.useState(readViewportWidth);
const isFull = width >= 100;
const minPercent = effectiveMinPercent(minWidthPercent, viewportWidth);
const renderedWidth = clampWidth(width, minPercent);
React.useEffect(() => () => cleanupRef.current?.(), []);
React.useEffect(() => {
const onResize = () => setViewportWidth(window.innerWidth);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
const applyWidth = (next: number, persist: boolean) => {
const clamped = clampWidth(next, effectiveMinPercent(minWidthPercent, window.innerWidth));
widthRef.current = clamped;
setWidth(clamped);
if (persist) setLocalStorageItem(storageKey, String(clamped));
};
const onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
event.preventDefault();
cleanupRef.current?.();
const onMove = (e: PointerEvent) => {
applyWidth(((window.innerWidth - e.clientX) / window.innerWidth) * 100, false);
};
const onUp = () => {
cleanupRef.current?.();
cleanupRef.current = null;
if (widthRef.current < 100) setLocalStorageItem(storageKey, String(widthRef.current));
};
cleanupRef.current = () => {
window.removeEventListener("pointermove", onMove);
window.removeEventListener("pointerup", onUp);
window.removeEventListener("pointercancel", onUp);
};
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp);
window.addEventListener("pointercancel", onUp);
};
const toggle = () => {
if (isFull) {
applyWidth(lastNonFullWidthRef.current, true);
return;
}
lastNonFullWidthRef.current = width;
applyWidth(100, false);
};
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
applyWidth(widthRef.current + 5, true);
return;
}
if (event.key === "ArrowRight") {
event.preventDefault();
applyWidth(widthRef.current - 5, true);
return;
}
if (event.key === "Home") {
event.preventDefault();
applyWidth(100, false);
return;
}
if (event.key === "End") {
event.preventDefault();
applyWidth(effectiveMinPercent(minWidthPercent, window.innerWidth), true);
}
};
return (
<SheetContent
side="right"
className={cn(
"data-[side=right]:w-full data-[side=right]:sm:w-(--sheet-width) data-[side=right]:sm:min-w-[min(720px,100%)] data-[side=right]:sm:max-w-none",
className,
)}
style={{ "--sheet-width": `${width}%` } as React.CSSProperties}
{...props}
>
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto">{children}</div>
<div
role="separator"
aria-orientation="vertical"
aria-label="Resize drawer"
data-slot="sheet-resize-handle"
className="absolute inset-y-0 left-0 hidden w-1.5 cursor-col-resize touch-none select-none hover:bg-border focus-visible:bg-border focus-visible:outline-none sm:block"
tabIndex={0}
aria-valuenow={Math.round(renderedWidth)}
aria-valuemin={Math.round(minPercent)}
aria-valuemax={100}
onPointerDown={onPointerDown}
onKeyDown={onKeyDown}
/>
<Button
variant="ghost"
size="icon-sm"
className="absolute top-4 right-14 hidden sm:inline-flex"
aria-label={isFull ? "Collapse drawer" : "Expand drawer"}
onClick={toggle}
>
{isFull ? <Minimize2Icon /> : <Maximize2Icon />}
</Button>
</SheetContent>
);
}
export { ResizableSheetContent };

View file

@ -131,4 +131,64 @@ describe("AuditLogDrawer", () => {
await waitFor(() => expect(writeText).toHaveBeenCalledWith(JSON.stringify({ max_budget: 10 }, null, 2)));
});
describe("width", () => {
beforeEach(() => {
localStorage.clear();
Object.defineProperty(window, "innerWidth", { value: 2000, configurable: true, writable: true });
});
function sheetContent(): HTMLElement {
return screen.getByRole("dialog");
}
it("defaults to 75% width", () => {
render(<AuditLogDrawer {...defaultProps} />);
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("75%");
});
it("expands to full width and collapses back to the previous width", async () => {
const user = userEvent.setup();
render(<AuditLogDrawer {...defaultProps} />);
await user.click(screen.getByRole("button", { name: /expand drawer/i }));
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("100%");
await user.click(screen.getByRole("button", { name: /collapse drawer/i }));
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("75%");
expect(screen.getByRole("button", { name: /expand drawer/i })).toBeInTheDocument();
});
it("restores a stored width", () => {
localStorage.setItem("litellm:auditLogDrawerWidth", "58");
render(<AuditLogDrawer {...defaultProps} />);
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("58%");
});
it("keeps the saved width while expanded and restores it on collapse", async () => {
const user = userEvent.setup();
localStorage.setItem("litellm:auditLogDrawerWidth", "58");
render(<AuditLogDrawer {...defaultProps} />);
await user.click(screen.getByRole("button", { name: /expand drawer/i }));
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("100%");
expect(localStorage.getItem("litellm:auditLogDrawerWidth")).toBe("58");
await user.click(screen.getByRole("button", { name: /collapse drawer/i }));
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("58%");
expect(localStorage.getItem("litellm:auditLogDrawerWidth")).toBe("58");
});
it("falls back to the default width for a non-numeric stored value", () => {
localStorage.setItem("litellm:auditLogDrawerWidth", "abc");
render(<AuditLogDrawer {...defaultProps} />);
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("75%");
});
it("falls back to the default width for an out-of-range stored value", () => {
localStorage.setItem("litellm:auditLogDrawerWidth", "10");
render(<AuditLogDrawer {...defaultProps} />);
expect(sheetContent().style.getPropertyValue("--sheet-width")).toBe("75%");
});
});
});

View file

@ -6,7 +6,8 @@ import DefaultProxyAdminTag from "../../common_components/DefaultProxyAdminTag";
import CopyButton from "@/components/shared/CopyButton";
import { StatusBadge, type StatusTone } from "@/components/shared/table_cells/status_badge";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
import { Sheet, SheetTitle } from "@/components/ui/sheet";
import { ResizableSheetContent } from "@/components/ui/ResizableSheetContent";
interface AuditLogDrawerProps {
open: boolean;
@ -176,7 +177,7 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
return (
<Sheet open={open} onOpenChange={(nextOpen) => !nextOpen && onClose()}>
<SheetContent side="right" className="w-[60%] gap-0 overflow-y-auto p-0 sm:max-w-none">
<ResizableSheetContent storageKey="litellm:auditLogDrawerWidth" className="gap-0 p-0">
<SheetTitle className="sr-only">Audit log details</SheetTitle>
<div className="flex shrink-0 items-center gap-3 border-b border-border bg-card px-6 py-4">
@ -217,7 +218,7 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
<DiffSection log={log} />
</div>
</SheetContent>
</ResizableSheetContent>
</Sheet>
);
}