From f231d46375e9e56553ef50ca96dccba4af94beac Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 23 Jul 2026 22:26:34 -0700 Subject: [PATCH 1/3] test(ui): decouple access-groups, vector-stores and organizations tests from antd markup Prepares the shadcn migration of these three routes by removing every assertion that depends on the current component library, so the same tests can gate the migration without being edited. FiltersButton and its OrganizationFilters consumer both asserted on the ".ant-badge" wrapper class; they now assert the active-filter indicator element itself, and FiltersButton additionally asserts that it is absent when there are no active filters. TestVectorStoreTab drove the antd Select with fireEvent.mouseDown and picked options by node; it now clicks through the combobox role and the option text, which works against any listbox implementation. The vector-stores index test relied on Tremor mounting every TabPanel at once, so it read the Manage tab's table without ever opening that tab. It now clicks the tab first, which is what a user does and what any tabs implementation supports. VectorStoreTester had no test at all, so this adds a characterisation suite covering the empty state, the blank-query guard, the search call and its rendered result, result expansion, Enter versus Shift+Enter, the failure path and clearing history. All of these pass against the current antd and Tremor components --- .../OrganizationFilters.test.tsx | 7 +- .../_components/TestVectorStoreTab.test.tsx | 28 ++-- .../_components/VectorStoreTester.test.tsx | 156 ++++++++++++++++++ .../vector-stores/_components/index.test.tsx | 9 + .../Filters/FiltersButton.test.tsx | 14 +- 5 files changed, 189 insertions(+), 25 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.test.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx index 37eeaf4c2af9..c1eda1be670a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx @@ -106,7 +106,7 @@ describe("OrganizationFilters", () => { org_alias: "test org", }; - render( + const { container } = render( { />, ); - const filtersButton = screen.getByRole("button", { name: /^filters$/i }); - const badgeWrapper = filtersButton.closest(".ant-badge"); - expect(badgeWrapper).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /^filters$/i })).toBeInTheDocument(); + expect(container.querySelector("sup")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.test.tsx index c1322dced609..1d7bce34e041 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.test.tsx @@ -1,4 +1,5 @@ -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { describe, it, expect, vi } from "vitest"; import TestVectorStoreTab from "./TestVectorStoreTab"; import { VectorStore } from "@/components/vector_store_management/types"; @@ -60,31 +61,24 @@ describe("TestVectorStoreTab", () => { expect(screen.getByTestId("tester-access-token")).toHaveTextContent("test-token"); }); - it("should update VectorStoreTester when selecting different vector store", () => { + it("should update VectorStoreTester when selecting different vector store", async () => { + const user = userEvent.setup(); render(); - // Find the select component - const selectElement = screen.getByRole("combobox"); + await user.click(screen.getByRole("combobox")); + await user.click(await screen.findByText("Test Store 2")); - // Change selection - fireEvent.mouseDown(selectElement); - - // Wait for options to appear and click the second one - const option2 = screen.getByText("Test Store 2"); - fireEvent.click(option2); - - // Verify the tester component updated expect(screen.getByTestId("tester-vector-store-id")).toHaveTextContent("vs_456"); }); - it("should display vector store names in select options", () => { + it("should display vector store names in select options", async () => { + const user = userEvent.setup(); render(); - const selectElement = screen.getByRole("combobox"); - fireEvent.mouseDown(selectElement); + await user.click(screen.getByRole("combobox")); - // Use getAllByText since the selected value also shows the name - expect(screen.getAllByText("Test Store 1").length).toBeGreaterThan(0); + // The selected store's name may also render in the trigger, so only require at least one match. + expect((await screen.findAllByText("Test Store 1")).length).toBeGreaterThan(0); expect(screen.getByText("Test Store 2")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.test.tsx new file mode 100644 index 000000000000..cbabcc6dca5a --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.test.tsx @@ -0,0 +1,156 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { vectorStoreSearchCall } from "@/components/networking"; + +import { VectorStoreTester } from "./VectorStoreTester"; + +vi.mock("@/components/networking", () => ({ + vectorStoreSearchCall: vi.fn(), +})); + +const mockWarning = vi.fn(); +vi.mock("@/components/molecules/message_manager", () => ({ + __esModule: true, + default: { warning: (...args: unknown[]) => mockWarning(...args) }, +})); + +const mockFromBackend = vi.fn(); +const mockSuccess = vi.fn(); +vi.mock("@/components/molecules/notifications_manager", () => ({ + __esModule: true, + default: { + fromBackend: (...args: unknown[]) => mockFromBackend(...args), + success: (...args: unknown[]) => mockSuccess(...args), + }, +})); + +const mockSearch = vi.mocked(vectorStoreSearchCall); + +const searchResponse = { + object: "vector_store.search_results.page", + search_query: "hello", + data: [ + { + score: 0.91234, + content: [{ text: "the quick brown fox", type: "text" }], + file_id: "file-1", + filename: "notes.txt", + attributes: { source: "manual" }, + }, + ], +}; + +const EMPTY_STATE = "Test your vector store by entering a search query below"; + +const renderTester = () => render(); + +const queryInput = () => screen.getByPlaceholderText(/enter your search query/i); +const searchButton = () => screen.getByRole("button", { name: /search/i }); + +describe("VectorStoreTester", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockSearch.mockResolvedValue(searchResponse); + }); + + it("shows the empty state before any search has run", () => { + renderTester(); + expect(screen.getByText(EMPTY_STATE)).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /clear history/i })).not.toBeInTheDocument(); + }); + + it("does not search until a non-blank query is entered", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.click(searchButton()); + expect(mockSearch).not.toHaveBeenCalled(); + + await user.type(queryInput(), "hello"); + await user.click(searchButton()); + + await waitFor(() => expect(mockSearch).toHaveBeenCalledWith("sk-test", "vs_123", "hello")); + }); + + it("renders the returned result and clears the query input", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.type(queryInput(), "hello"); + await user.click(searchButton()); + + expect(await screen.findByText("Result 1")).toBeInTheDocument(); + expect(screen.getByText("1 results")).toBeInTheDocument(); + expect(screen.getByText("Score: 0.9123")).toBeInTheDocument(); + expect(screen.queryByText(EMPTY_STATE)).not.toBeInTheDocument(); + await waitFor(() => expect(queryInput()).toHaveValue("")); + }); + + it("expands a result to reveal its content and metadata", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.type(queryInput(), "hello"); + await user.click(searchButton()); + + expect(await screen.findByText("Result 1")).toBeInTheDocument(); + expect(screen.queryByText("the quick brown fox")).not.toBeInTheDocument(); + + await user.click(screen.getByText("Result 1")); + + expect(screen.getByText("the quick brown fox")).toBeInTheDocument(); + expect(screen.getByText("File ID:").parentElement).toHaveTextContent("file-1"); + expect(screen.getByText("Filename:").parentElement).toHaveTextContent("notes.txt"); + }); + + it("warns instead of searching when the query is only whitespace", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.type(queryInput(), " "); + await user.type(queryInput(), "{Enter}"); + + expect(mockWarning).toHaveBeenCalledWith("Please enter a search query"); + expect(mockSearch).not.toHaveBeenCalled(); + }); + + it("submits on Enter but not on Shift+Enter", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.type(queryInput(), "hello"); + await user.type(queryInput(), "{Shift>}{Enter}{/Shift}"); + expect(mockSearch).not.toHaveBeenCalled(); + + await user.type(queryInput(), "{Enter}"); + await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(1)); + }); + + it("reports a failed search and keeps the history empty", async () => { + const user = userEvent.setup(); + mockSearch.mockRejectedValue(new Error("boom")); + renderTester(); + + await user.type(queryInput(), "hello"); + await user.click(searchButton()); + + await waitFor(() => expect(mockFromBackend).toHaveBeenCalledWith("Failed to search vector store")); + expect(screen.getByText(EMPTY_STATE)).toBeInTheDocument(); + }); + + it("clears the search history", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.type(queryInput(), "hello"); + await user.click(searchButton()); + expect(await screen.findByText("Result 1")).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: /clear history/i })); + + expect(screen.queryByText("Result 1")).not.toBeInTheDocument(); + expect(screen.getByText(EMPTY_STATE)).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/index.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/index.test.tsx index 2931372f384d..521c1f879ee5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/index.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/index.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { vectorStoreListCall } from "@/components/networking"; @@ -25,18 +26,25 @@ vi.mock("./TestVectorStoreTab", () => ({ __esModule: true, default: () => null } const mockVectorStoreListCall = vi.mocked(vectorStoreListCall); +const openManageTab = async (user: ReturnType) => { + await user.click(screen.getByRole("tab", { name: "Manage Vector Stores" })); +}; + describe("VectorStoreManagement loading state", () => { beforeEach(() => { vi.clearAllMocks(); }); it("should resolve the loading state when accessToken is null instead of showing the skeleton forever", async () => { + const user = userEvent.setup(); render(); + await openManageTab(user); expect(await screen.findByText("table-loaded")).toBeInTheDocument(); expect(mockVectorStoreListCall).not.toHaveBeenCalled(); }); it("should show the loading state until the vector store fetch settles", async () => { + const user = userEvent.setup(); let resolveFetch: (value: { data: never[] }) => void = () => {}; mockVectorStoreListCall.mockReturnValue( new Promise((resolve) => { @@ -44,6 +52,7 @@ describe("VectorStoreManagement loading state", () => { }), ); render(); + await openManageTab(user); expect(screen.getByText("table-loading")).toBeInTheDocument(); resolveFetch({ data: [] }); diff --git a/ui/litellm-dashboard/src/components/common_components/Filters/FiltersButton.test.tsx b/ui/litellm-dashboard/src/components/common_components/Filters/FiltersButton.test.tsx index ccb2c5d9e53b..145fdb1f5761 100644 --- a/ui/litellm-dashboard/src/components/common_components/Filters/FiltersButton.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/Filters/FiltersButton.test.tsx @@ -21,12 +21,18 @@ describe("FiltersButton", () => { expect(onClick).toHaveBeenCalledTimes(1); }); - it("should show badge when hasActiveFilters is true", () => { + it("should show the active-filter indicator when hasActiveFilters is true", () => { const onClick = vi.fn(); const { container } = render(); - const button = screen.getByRole("button", { name: /filters/i }); - const badgeWrapper = button.closest(".ant-badge"); - expect(badgeWrapper).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /filters/i })).toBeInTheDocument(); + expect(container.querySelector("sup")).toBeInTheDocument(); + }); + + it("should not show the active-filter indicator when hasActiveFilters is false", () => { + const onClick = vi.fn(); + const { container } = render(); + expect(screen.getByRole("button", { name: /filters/i })).toBeInTheDocument(); + expect(container.querySelector("sup")).not.toBeInTheDocument(); }); it("should render custom label when provided", () => { From a1bacb660f0f340a01238046a76b72788a181ce4 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 23 Jul 2026 22:54:00 -0700 Subject: [PATCH 2/3] refactor(ui): migrate access-groups, vector-stores, organizations to shadcn Moves the nine files these three routes exclusively own off antd and Tremor onto the shadcn primitives in src/components/ui. Scope came from the migration analyzer's import closure, so nothing reached by a second route is touched and every file carrying an antd Form is left alone until #34195 lands. access-groups gets the page header, search box and the whole detail view; vector-stores gets the tab shell, the store picker and the tester panel; organizations gets the organization detail view and the three filter controls. Two changes are behavioural rather than cosmetic. The vector-stores tab strip moves from Tremor, which mounts every panel at once, to Base UI, which mounts only the active panel; that is the correct behaviour and the reworked test now opens the tab it asserts on. The antd Select on the Test Vector Store tab becomes a combobox rather than a plain select so its showSearch type-ahead survives. organization_view keeps one antd import, the ColumnsType used to build the extra columns it hands to the shared MemberTable; that is dictated by the shared component's API and goes away when MemberTable migrates. eslint-suppressions.json ratchets down accordingly: eight files lose their no-restricted-imports entry and organization_view drops from three to one. Every test passes unedited across the migration, and the visual gate reports the three migrated routes changed with the other 32 pixel-identical --- ui/litellm-dashboard/eslint-suppressions.json | 38 +- .../_components/AccessGroupsDetailsPage.tsx | 378 +++++++---------- .../_components/AccessGroupsPage.tsx | 71 ++-- .../_components/TestVectorStoreTab.tsx | 83 ++-- .../_components/VectorStoreTester.tsx | 106 +++-- .../vector-stores/_components/index.tsx | 113 +++-- .../common_components/Filters/FilterInput.tsx | 17 +- .../Filters/FiltersButton.tsx | 13 +- .../Filters/ResetFiltersButton.tsx | 5 +- .../organization/organization_view.tsx | 397 +++++++++--------- 10 files changed, 557 insertions(+), 664 deletions(-) diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index ec1e3ac05bab..571598b18530 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -4,11 +4,6 @@ "count": 1 } }, - "src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupBaseForm.tsx": { "no-restricted-imports": { "count": 2 @@ -24,11 +19,6 @@ "count": 1 } }, - "src/app/(dashboard)/access-groups/_components/AccessGroupsPage.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/admin-panel/_components/AdminPanel.tsx": { "no-restricted-imports": { "count": 2 @@ -2054,11 +2044,6 @@ "count": 1 } }, - "src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/vector-stores/_components/VectorStoreForm.tsx": { "no-nested-ternary": { "count": 2 @@ -2070,18 +2055,10 @@ "count": 1 } }, - "src/app/(dashboard)/vector-stores/_components/VectorStoreTester.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/vector-stores/_components/index.tsx": { "local/filename-pascal-case": { "count": 1 }, - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -3017,23 +2994,10 @@ } }, "src/components/common_components/Filters/FilterInput.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/components/common_components/Filters/FiltersButton.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/components/common_components/Filters/ResetFiltersButton.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/IconActionButton/BaseActionButton.tsx": { "no-restricted-imports": { "count": 1 @@ -3595,7 +3559,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 3 + "count": 1 } }, "src/components/page_utils.test.ts": { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.tsx index 72a89093bdb0..9476a8d98af4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.tsx @@ -1,68 +1,63 @@ import { useAccessGroupDetails } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails"; -import { - Button, - Card, - Col, - Descriptions, - Empty, - Flex, - Layout, - List, - Row, - Spin, - Tabs, - Tag, - theme, - Typography, -} from "antd"; import { ArrowLeftIcon, BotIcon, EditIcon, KeyIcon, LayersIcon, ServerIcon, UsersIcon } from "lucide-react"; import { useState } from "react"; import DefaultProxyAdminTag from "@/components/common_components/DefaultProxyAdminTag"; +import CopyButton from "@/components/shared/CopyButton"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { AccessGroupEditModal } from "./AccessGroupsModal/AccessGroupEditModal"; -const { Title, Text } = Typography; -const { Content } = Layout; - interface AccessGroupDetailProps { accessGroupId: string; onBack: () => void; } +const MAX_PREVIEW = 5; + +function ResourceList({ ids, emptyMessage }: { ids: string[]; emptyMessage: string }) { + if (ids.length === 0) { + return

{emptyMessage}

; + } + return ( +
+ {ids.map((id) => ( + + + {id} + + + ))} +
+ ); +} + export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailProps) { const { data: accessGroup, isLoading } = useAccessGroupDetails(accessGroupId); - const { token } = theme.useToken(); const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [showAllKeys, setShowAllKeys] = useState(false); const [showAllTeams, setShowAllTeams] = useState(false); - const MAX_PREVIEW = 5; - if (isLoading) { return ( - - - - - +
+
+ +
+
); } if (!accessGroup) { return ( - - +

Access group not found

+ ); } @@ -75,224 +70,159 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr const displayedKeys = showAllKeys ? keyIds : keyIds.slice(0, MAX_PREVIEW); const displayedTeams = showAllTeams ? teamIds : teamIds.slice(0, MAX_PREVIEW); - const handleEdit = () => { - setIsEditModalVisible(true); - }; - - const tabItems = [ - { - key: "models", - label: ( - - - Models - {modelIds?.length} - - ), - children: - modelIds?.length > 0 ? ( - ( - - - {id} - - - )} - /> - ) : ( - - ), - }, - { - key: "mcp", - label: ( - - - MCP Servers - {mcpServerIds?.length} - - ), - children: - mcpServerIds?.length > 0 ? ( - ( - - - {id} - - - )} - /> - ) : ( - - ), - }, - { - key: "agents", - label: ( - - - Agents - {agentIds?.length} - - ), - children: - agentIds?.length > 0 ? ( - ( - - - {id} - - - )} - /> - ) : ( - - ), - }, - ]; - return ( - - {/* Header */} -
-
-
- - {accessGroup.access_group_name} - - - ID: {accessGroup.access_group_id} - +

{accessGroup.access_group_name}

+
+ ID: {accessGroup.access_group_id} + +
-
- {/* Group Details */} - - - - {accessGroup.description || "—"} - + + + Group Details + + +
+
Description
+
{accessGroup.description || "—"}
+
Created
+
{new Date(accessGroup.created_at).toLocaleString()} {accessGroup.created_by && ( - -  {"by"}  + <> + by - + )} - - +
+
Last Updated
+
{new Date(accessGroup.updated_at).toLocaleString()} {accessGroup.updated_by && ( - -  {"by"}  + <> + by - + )} - - - - +
+
+
+
- {/* Attached Keys & Teams */} - - - - - Attached Keys - {keyIds?.length} - - } - extra={ - keyIds?.length > MAX_PREVIEW ? ( - - ) : null - } - > - {keyIds?.length > 0 ? ( - + + )} + + + {keyIds.length > 0 ? ( +
{displayedKeys.map((id) => ( - - - {id.length > 20 ? `${id.slice(0, 10)}...${id.slice(-6)}` : id} - - + + {id.length > 20 ? `${id.slice(0, 10)}...${id.slice(-6)}` : id} + ))} - +
) : ( - +

No keys attached

)} -
- - - - - Attached Teams - {teamIds?.length} - - } - extra={ - teamIds?.length > MAX_PREVIEW ? ( - - ) : null - } - > - {teamIds?.length > 0 ? ( - + + )} + + + {teamIds.length > 0 ? ( +
{displayedTeams.map((id) => ( - - - {id} - - + + {id} + ))} - +
) : ( - +

No teams attached

)} -
- -
+ +
+ - {/* Resources Tabs */} - + + + + + + Models + {modelIds.length} + + + + MCP Servers + {mcpServerIds.length} + + + + Agents + {agentIds.length} + + + + + + + + + + + + + - {/* Edit Modal */} setIsEditModalVisible(false)} /> -
+ ); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.tsx index 0de6596f57c6..f37acb3d85aa 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.tsx @@ -1,10 +1,11 @@ import { AccessGroupResponse, useAccessGroups } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups"; import { useDeleteAccessGroup } from "@/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup"; -import { PlusOutlined } from "@ant-design/icons"; -import { Button, Flex, Input, Layout, Space, theme, Typography } from "antd"; -import { SearchIcon } from "lucide-react"; +import { Plus, SearchIcon, X } from "lucide-react"; import { useMemo, useState } from "react"; import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; +import { PageHeader } from "@/components/shared/PageHeader"; +import { Button } from "@/components/ui/button"; +import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group"; import { AccessGroupDetail } from "./AccessGroupsDetailsPage"; import { AccessGroupCreateModal } from "./AccessGroupsModal/AccessGroupCreateModal"; import { AccessGroupsTable } from "./AccessGroupsTable"; @@ -12,9 +13,6 @@ import { AccessGroup } from "./types"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { isProxyAdminRole } from "@/utils/roles"; -const { Title, Text } = Typography; -const { Content } = Layout; - function mapResponseToAccessGroup(r: AccessGroupResponse): AccessGroup { return { id: r.access_group_id, @@ -33,7 +31,6 @@ function mapResponseToAccessGroup(r: AccessGroupResponse): AccessGroup { } export function AccessGroupsPage() { - const { token } = theme.useToken(); const { userRole } = useAuthorized(); // Admin Viewer follows the read-parity rule: see access groups, no writes. const canModify = isProxyAdminRole(userRole ?? ""); @@ -62,31 +59,41 @@ export function AccessGroupsPage() { } return ( - - - - - Access Groups - - Manage resource permissions for your organization - - {canModify && ( - - )} - - - - } - placeholder="Search groups by name, ID, or description..." - style={{ maxWidth: 400 }} - value={searchText} - onChange={(e) => setSearchText(e.target.value)} - allowClear +
+
+ setIsCreateModalVisible(true)}> + + Create Access Group + + ) : undefined + } /> - +
+ +
+ + + + + setSearchText(e.target.value)} + /> + {searchText && ( + + setSearchText("")}> + + + + )} + +
- +
); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.tsx index 3156da0b413f..fec2dc62e9cc 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.tsx @@ -1,24 +1,32 @@ import React, { useState } from "react"; -import { Card, Select, Typography } from "antd"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, +} from "@/components/ui/combobox"; import { VectorStoreTester } from "./VectorStoreTester"; import { VectorStore } from "@/components/vector_store_management/types"; -const { Text, Title } = Typography; - interface TestVectorStoreTabProps { accessToken: string | null; vectorStores: VectorStore[]; } +const storeLabel = (store: VectorStore) => store.vector_store_name || store.vector_store_id; + const TestVectorStoreTab: React.FC = ({ accessToken, vectorStores }) => { - const [selectedVectorStoreId, setSelectedVectorStoreId] = useState( - vectorStores.length > 0 ? vectorStores[0].vector_store_id : undefined, - ); + const [selectedVectorStore, setSelectedVectorStore] = useState(vectorStores[0] ?? null); if (!accessToken) { return ( - Access token is required to test vector stores. + +

Access token is required to test vector stores.

+
); } @@ -26,9 +34,11 @@ const TestVectorStoreTab: React.FC = ({ accessToken, ve if (vectorStores.length === 0) { return ( -
- No vector stores available. Create one first to test it. -
+ +
+

No vector stores available. Create one first to test it.

+
+
); } @@ -36,36 +46,41 @@ const TestVectorStoreTab: React.FC = ({ accessToken, ve return (
-
+
- Select Vector Store - Choose a vector store to test search queries against +
Select Vector Store
+

Choose a vector store to test search queries against

- -
+ + + No matching vector stores + + {(store: VectorStore) => ( + +
+ {storeLabel(store)} + {store.vector_store_name && ( + {store.vector_store_id} + )} +
+
+ )} +
+
+ +
- {selectedVectorStoreId && } + {selectedVectorStore && ( + + )}
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.tsx index fa4587b526c2..015d58e86493 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.tsx @@ -1,12 +1,13 @@ import React, { useState } from "react"; -import { Button, Input, Card, Typography, Spin, Divider } from "antd"; import MessageManager from "@/components/molecules/message_manager"; -import { SendOutlined, DatabaseOutlined, LoadingOutlined, DownOutlined, RightOutlined } from "@ant-design/icons"; +import { ChevronDown, ChevronRight, Database, Send } from "lucide-react"; import { vectorStoreSearchCall } from "@/components/networking"; import NotificationsManager from "@/components/molecules/notifications_manager"; - -const { TextArea } = Input; -const { Text, Title } = Typography; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; interface VectorStoreContent { text: string; @@ -98,18 +99,16 @@ export const VectorStoreTester: React.FC = ({ vectorStor }; return ( - -
+ +
{/* Header */} -
+
- - - Test Vector Store - + +

Test Vector Store

{searchHistory.length > 0 && ( - )} @@ -118,9 +117,9 @@ export const VectorStoreTester: React.FC = ({ vectorStor {/* Results Area */}
{searchHistory.length === 0 ? ( -
- - Test your vector store by entering a search query below +
+ +

Test your vector store by entering a search query below

) : (
@@ -128,10 +127,10 @@ export const VectorStoreTester: React.FC = ({ vectorStor
{/* User Query */}
-
-
+
+
Query - {formatTimestamp(entry.timestamp)} + {formatTimestamp(entry.timestamp)}
{entry.query}
@@ -139,12 +138,12 @@ export const VectorStoreTester: React.FC = ({ vectorStor {/* Vector Store Response */}
-
-
- +
+
+ Vector Store Results {entry.response && ( - + {entry.response.data?.length || 0} results )} @@ -156,40 +155,42 @@ export const VectorStoreTester: React.FC = ({ vectorStor const isExpanded = expandedResults[`${index}-${resultIndex}`] || false; return ( -
+
{/* Clickable Header */}
toggleResultExpansion(index, resultIndex)} >
{isExpanded ? ( - + ) : ( - + )} - Result {resultIndex + 1} + Result {resultIndex + 1} {/* Show preview of content when collapsed */} {!isExpanded && result.content && result.content[0] && ( - + - {result.content[0].text.substring(0, 100)}... )}
- + Score: {result.score.toFixed(4)}
{/* Expandable Content */} {isExpanded && ( -
+
{/* Content */} {result.content && result.content.map((content, contentIndex) => (
-
Content ({content.type})
-
+
+ Content ({content.type}) +
+
{content.text}
@@ -197,23 +198,23 @@ export const VectorStoreTester: React.FC = ({ vectorStor {/* Metadata */} {(result.file_id || result.filename || result.attributes) && ( -
-
Metadata
+
+
Metadata
{result.file_id && ( -
+
File ID: {result.file_id}
)} {result.filename && ( -
+
Filename: {result.filename}
)} {result.attributes && Object.keys(result.attributes).length > 0 && ( -
- Attributes: -
+                                            
+ Attributes: +
                                                 {JSON.stringify(result.attributes, null, 2)}
                                               
@@ -228,45 +229,40 @@ export const VectorStoreTester: React.FC = ({ vectorStor })}
) : ( -
No results found
+
No results found
)}
- {index < searchHistory.length - 1 && } + {index < searchHistory.length - 1 && }
))}
)} {isLoading && ( -
- } /> +
+
)}
{/* Input Area */} -
+
-