From 3d76dfc72e40976c5e2cb0de8cbffed1b0852572 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 12 Aug 2026 23:43:30 -0700 Subject: [PATCH] refactor(ui): migrate search and user controls to shadcn (#36694) * test(ui): characterize shared migration surfaces * refactor(ui): migrate search and user controls * fix(ui): restore search tool clear action --- ui/litellm-dashboard/eslint-suppressions.json | 8 -- .../search_tools/SearchToolSelector.test.tsx | 44 ++++++ .../search_tools/SearchToolSelector.tsx | 67 ++++++--- .../src/components/team/MyUserTab.test.tsx | 49 +++++++ .../src/components/team/MyUserTab.tsx | 134 ++++++++---------- .../src/components/ui/combobox.tsx | 1 + 6 files changed, 207 insertions(+), 96 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/search_tools/SearchToolSelector.test.tsx create mode 100644 ui/litellm-dashboard/src/components/team/MyUserTab.test.tsx diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 18dd7c949ec..590aeb3507e 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -3296,9 +3296,6 @@ "src/components/search_tools/SearchToolSelector.tsx": { "no-nested-ternary": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/components/settings.test.tsx": { @@ -3468,11 +3465,6 @@ "count": 2 } }, - "src/components/team/MyUserTab.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/team/TeamInfo.tsx": { "max-lines": { "count": 1 diff --git a/ui/litellm-dashboard/src/components/search_tools/SearchToolSelector.test.tsx b/ui/litellm-dashboard/src/components/search_tools/SearchToolSelector.test.tsx new file mode 100644 index 00000000000..b8895718df0 --- /dev/null +++ b/ui/litellm-dashboard/src/components/search_tools/SearchToolSelector.test.tsx @@ -0,0 +1,44 @@ +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { fetchSearchTools } from "../networking"; +import SearchToolSelector from "./SearchToolSelector"; + +vi.mock("../networking", () => ({ + fetchSearchTools: vi.fn(), +})); + +describe("SearchToolSelector", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(fetchSearchTools).mockResolvedValue({ + search_tools: [{ search_tool_name: "search-one" }, { search_tool_name: "search-two" }], + }); + }); + + it("should render", () => { + renderWithProviders(); + + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); + + it("should load and display available search tools", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByRole("combobox")); + + expect(await screen.findByRole("option", { name: "search-one" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "search-two" })).toBeInTheDocument(); + }); + + it("should clear all selected search tools", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + renderWithProviders(); + + await user.click(screen.getByRole("button", { name: "Clear all search tools" })); + + expect(onChange).toHaveBeenCalledWith([]); + }); +}); diff --git a/ui/litellm-dashboard/src/components/search_tools/SearchToolSelector.tsx b/ui/litellm-dashboard/src/components/search_tools/SearchToolSelector.tsx index c93ff35e7de..56f5954345d 100644 --- a/ui/litellm-dashboard/src/components/search_tools/SearchToolSelector.tsx +++ b/ui/litellm-dashboard/src/components/search_tools/SearchToolSelector.tsx @@ -1,5 +1,17 @@ import React, { useEffect, useState } from "react"; -import { Select } from "antd"; +import { + Combobox, + ComboboxChip, + ComboboxChips, + ComboboxChipsInput, + ComboboxClear, + ComboboxContent, + ComboboxEmpty, + ComboboxItem, + ComboboxList, + ComboboxValue, +} from "@/components/ui/combobox"; +import { cn } from "@/lib/cva.config"; import { fetchSearchTools } from "../networking"; export interface SearchToolSelectorProps { @@ -19,7 +31,7 @@ const SearchToolSelector: React.FC = ({ placeholder = "Select search tools (optional)", disabled = false, }) => { - const [options, setOptions] = useState<{ label: string; value: string }[]>([]); + const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { @@ -36,8 +48,7 @@ const SearchToolSelector: React.FC = ({ setOptions( tools .map((tool: { search_tool_name?: string }) => tool?.search_tool_name) - .filter((name: unknown): name is string => typeof name === "string" && name.length > 0) - .map((name: string) => ({ label: name, value: name })), + .filter((name: unknown): name is string => typeof name === "string" && name.length > 0), ); } catch (e) { console.error("Failed to load search tools:", e); @@ -49,20 +60,42 @@ const SearchToolSelector: React.FC = ({ }, [accessToken]); return ( -