From ef72e1afd64b81e5f4e745caab97c7d54a14c0f1 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 18 Aug 2026 14:29:06 -0700 Subject: [PATCH] refactor(ui): move the tag and vector store views off tremor (#37311) * refactor(ui): move the search tool, tag and vector store views off tremor Swaps the tremor Button, TextInput, Text, Title, Card, Badge, Accordion and TabGroup usages in the search tools, tag management and vector store views for the shadcn primitives, following the tremor conversion cookbook. Both tab panels in the vector store info view carry keepMounted so the tester's state survives switching to Details and back, and a test pins that contract. tremor's Button renders a bare button element with no type, so inside the antd Forms here the Test Connection button in the create search tool modal and the Cancel buttons in the tag editor and the vector store form were implicit submit buttons. The shadcn Button defaults to type="button", so they can no longer submit, and every button that is meant to submit now carries an explicit type="submit". Clicking Test Connection and Cancel against a live proxy on the merge base already only ran the connection test and only cancelled, so this closes a latent trap rather than changing what the pages do. Decrements the seven no-restricted-imports suppression counts these files no longer need, leaving the antd half of each entry in place for the antd pass. * refactor(ui): use the line tab strip in the vector store detail view The detail view's tabs kept the default pill TabsList, so it no longer matched the underline strip tremor rendered before the swap or the one the vector store list view already uses. * test(ui): pin the tag and vector store form save and cancel buttons Cancel in the tag editor used to submit the form and save the tag because the tremor button carried no type; nothing in the suite failed if it started doing that again. Each form now has a pair of cases: Save Changes and Create still submit, and Cancel leaves the record alone. * fix(ui): keep the reveal toggle on the search tool API key tremor's TextInput drew its own show/hide button whenever the type was password, and the shadcn Input is a plain native input, so the straight prop pass-through silently deleted that affordance from the create search tool form's API key field. Puts it on antd's Input.Password instead, which is what the sibling edit form in the same directory (SearchTools.tsx) already uses for the very same field, so the reveal survives and the two forms behave the same. The file already imports antd, so this adds no import and no suppression. --- ui/litellm-dashboard/eslint-suppressions.json | 7 +- .../_components/tag_info.integration.test.tsx | 13 ++ .../tag-management/_components/tag_info.tsx | 149 +++++++++--------- .../_components/CreateVectorStore.tsx | 58 ++++--- .../_components/VectorStoreForm.test.tsx | 44 +++++- .../_components/vector_store_info.test.tsx | 47 +++++- .../_components/vector_store_info.tsx | 133 ++++++++-------- 7 files changed, 282 insertions(+), 169 deletions(-) diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 46b6ec9b9ab..df6fd1ef586 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1406,7 +1406,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 3 + "count": 1 }, "react-hooks/set-state-in-effect": { "count": 1 @@ -1513,7 +1513,7 @@ }, "src/app/(dashboard)/vector-stores/_components/CreateVectorStore.tsx": { "no-restricted-imports": { - "count": 3 + "count": 2 } }, "src/app/(dashboard)/vector-stores/_components/S3VectorsConfig.tsx": { @@ -1541,9 +1541,6 @@ "local/filename-pascal-case": { "count": 1 }, - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tag_info.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tag_info.integration.test.tsx index d502b5f074b..8e81c544041 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tag_info.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tag_info.integration.test.tsx @@ -136,4 +136,17 @@ describe("TagInfoView save payload", () => { expect(mockTagUpdateCall).toHaveBeenCalledWith("sk-test", expected); }); + + it("leaves the tag untouched and returns to the detail view when Cancel is clicked", async () => { + const { user } = await renderEditor(); + + const descriptionInput = screen.getByLabelText("Description"); + await user.clear(descriptionInput); + await user.type(descriptionInput, "abandoned description"); + + await user.click(screen.getByRole("button", { name: "Cancel" })); + + expect(await screen.findByText("Tag Details")).toBeInTheDocument(); + expect(mockTagUpdateCall).not.toHaveBeenCalled(); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tag_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tag_info.tsx index 353dd720811..6db19c6d090 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tag_info.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tag_info.tsx @@ -1,7 +1,6 @@ "use client"; import React, { useState, useEffect } from "react"; -import { Card, Text, Title, Button as TremorButton, Badge } from "@tremor/react"; import { Tooltip, Button as AntdButton } from "antd"; import { z } from "zod/v4"; import { fetchUserModels } from "@/components/organisms/create_key_button"; @@ -14,7 +13,9 @@ import BudgetDurationDropdown from "@/components/common_components/budget_durati import { FieldGroup } from "@/components/shared/form/field"; import { FormField } from "@/components/shared/form/FormField"; import { MultiSelect } from "@/components/shared/MultiSelect"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardTitle } from "@/components/ui/card"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; @@ -220,11 +221,11 @@ const TagInfoView: React.FC = ({ tagId, onClose, accessToken,
- +
- Tag Name: + Tag Name: {tagDetails.name} @@ -240,91 +241,97 @@ const TagInfoView: React.FC = ({ tagId, onClose, accessToken, }`} />
- {tagDetails.description || "No description"} +

{tagDetails.description || "No description"}

- {is_admin && !isEditing && setIsEditing(true)}>Edit Tag} + {is_admin && !isEditing && }
{isEditing ? ( - setIsEditing(false)} - onSave={handleSave} - /> + + setIsEditing(false)} + onSave={handleSave} + /> + ) : (
- Tag Details -
-
- Name - {tagDetails.name} -
-
- Description - {tagDetails.description || "-"} -
-
- Allowed Models -
- {!tagDetails.models || tagDetails.models.length === 0 ? ( - All Models - ) : ( - tagDetails.models.map((modelId) => ( - - {tagDetails.model_info?.[modelId] || modelId} - - )) - )} + + Tag Details +
+
+

Name

+

{tagDetails.name}

+
+
+

Description

+

{tagDetails.description || "-"}

+
+
+

Allowed Models

+
+ {!tagDetails.models || tagDetails.models.length === 0 ? ( + All Models + ) : ( + tagDetails.models.map((modelId) => ( + + {tagDetails.model_info?.[modelId] || modelId} + + )) + )} +
+
+
+

Created

+

{tagDetails.created_at ? new Date(tagDetails.created_at).toLocaleString() : "-"}

+
+
+

Last Updated

+

{tagDetails.updated_at ? new Date(tagDetails.updated_at).toLocaleString() : "-"}

-
- Created - {tagDetails.created_at ? new Date(tagDetails.created_at).toLocaleString() : "-"} -
-
- Last Updated - {tagDetails.updated_at ? new Date(tagDetails.updated_at).toLocaleString() : "-"} -
-
+ {tagDetails.litellm_budget_table && ( - Budget & Rate Limits -
- {tagDetails.litellm_budget_table.max_budget !== undefined && - tagDetails.litellm_budget_table.max_budget !== null && ( + + Budget & Rate Limits +
+ {tagDetails.litellm_budget_table.max_budget !== undefined && + tagDetails.litellm_budget_table.max_budget !== null && ( +
+

Max Budget

+

${tagDetails.litellm_budget_table.max_budget}

+
+ )} + {tagDetails.litellm_budget_table.budget_duration && (
- Max Budget - ${tagDetails.litellm_budget_table.max_budget} +

Budget Duration

+

{tagDetails.litellm_budget_table.budget_duration}

)} - {tagDetails.litellm_budget_table.budget_duration && ( -
- Budget Duration - {tagDetails.litellm_budget_table.budget_duration} -
- )} - {tagDetails.litellm_budget_table.tpm_limit !== undefined && - tagDetails.litellm_budget_table.tpm_limit !== null && ( -
- TPM Limit - {tagDetails.litellm_budget_table.tpm_limit.toLocaleString()} -
- )} - {tagDetails.litellm_budget_table.rpm_limit !== undefined && - tagDetails.litellm_budget_table.rpm_limit !== null && ( -
- RPM Limit - {tagDetails.litellm_budget_table.rpm_limit.toLocaleString()} -
- )} -
+ {tagDetails.litellm_budget_table.tpm_limit !== undefined && + tagDetails.litellm_budget_table.tpm_limit !== null && ( +
+

TPM Limit

+

{tagDetails.litellm_budget_table.tpm_limit.toLocaleString()}

+
+ )} + {tagDetails.litellm_budget_table.rpm_limit !== undefined && + tagDetails.litellm_budget_table.rpm_limit !== null && ( +
+

RPM Limit

+

{tagDetails.litellm_budget_table.rpm_limit.toLocaleString()}

+
+ )} +
+
)}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/CreateVectorStore.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/CreateVectorStore.tsx index baa0170c278..2765dd687f7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/CreateVectorStore.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/CreateVectorStore.tsx @@ -1,5 +1,4 @@ import React, { useState } from "react"; -import { Card, Title, Text } from "@tremor/react"; import { Upload, Alert } from "antd"; import { toast } from "@/lib/toast"; import { InboxOutlined } from "@ant-design/icons"; @@ -18,6 +17,7 @@ import { import { Logo } from "@/components/molecules/logo/Logo"; import { Field, FieldGroup, FieldLabel } from "@/components/shared/form/field"; import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; @@ -210,47 +210,53 @@ const CreateVectorStore: React.FC = ({ accessToken, onSu
- Create Vector Store - +

Create Vector Store

+

Upload documents and select a provider to create a new vector store with embedded content. - +

{/* Upload Area */} -
- Step 1: Upload Documents - - Upload one or more documents (PDF, TXT, DOCX, MD). Maximum file size: 50MB per file. - -
- -

- -

-

Click or drag files to this area to upload

-

Support for single or bulk upload. Supported formats: PDF, TXT, DOCX, MD

-
+ +
+

Step 1: Upload Documents

+

+ Upload one or more documents (PDF, TXT, DOCX, MD). Maximum file size: 50MB per file. +

+
+ +

+ +

+

Click or drag files to this area to upload

+

+ Support for single or bulk upload. Supported formats: PDF, TXT, DOCX, MD +

+
+
{/* Documents Table */} {documents.length > 0 && ( -
- Uploaded Documents ({documents.length}) -
- + +
+

Uploaded Documents ({documents.length})

+
+ +
)} {/* Provider Selection and Vector Store Details */} -
+
- Step 2: Configure Vector Store - +

Step 2: Configure Vector Store

+

Choose the provider and optionally provide a name and description for your vector store. - +

@@ -342,7 +348,7 @@ const CreateVectorStore: React.FC = ({ accessToken, onSu {isCreating ? "Creating Vector Store..." : "Create Vector Store"}
-
+ {/* Success Message */} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreForm.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreForm.test.tsx index 94511ea114d..9062dccafd0 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreForm.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreForm.test.tsx @@ -1,17 +1,26 @@ import { render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; -import { CredentialItem } from "@/components/networking"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { CredentialItem, vectorStoreCreateCall } from "@/components/networking"; import { Providers, providerLogoMap } from "@/components/provider_info_helpers"; import { VectorStoreProviders } from "@/components/vector_store_providers"; import VectorStoreForm from "./VectorStoreForm"; vi.mock("@/components/networking"); -const renderForm = () => +vi.mock("@/components/molecules/notifications_manager", () => ({ + __esModule: true, + default: { + success: vi.fn(), + fromBackend: vi.fn(), + }, +})); + +const renderForm = (onCancel: () => void = vi.fn()) => render( ); describe("VectorStoreForm", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it("should render the form when visible", () => { renderForm(); @@ -31,4 +44,27 @@ describe("VectorStoreForm", () => { const logo = screen.getByRole("img", { name: `${VectorStoreProviders.Bedrock} logo` }); expect(logo).toHaveAttribute("src", providerLogoMap[Providers.Bedrock]); }); + + it("creates the vector store when Create is clicked on a filled form", async () => { + const user = userEvent.setup(); + renderForm(); + + await user.type(screen.getByLabelText(/Vector Store ID/), "vs-created"); + await user.click(screen.getByRole("button", { name: "Create" })); + + await vi.waitFor(() => expect(vectorStoreCreateCall).toHaveBeenCalledTimes(1)); + expect(vi.mocked(vectorStoreCreateCall).mock.calls[0][1]).toMatchObject({ vector_store_id: "vs-created" }); + }); + + it("cancels without creating the vector store when Cancel is clicked on a filled form", async () => { + const user = userEvent.setup(); + const onCancel = vi.fn(); + renderForm(onCancel); + + await user.type(screen.getByLabelText(/Vector Store ID/), "vs-abandoned"); + await user.click(screen.getByRole("button", { name: "Cancel" })); + + expect(onCancel).toHaveBeenCalledTimes(1); + expect(vectorStoreCreateCall).not.toHaveBeenCalled(); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.test.tsx index b5dc359b328..697d5f9692b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.test.tsx @@ -12,7 +12,21 @@ vi.mock("@/components/networking", () => ({ credentialListCall: vi.fn(), })); -vi.mock("./VectorStoreTester", () => ({ __esModule: true, default: () => null })); +vi.mock("./VectorStoreTester", async () => { + const { useState } = await import("react"); + const VectorStoreTesterStub = () => { + const [searchesRun, setSearchesRun] = useState(0); + return ( +
+ +

Searches run: {searchesRun}

+
+ ); + }; + return { __esModule: true, default: VectorStoreTesterStub }; +}); const mockVectorStoreInfoCall = vi.mocked(vectorStoreInfoCall); const mockCredentialListCall = vi.mocked(credentialListCall); @@ -65,6 +79,37 @@ describe("VectorStoreInfoView", () => { expect(onClose).toHaveBeenCalled(); }); + it("keeps the test panel's search state when switching to Details and back", async () => { + const user = userEvent.setup(); + mockVectorStoreInfoCall.mockResolvedValue({ + vector_store: { + vector_store_id: "vs-1", + vector_store_name: "support-docs-store", + custom_llm_provider: "bedrock", + created_at: "2024-01-01T00:00:00Z", + updated_at: "2024-01-01T00:00:00Z", + }, + }); + render( + , + ); + expect(await screen.findByText("Vector Store ID: vs-1")).toBeInTheDocument(); + + await user.click(screen.getByRole("tab", { name: "Test Vector Store" })); + await user.click(screen.getByRole("button", { name: "Run search" })); + expect(screen.getByText("Searches run: 1")).toBeInTheDocument(); + + await user.click(screen.getByRole("tab", { name: "Details" })); + await user.click(screen.getByRole("tab", { name: "Test Vector Store" })); + expect(screen.getByText("Searches run: 1")).toBeInTheDocument(); + }); + it("should show the not-found state when the fetch resolves without a vector store", async () => { mockVectorStoreInfoCall.mockResolvedValue({ vector_store: null }); render( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.tsx index b76b08cf20a..638122a89e7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/vector_store_info.tsx @@ -1,7 +1,5 @@ import React, { useState, useEffect } from "react"; -import { Card, Text, Title, Button, Badge, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react"; -import { CircleHelp } from "lucide-react"; -import { ArrowLeftIcon } from "@heroicons/react/outline"; +import { ArrowLeft, CircleHelp } from "lucide-react"; import { z } from "zod/v4"; import { vectorStoreInfoCall, @@ -17,7 +15,9 @@ import VectorStoreTester from "./VectorStoreTester"; import { toast } from "@/lib/toast"; import { FieldGroup } from "@/components/shared/form/field"; import { FormField } from "@/components/shared/form/FormField"; -import { Button as ShadcnButton } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; import { Combobox, ComboboxContent, @@ -28,6 +28,7 @@ import { } from "@/components/ui/combobox"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { useZodForm } from "@/lib/forms/useZodForm"; @@ -183,13 +184,14 @@ const VectorStoreInfoView: React.FC = ({ if (loadFailed) { return (
- - Vector store not found - +

Vector store not found

+

Vector store {vectorStoreId} could not be loaded. It may have been deleted. - +

); } @@ -202,31 +204,36 @@ const VectorStoreInfoView: React.FC = ({
- - Vector Store ID: {vectorStoreDetails.vector_store_id} - +

Vector Store ID: {vectorStoreDetails.vector_store_id}

+

{vectorStoreDetails.vector_store_description || "No description"} - +

{is_admin && !isEditing && }
- - - Details - Test Vector Store - + + + + Details + + + Test Vector Store + + - - - {isEditing ? ( -
-
- Edit Vector Store -
- + + {isEditing ? ( +
+
+

Edit Vector Store

+
+ +
@@ -287,9 +294,9 @@ const VectorStoreInfoView: React.FC = ({ )} - +

Either select existing credentials OR enter provider credentials below - +

{({ @@ -352,37 +359,39 @@ const VectorStoreInfoView: React.FC = ({
- setIsEditing(false)}> + +
-
+ + +
+ ) : ( +
+
+

Vector Store Details

+ {is_admin && }
- ) : ( -
-
- Vector Store Details - {is_admin && } -
- + +
- ID - {vectorStoreDetails.vector_store_id} +

ID

+

{vectorStoreDetails.vector_store_id}

- Name - {vectorStoreDetails.vector_store_name || "-"} +

Name

+

{vectorStoreDetails.vector_store_name || "-"}

- Description - {vectorStoreDetails.vector_store_description || "-"} +

Description

+

{vectorStoreDetails.vector_store_description || "-"}

- Provider +

Provider

{(() => { const provider = vectorStoreDetails.custom_llm_provider || "bedrock"; @@ -391,41 +400,41 @@ const VectorStoreInfoView: React.FC = ({ return ( <> - {displayName} + {displayName} ); })()}
- Metadata +

Metadata

{metadataString}
- Created - +

Created

+

{vectorStoreDetails.created_at ? new Date(vectorStoreDetails.created_at).toLocaleString() : "-"} - +

- Last Updated - +

Last Updated

+

{vectorStoreDetails.updated_at ? new Date(vectorStoreDetails.updated_at).toLocaleString() : "-"} - +

-
-
- )} - + + +
+ )} +
- - - - - + + + +
); };