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() : "-"} - +

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