From 49c697ee895b7509c73526e4f62dd792a9d3abd7 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Wed, 12 Aug 2026 22:03:20 -0700 Subject: [PATCH 1/2] refactor(ui): migrate HistoryTree and CollapsibleMessage to shadcn Both log-drawer collapse rows hand-rolled the same pattern: a click handler on a plain div, hover tracked in React state, and a max-height tween on an always-mounted panel. Move both onto the ui/collapsible primitive with lucide chevrons, so the row is a real button that keyboard users can reach and the open state lives in the primitive. CollapsibleContent keeps keepMounted, which preserves the existing contract that panel content stays in the DOM while collapsed. Neither test file is touched: both were already role and text based, and they pass unedited against the new markup. --- ui/litellm-dashboard/eslint-suppressions.json | 10 --- .../LogDetailsDrawer/CollapsibleMessage.tsx | 70 ++++------------- .../LogDetailsDrawer/HistoryTree.tsx | 76 +++++-------------- 3 files changed, 36 insertions(+), 120 deletions(-) diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 8a86305b4cb..299d2a6cfdf 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -3858,21 +3858,11 @@ "count": 1 } }, - "src/components/view_logs/LogDetailsDrawer/CollapsibleMessage.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx": { "no-restricted-imports": { "count": 1 } }, - "src/components/view_logs/LogDetailsDrawer/HistoryTree.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx": { "no-restricted-imports": { "count": 1 diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/CollapsibleMessage.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/CollapsibleMessage.tsx index 9aa13c3832a..81d2fc20fdf 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/CollapsibleMessage.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/CollapsibleMessage.tsx @@ -4,10 +4,8 @@ */ import { useState } from "react"; -import { Typography } from "antd"; -import { DownOutlined, RightOutlined } from "@ant-design/icons"; - -const { Text } = Typography; +import { ChevronDown, ChevronRight } from "lucide-react"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; interface CollapsibleMessageProps { label: string; @@ -17,7 +15,6 @@ interface CollapsibleMessageProps { export function CollapsibleMessage({ label, content, defaultExpanded = false }: CollapsibleMessageProps) { const [isExpanded, setIsExpanded] = useState(defaultExpanded); - const [isHovered, setIsHovered] = useState(false); const charCount = content?.length || 0; if (!content || charCount === 0) { @@ -25,60 +22,23 @@ export function CollapsibleMessage({ label, content, defaultExpanded = false }: } return ( -
- {/* Clickable Header with hover state */} -
setIsExpanded(!isExpanded)} - onMouseEnter={() => setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - style={{ - display: "flex", - alignItems: "center", - gap: 6, - cursor: "pointer", - padding: "4px 0", - borderRadius: 4, - background: isHovered ? "#f5f5f5" : "transparent", - transition: "background 0.15s ease", - marginBottom: isExpanded ? 4 : 0, - }} - > + + {isExpanded ? ( - + ) : ( - + )} - - {label} - - - ({charCount.toLocaleString()} chars) - -
+ {label} + ({charCount.toLocaleString()} chars) + - {/* Content with smooth animation */} -
-
- {content} -
-
-
+ {content} + + ); } diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/HistoryTree.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/HistoryTree.tsx index b6c5048513c..4854ca90fc2 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/HistoryTree.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/HistoryTree.tsx @@ -4,80 +4,46 @@ */ import { useState } from "react"; -import { Typography } from "antd"; -import { DownOutlined, RightOutlined } from "@ant-design/icons"; +import { ChevronDown, ChevronRight } from "lucide-react"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { ParsedMessage } from "./prettyMessagesTypes"; import { SimpleMessageBlock } from "./SimpleMessageBlock"; -const { Text } = Typography; - interface HistoryTreeProps { messages: ParsedMessage[]; } export function HistoryTree({ messages }: HistoryTreeProps) { const [isExpanded, setIsExpanded] = useState(false); - const [isHovered, setIsHovered] = useState(false); if (messages.length === 0) { return null; } return ( -
- {/* Clickable Header with hover state */} -
setIsExpanded(!isExpanded)} - onMouseEnter={() => setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - style={{ - display: "flex", - alignItems: "center", - gap: 6, - cursor: "pointer", - padding: "4px 0", - borderRadius: 4, - background: isHovered ? "#f5f5f5" : "transparent", - transition: "background 0.15s ease", - marginBottom: isExpanded ? 4 : 0, - }} - > + + {isExpanded ? ( - + ) : ( - + )} - + HISTORY ({messages.length} message{messages.length !== 1 ? "s" : ""}) - -
+ + - {/* Expanded Tree Content with smooth animation */} -
-
- {messages.map((msg, index) => ( - - ))} -
-
-
+ + {messages.map((msg, index) => ( + + ))} + + ); } From 266a35b88c61a7fd97ee96d27df6eca36a2adb62 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Wed, 12 Aug 2026 22:39:29 -0700 Subject: [PATCH 2/2] test(ui): cover keyboard activation of the log drawer collapse rows The migration turned each collapse row into a real button, but the existing tests only click, so a regression in Enter or Space activation would still pass. Add one test per component that tabs to the row, expands with Enter and collapses with Space, asserting visibility rather than markup. Both fail against the antd version and pass against the migrated one. --- .../CollapsibleMessage.test.tsx | 14 ++++++++++++++ .../LogDetailsDrawer/HistoryTree.test.tsx | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/CollapsibleMessage.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/CollapsibleMessage.test.tsx index 1477febd967..3aa8d21c31c 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/CollapsibleMessage.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/CollapsibleMessage.test.tsx @@ -37,4 +37,18 @@ describe("CollapsibleMessage", () => { await user.click(screen.getByText("SYSTEM")); expect(screen.getByText("Toggle me")).toBeInTheDocument(); }); + + it("should expand with Enter and collapse with Space from the keyboard", async () => { + const user = userEvent.setup(); + render(); + + expect(screen.getByText("Toggle me")).not.toBeVisible(); + + await user.tab(); + await user.keyboard("{Enter}"); + expect(screen.getByText("Toggle me")).toBeVisible(); + + await user.keyboard(" "); + expect(screen.getByText("Toggle me")).not.toBeVisible(); + }); }); diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/HistoryTree.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/HistoryTree.test.tsx index 10d6bfa0847..0726a9f4b82 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/HistoryTree.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/HistoryTree.test.tsx @@ -41,4 +41,22 @@ describe("HistoryTree", () => { expect(screen.getByText("Hello")).toBeInTheDocument(); expect(screen.getByText("Hi there")).toBeInTheDocument(); }); + + it("should expand with Enter and collapse with Space from the keyboard", async () => { + const user = userEvent.setup(); + const messages: ParsedMessage[] = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there" }, + ]; + render(); + + expect(screen.getByText("Hello")).not.toBeVisible(); + + await user.tab(); + await user.keyboard("{Enter}"); + expect(screen.getByText("Hello")).toBeVisible(); + + await user.keyboard(" "); + expect(screen.getByText("Hello")).not.toBeVisible(); + }); });