From 328754feee90001f65f4a215f288ccf862ce5286 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 10:46:34 +0000 Subject: [PATCH] feat(ui): simple_table + team_dropdown migrate off antd/@tremor Text - simple_table: the Tremor Table family is whitelisted in phase 1 so only the @tremor Text wrapper was swapped for a plain span with semantic text-muted-foreground. File keeps its inline eslint-disable for the preserved Tremor table primitives. - team_dropdown: antd Select with infinite scroll + debounced remote search + antd LoadingOutlined \u2192 shadcn Popover + Button (combobox) + Input + lucide Loader2/ChevronsUpDown/X. Infinite-scroll is driven by the list element's scroll handler with the same 0.8 threshold. Cleared button spans stopPropagation to prevent the popover from toggling open on clear. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- .../common_components/simple_table.tsx | 25 ++- .../common_components/team_dropdown.tsx | 165 ++++++++++++------ 2 files changed, 134 insertions(+), 56 deletions(-) diff --git a/ui/litellm-dashboard/src/components/common_components/simple_table.tsx b/ui/litellm-dashboard/src/components/common_components/simple_table.tsx index 3ef3a070120..a2fcb56945d 100644 --- a/ui/litellm-dashboard/src/components/common_components/simple_table.tsx +++ b/ui/litellm-dashboard/src/components/common_components/simple_table.tsx @@ -1,5 +1,13 @@ import React from "react"; -import { Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell, Text } from "@tremor/react"; +// eslint-disable-next-line litellm-ui/no-banned-ui-imports +import { + Table, + TableHead, + TableRow, + TableHeaderCell, + TableBody, + TableCell, +} from "@tremor/react"; export interface SimpleTableColumn { header: string; @@ -18,8 +26,10 @@ interface SimpleTableProps { } /** - * Simple table component for forms and settings pages - * For complex tables with sorting/filtering, use DataTable from view_logs + * Simple table component for forms and settings pages. + * The underlying primitives are still Tremor's Table family (whitelisted + * by the banned-imports rule); only the Text wrapper was swapped for a + * semantic . */ export function SimpleTable({ data, @@ -44,7 +54,7 @@ export function SimpleTable({ {isLoading ? ( - {loadingMessage} + {loadingMessage} ) : data.length > 0 ? ( @@ -52,7 +62,9 @@ export function SimpleTable({ {columns.map((column, colIndex) => ( - {column.cell ? column.cell(row) : String(row[column.accessor as keyof T] ?? "")} + {column.cell + ? column.cell(row) + : String(row[column.accessor as keyof T] ?? "")} ))} @@ -60,7 +72,7 @@ export function SimpleTable({ ) : ( - {emptyMessage} + {emptyMessage} )} @@ -68,4 +80,3 @@ export function SimpleTable({ ); } - diff --git a/ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx b/ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx index 8bdde4771fd..cdf5ce8ecbd 100644 --- a/ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx +++ b/ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx @@ -1,11 +1,16 @@ -import React, { useMemo, useState, type UIEvent } from "react"; -import { Select, Typography } from "antd"; -import { LoadingOutlined } from "@ant-design/icons"; +import React, { useMemo, useRef, useState } from "react"; +import { ChevronsUpDown, Loader2, X } from "lucide-react"; import { useDebouncedState } from "@tanstack/react-pacer/debouncer"; import { useInfiniteTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import { Team } from "../key_team_helpers/key_list"; - -const { Text } = Typography; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; interface TeamDropdownProps { value?: string; @@ -29,22 +34,15 @@ const TeamDropdown: React.FC = ({ organizationId, pageSize = 20, }) => { + const [open, setOpen] = useState(false); const [searchInput, setSearchInput] = useState(""); const [debouncedSearch, setDebouncedSearch] = useDebouncedState("", { wait: DEBOUNCE_MS, }); + const listRef = useRef(null); - const { - data, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - isLoading, - } = useInfiniteTeams( - pageSize, - debouncedSearch || undefined, - organizationId, - ); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = + useInfiniteTeams(pageSize, debouncedSearch || undefined, organizationId); const teams = useMemo(() => { if (!data?.pages) return []; @@ -60,7 +58,11 @@ const TeamDropdown: React.FC = ({ return result; }, [data]); - const handlePopupScroll = (e: UIEvent) => { + const selectedTeam = value + ? teams.find((t) => t.team_id === value) + : undefined; + + const handleScroll = (e: React.UIEvent) => { const target = e.currentTarget; const scrollRatio = (target.scrollTop + target.clientHeight) / target.scrollHeight; @@ -74,47 +76,112 @@ const TeamDropdown: React.FC = ({ setDebouncedSearch(val); }; - const handleChange = (teamId: string | undefined) => { - onChange?.(teamId ?? ""); + const select = (teamId: string) => { + onChange?.(teamId); if (onTeamSelect) { - const team = teamId ? teams.find((t) => t.team_id === teamId) ?? null : null; + const team = teams.find((t) => t.team_id === teamId) ?? null; onTeamSelect(team); } + setOpen(false); + }; + + const clear = (e: React.MouseEvent) => { + e.stopPropagation(); + onChange?.(""); + onTeamSelect?.(null); }; return ( - handleSearch(e.target.value)} + className="h-8" + /> + +
+ {isLoading && teams.length === 0 ? ( +
+ +
+ ) : teams.length === 0 ? ( +
+ No teams found +
+ ) : ( + teams.map((team) => ( + + )) + )} {isFetchingNextPage && ( -
- +
+
)} - - )} - > - {teams.map((team) => ( - - {team.team_alias}{" "} - ({team.team_id}) - - ))} - +
+ + ); };