From 550301679c35b3060bdf7efea1482451b3d971dc Mon Sep 17 00:00:00 2001 From: cursor Date: Fri, 24 Apr 2026 07:39:11 +0000 Subject: [PATCH] feat(ui): migrate vector_store_management to shadcn Co-authored-by: yuneng-jiang --- .../CreateVectorStore.test.tsx | 64 +- .../CreateVectorStore.tsx | 494 +++++++------ .../DocumentsTable.test.tsx | 20 +- .../DocumentsTable.tsx | 180 ++--- .../S3VectorsConfig.tsx | 276 ++++--- .../TestVectorStoreTab.test.tsx | 36 +- .../TestVectorStoreTab.tsx | 52 +- .../VectorStoreForm.tsx | 608 ++++++++++------ .../VectorStoreSelector.test.tsx | 344 ++++----- .../VectorStoreSelector.tsx | 140 +++- .../VectorStoreTable.test.tsx | 37 +- .../vector_store_info.tsx | 683 +++++++++--------- 12 files changed, 1602 insertions(+), 1332 deletions(-) diff --git a/ui/litellm-dashboard/src/components/vector_store_management/CreateVectorStore.test.tsx b/ui/litellm-dashboard/src/components/vector_store_management/CreateVectorStore.test.tsx index a8ba55b91f6..87cec598fb9 100644 --- a/ui/litellm-dashboard/src/components/vector_store_management/CreateVectorStore.test.tsx +++ b/ui/litellm-dashboard/src/components/vector_store_management/CreateVectorStore.test.tsx @@ -1,4 +1,5 @@ import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { describe, it, expect, vi, beforeEach } from "vitest"; import CreateVectorStore from "./CreateVectorStore"; import * as networking from "../networking"; @@ -206,25 +207,37 @@ describe("CreateVectorStore", () => { }); }); + const selectProvider = async (user: ReturnType, providerLabel: RegExp) => { + const providerSelect = screen.getByRole("combobox"); + await act(async () => { + await user.click(providerSelect); + }); + await screen.findByRole("option", { name: providerLabel }); + // Walk the option list with keyboard — Radix Select closes reliably on Enter + // whereas user.click on an option inside a portal does not always close in jsdom. + const options = screen.getAllByRole("option"); + const index = options.findIndex((o) => providerLabel.test(o.textContent ?? "")); + for (let i = 0; i <= index; i += 1) { + await act(async () => { + await user.keyboard("{ArrowDown}"); + }); + } + // Select highlighted option + await act(async () => { + await user.keyboard("{Enter}"); + }); + // Wait for portal to tear down + await waitFor(() => { + expect(document.body.getAttribute("data-scroll-locked")).toBeNull(); + }); + }; + it("should display S3 Vectors provider-specific fields when selected", async () => { + const user = userEvent.setup(); render(); - // Find and click the provider dropdown - const providerSelect = screen.getByRole("combobox"); + await selectProvider(user, /AWS S3 Vectors/); - await act(async () => { - fireEvent.mouseDown(providerSelect); - }); - - // Wait for dropdown options to appear - await waitFor(() => { - const s3Option = screen.queryByText("AWS S3 Vectors"); - if (s3Option) { - fireEvent.click(s3Option); - } - }); - - // Check if S3-specific fields are displayed await waitFor(() => { expect(screen.queryByText("Vector Bucket Name")).toBeInTheDocument(); expect(screen.queryByText("AWS Region")).toBeInTheDocument(); @@ -233,9 +246,9 @@ describe("CreateVectorStore", () => { }); it("should validate S3 Vectors required fields before submission", async () => { + const user = userEvent.setup(); render(); - // Upload a file first const file = new File(["test content"], "test.pdf", { type: "application/pdf" }); const uploadInput = document.querySelector('input[type="file"]') as HTMLInputElement; @@ -249,28 +262,15 @@ describe("CreateVectorStore", () => { expect(screen.getByText("Uploaded Documents (1)")).toBeInTheDocument(); }); - // Select S3 Vectors provider - const providerSelect = screen.getByRole("combobox"); + await selectProvider(user, /AWS S3 Vectors/); - await act(async () => { - fireEvent.mouseDown(providerSelect); - }); - - await waitFor(() => { - const s3Option = screen.queryByText("AWS S3 Vectors"); - if (s3Option) { - fireEvent.click(s3Option); - } - }); - - // Try to create without filling required fields const createButton = screen.getByRole("button", { name: /Create Vector Store/i }); await act(async () => { fireEvent.click(createButton); }); - // Should show validation warning (mocked message.warning would be called) - // The actual validation happens in the component + // validation runs in the component; just confirm no crash + expect(screen.getByText("Uploaded Documents (1)")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/components/vector_store_management/CreateVectorStore.tsx b/ui/litellm-dashboard/src/components/vector_store_management/CreateVectorStore.tsx index b740c3417ba..3c9152072d1 100644 --- a/ui/litellm-dashboard/src/components/vector_store_management/CreateVectorStore.tsx +++ b/ui/litellm-dashboard/src/components/vector_store_management/CreateVectorStore.tsx @@ -1,10 +1,26 @@ -import React, { useState } from "react"; -// eslint-disable-next-line litellm-ui/no-banned-ui-imports -import { Card, Title, Text } from "@tremor/react"; -import { Upload, Button, Select, Form, Alert, Tooltip, Input } from "antd"; +import React, { useRef, useState } from "react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import MessageManager from "@/components/molecules/message_manager"; -import { Inbox as InboxOutlined, Info as InfoCircleOutlined } from "lucide-react"; -import type { UploadProps } from "antd"; +import { cn } from "@/lib/utils"; +import { Inbox, Info } from "lucide-react"; import { ragIngestCall } from "../networking"; import { DocumentUpload, RAGIngestResponse } from "./types"; import DocumentsTable from "./DocumentsTable"; @@ -18,15 +34,49 @@ import { import NotificationsManager from "../molecules/notifications_manager"; import S3VectorsConfig from "./S3VectorsConfig"; -const { Dragger } = Upload; - interface CreateVectorStoreProps { accessToken: string | null; onSuccess?: (vectorStoreId: string) => void; } +const ACCEPTED_TYPES = [ + "application/pdf", + "text/plain", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/msword", + "text/markdown", +]; + +function FieldLabel({ + children, + tooltip, + htmlFor, + required, +}: { + children: React.ReactNode; + tooltip: string; + htmlFor?: string; + required?: boolean; +}) { + return ( + + ); +} + const CreateVectorStore: React.FC = ({ accessToken, onSuccess }) => { - const [form] = Form.useForm(); const [documents, setDocuments] = useState([]); const [isCreating, setIsCreating] = useState(false); const [selectedProvider, setSelectedProvider] = useState("bedrock"); @@ -34,53 +84,35 @@ const CreateVectorStore: React.FC = ({ accessToken, onSu const [vectorStoreDescription, setVectorStoreDescription] = useState(""); const [ingestResults, setIngestResults] = useState([]); const [providerParams, setProviderParams] = useState>({}); + const [isDragActive, setIsDragActive] = useState(false); + const [showSuccessAlert, setShowSuccessAlert] = useState(true); + const fileInputRef = useRef(null); - const uploadProps: UploadProps = { - name: "file", - multiple: true, - accept: ".pdf,.txt,.docx,.md,.doc", - beforeUpload: (file) => { - const isValidType = [ - "application/pdf", - "text/plain", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/msword", - "text/markdown", - ].includes(file.type); - - if (!isValidType) { - MessageManager.error(`${file.name} is not a supported file type. Please upload PDF, TXT, DOCX, or MD files.`); - return Upload.LIST_IGNORE; + const acceptFiles = (files: FileList | File[]) => { + const accepted: DocumentUpload[] = []; + for (const file of Array.from(files)) { + if (!ACCEPTED_TYPES.includes(file.type)) { + MessageManager.error( + `${file.name} is not a supported file type. Please upload PDF, TXT, DOCX, or MD files.`, + ); + continue; } - - const isLt50M = file.size / 1024 / 1024 < 50; - if (!isLt50M) { + if (file.size / 1024 / 1024 >= 50) { MessageManager.error(`${file.name} must be smaller than 50MB!`); - return Upload.LIST_IGNORE; + continue; } - - const newDoc: DocumentUpload = { - uid: file.uid, + accepted.push({ + uid: `${file.name}-${file.size}-${file.lastModified}-${Math.random()}`, name: file.name, status: "done", size: file.size, type: file.type, originFileObj: file, - }; - - setDocuments((prev) => [...prev, newDoc]); - return false; // Prevent auto upload - }, - onRemove: (file) => { - setDocuments((prev) => prev.filter((doc) => doc.uid !== file.uid)); - }, - fileList: documents.map((doc) => ({ - uid: doc.uid, - name: doc.name, - status: doc.status, - size: doc.size, - })), - showUploadList: false, // We'll use our custom table + }); + } + if (accepted.length > 0) { + setDocuments((prev) => [...prev, ...accepted]); + } }; const handleRemoveDocument = (uid: string) => { @@ -98,7 +130,6 @@ const CreateVectorStore: React.FC = ({ accessToken, onSu return; } - // Validate provider-specific required fields const requiredFields = getProviderSpecificFields(selectedProvider).filter((field) => field.required); for (const field of requiredFields) { if (!providerParams[field.name]) { @@ -107,7 +138,6 @@ const CreateVectorStore: React.FC = ({ accessToken, onSu } } - // S3 Vectors specific validation if (selectedProvider === "s3_vectors") { if (providerParams.vector_bucket_name && providerParams.vector_bucket_name.length < 3) { MessageManager.warning("Vector bucket name must be at least 3 characters"); @@ -129,13 +159,11 @@ const CreateVectorStore: React.FC = ({ accessToken, onSu let vectorStoreId: string | undefined; try { - // Ingest each document for (const doc of documents) { if (!doc.originFileObj) continue; - // Update document status to uploading setDocuments((prev) => - prev.map((d) => (d.uid === doc.uid ? { ...d, status: "uploading" as const } : d)) + prev.map((d) => (d.uid === doc.uid ? { ...d, status: "uploading" as const } : d)), ); try { @@ -143,43 +171,40 @@ const CreateVectorStore: React.FC = ({ accessToken, onSu accessToken, doc.originFileObj, selectedProvider, - vectorStoreId, // Use the same vector store ID for subsequent uploads + vectorStoreId, vectorStoreName || undefined, vectorStoreDescription || undefined, - providerParams + providerParams, ); - // Store the vector store ID from the first successful ingest if (!vectorStoreId && result.vector_store_id) { vectorStoreId = result.vector_store_id; } results.push(result); - // Update document status to done setDocuments((prev) => - prev.map((d) => (d.uid === doc.uid ? { ...d, status: "done" as const } : d)) + prev.map((d) => (d.uid === doc.uid ? { ...d, status: "done" as const } : d)), ); } catch (error) { console.error(`Error ingesting ${doc.name}:`, error); - // Update document status to error setDocuments((prev) => - prev.map((d) => (d.uid === doc.uid ? { ...d, status: "error" as const } : d)) + prev.map((d) => (d.uid === doc.uid ? { ...d, status: "error" as const } : d)), ); - throw error; // Stop processing on first error + throw error; } } setIngestResults(results); + setShowSuccessAlert(true); NotificationsManager.success( - `Successfully created vector store with ${results.length} document(s). Vector Store ID: ${vectorStoreId}` + `Successfully created vector store with ${results.length} document(s). Vector Store ID: ${vectorStoreId}`, ); if (onSuccess && vectorStoreId) { onSuccess(vectorStoreId); } - // Clear documents after successful creation setTimeout(() => { setDocuments([]); setIngestResults([]); @@ -192,142 +217,208 @@ const CreateVectorStore: React.FC = ({ accessToken, onSu } }; + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragActive(false); + if (e.dataTransfer?.files?.length) { + acceptFiles(e.dataTransfer.files); + } + }; + + const renderProviderField = (field: VectorStoreFieldConfig) => { + const fieldId = `provider-field-${field.name}`; + if (field.type === "select") { + return ( +
+ + {field.label} + + + setProviderParams((prev) => ({ ...prev, [field.name]: e.target.value })) + } + placeholder={field.placeholder} + className="rounded-md" + /> +
+ ); + } + + return ( +
+ + {field.label} + + + setProviderParams((prev) => ({ ...prev, [field.name]: e.target.value })) + } + placeholder={field.placeholder} + className="rounded-md" + /> +
+ ); + }; + return (
- 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 - +

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

-

+

+
fileInputRef.current?.click()} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") fileInputRef.current?.click(); + }} + onDragOver={(e) => { + e.preventDefault(); + setIsDragActive(true); + }} + onDragLeave={() => setIsDragActive(false)} + onDrop={handleDrop} + className={cn( + "flex flex-col items-center justify-center gap-2 border-2 border-dashed rounded-md p-8 cursor-pointer transition-colors", + isDragActive + ? "border-primary bg-accent/50" + : "border-border hover:border-primary", + )} + > + +

Click or drag files to this area to upload

+

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

- + { + if (e.target.files?.length) acceptFiles(e.target.files); + if (fileInputRef.current) fileInputRef.current.value = ""; + }} + /> +
- {/* 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. - +

-
- - Vector Store Name{" "} - - - - - } - > +
+
+ + Vector Store Name + setVectorStoreName(e.target.value)} placeholder="e.g., Product Documentation, Customer Support KB" - size="large" className="rounded-md" /> - +
- - Description{" "} - - - - - } - > - + + Description + +