feat(ui): show vector store indexes on the Vector Stores page (#36306)

* feat(ui): show vector store indexes on the Vector Stores page

Adds a proxy-admin-only Indexes tab listing rows from GET /v1/indexes:
index name, backing vector store, provider index, creator, and created
date. The tab is hidden for non proxy-admin roles to match the
endpoint's gate, and data loads lazily on first visit.

* feat(ui): link index rows to their vector store and creator

Vector Store cells open the store's info view when the name resolves to
a registered store, and Created By cells deep link to the users page via
a new userDetailHref, with the users page reading the user query param
through nuqs so the link is shareable.

* feat(ui): link docs and note supported providers on Indexes tab

* fix(ui): show not-found state instead of infinite loading for missing vector store
This commit is contained in:
ryan-crabbe-berri 2026-08-10 15:24:20 -07:00 committed by GitHub
parent 76ad1c319d
commit ec9ab43d20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 634 additions and 31 deletions

View file

@ -1,10 +1,11 @@
/* @vitest-environment jsdom */
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../../../tests/test-utils";
import ViewUserDashboard from "./view_users";
const userListCall = vi.fn();
@ -78,7 +79,7 @@ const defaultProps = {
};
const renderDashboard = () =>
render(
renderWithProviders(
<QueryClientProvider client={createQueryClient()}>
<ViewUserDashboard {...defaultProps} />
</QueryClientProvider>,

View file

@ -1,4 +1,5 @@
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
import { parseAsString, useQueryState } from "nuqs";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "antd";
@ -72,7 +73,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
const [selectionMode, setSelectionMode] = useState(false);
const [isBulkEditModalVisible, setIsBulkEditModalVisible] = useState(false);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [selectedUserId, setSelectedUserId] = useQueryState("user", parseAsString.withOptions({ history: "push" }));
const [openInEditMode, setOpenInEditMode] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
@ -139,15 +140,18 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
setRowSelection({});
}, []);
const handleUserClick = useCallback((userId: string, openInEdit: boolean = false) => {
setSelectedUserId(userId);
setOpenInEditMode(openInEdit);
}, []);
const handleUserClick = useCallback(
(userId: string, openInEdit: boolean = false) => {
void setSelectedUserId(userId);
setOpenInEditMode(openInEdit);
},
[setSelectedUserId],
);
const handleCloseUserInfo = useCallback(() => {
setSelectedUserId(null);
void setSelectedUserId(null);
setOpenInEditMode(false);
}, []);
}, [setSelectedUserId]);
const handleDelete = useCallback((user: UserInfo) => {
setUserToDelete(user);

View file

@ -0,0 +1,102 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { indexesListCall } from "@/components/networking";
import { VectorStore } from "@/components/vector_store_management/types";
import IndexesTable from "./IndexesTable";
export interface VectorStoreIndex {
id: string;
index_name: string;
litellm_params: {
vector_store_index: string;
vector_store_name: string;
};
index_info?: Record<string, unknown> | null;
created_at?: string | null;
created_by?: string | null;
updated_at?: string | null;
updated_by?: string | null;
}
interface IndexesTabProps {
accessToken: string | null;
vectorStores: VectorStore[];
onViewVectorStore: (vectorStoreId: string) => void;
}
const IndexesTab: React.FC<IndexesTabProps> = ({ accessToken, vectorStores, onViewVectorStore }) => {
const [indexes, setIndexes] = useState<VectorStoreIndex[]>([]);
const [isLoading, setIsLoading] = useState(true);
const vectorStoreIdsByName = useMemo(
() =>
new Map(
vectorStores.flatMap((store) =>
store.vector_store_name ? [[store.vector_store_name, store.vector_store_id] as const] : [],
),
),
[vectorStores],
);
const resolveVectorStoreId = useCallback((name: string) => vectorStoreIdsByName.get(name), [vectorStoreIdsByName]);
useEffect(() => {
const fetchIndexes = async () => {
if (!accessToken) {
setIsLoading(false);
return;
}
try {
const response = await indexesListCall(accessToken);
setIndexes(response.data || []);
} catch (error) {
console.error("Error fetching indexes:", error);
NotificationsManager.fromBackend("Error fetching indexes: " + error);
} finally {
setIsLoading(false);
}
};
fetchIndexes();
}, [accessToken]);
return (
<div className="w-full">
<p className="mb-4 text-sm text-muted-foreground">
Vector store indexes registered on this proxy via the <code>/v1/indexes</code> API. See the{" "}
<a
href="https://docs.litellm.ai/docs/providers/azure_ai/azure_ai_vector_stores_passthrough"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
vector store index docs
</a>{" "}
for how this works. Index passthrough is supported for Azure AI Search and Milvus today; support for more
providers can be added, so please{" "}
<a
href="https://github.com/BerriAI/litellm/issues"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
file a GitHub issue
</a>{" "}
if you want your provider supported.
</p>
<div className="grid grid-cols-1 gap-2 pt-2 pb-2 w-full">
<IndexesTable
data={indexes}
isLoading={isLoading}
resolveVectorStoreId={resolveVectorStoreId}
onViewVectorStore={onViewVectorStore}
/>
</div>
</div>
);
};
export default IndexesTab;

View file

@ -0,0 +1,100 @@
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import type { VectorStoreIndex } from "./IndexesTab";
import IndexesTable from "./IndexesTable";
vi.mock("next/navigation", async () => ({
...(await vi.importActual("next/navigation")),
useRouter: () => ({ push: vi.fn() }),
}));
const newerIndex: VectorStoreIndex = {
id: "idx-newer",
index_name: "newer-index",
litellm_params: { vector_store_index: "provider-newer", vector_store_name: "newer-store" },
created_by: "admin@example.com",
created_at: "2024-02-20T10:30:00Z",
};
const olderIndex: VectorStoreIndex = {
id: "idx-older",
index_name: "older-index",
litellm_params: { vector_store_index: "provider-older", vector_store_name: "older-store" },
created_by: "admin@example.com",
created_at: "2024-01-10T09:15:00Z",
};
const undatedIndex: VectorStoreIndex = {
id: "idx-undated",
index_name: "undated-index",
litellm_params: { vector_store_index: "provider-undated", vector_store_name: "undated-store" },
created_by: null,
created_at: null,
};
const noResolve = () => undefined;
describe("IndexesTable", () => {
it("should display the empty state when no indexes are registered", () => {
render(<IndexesTable data={[]} resolveVectorStoreId={noResolve} onViewVectorStore={vi.fn()} />);
expect(screen.getByText("No indexes registered yet")).toBeInTheDocument();
});
it("should render index rows with dash fallbacks for missing created_by and created_at", () => {
render(
<IndexesTable data={[newerIndex, undatedIndex]} resolveVectorStoreId={noResolve} onViewVectorStore={vi.fn()} />,
);
expect(screen.getByText("newer-index")).toBeInTheDocument();
expect(screen.getByText("newer-store")).toBeInTheDocument();
expect(screen.getByText("provider-newer")).toBeInTheDocument();
expect(screen.getByText("admin@example.com")).toBeInTheDocument();
const undatedRow = screen.getByText("undated-index").closest("tr");
expect(undatedRow).not.toBeNull();
expect(within(undatedRow as HTMLElement).getAllByText("-")).toHaveLength(2);
});
it("should sort by created_at descending by default", () => {
render(
<IndexesTable data={[olderIndex, newerIndex]} resolveVectorStoreId={noResolve} onViewVectorStore={vi.fn()} />,
);
const rows = screen.getAllByRole("row").slice(1);
expect(within(rows[0]).getByText("newer-index")).toBeInTheDocument();
expect(within(rows[1]).getByText("older-index")).toBeInTheDocument();
});
it("should call onViewVectorStore with the resolved id when the vector store cell is clicked", async () => {
const user = userEvent.setup();
const onViewVectorStore = vi.fn();
render(
<IndexesTable
data={[newerIndex]}
resolveVectorStoreId={(name) => (name === "newer-store" ? "vs-newer" : undefined)}
onViewVectorStore={onViewVectorStore}
/>,
);
await user.click(screen.getByRole("button", { name: "newer-store" }));
expect(onViewVectorStore).toHaveBeenCalledWith("vs-newer");
});
it("should render an unresolvable vector store name as plain text without a clickable cell", () => {
render(<IndexesTable data={[newerIndex]} resolveVectorStoreId={noResolve} onViewVectorStore={vi.fn()} />);
expect(screen.getByText("newer-store")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "newer-store" })).not.toBeInTheDocument();
});
it("should link created_by to the user detail deep link", () => {
render(<IndexesTable data={[newerIndex]} resolveVectorStoreId={noResolve} onViewVectorStore={vi.fn()} />);
const link = screen.getByRole("link", { name: "admin@example.com" });
expect(link.getAttribute("href")).toMatch(/\/users\?user=admin%40example\.com$/);
});
it("should keep the dash fallback and render no link for a null created_by", () => {
render(<IndexesTable data={[undatedIndex]} resolveVectorStoreId={noResolve} onViewVectorStore={vi.fn()} />);
const row = screen.getByText("undated-index").closest("tr");
expect(row).not.toBeNull();
expect(within(row as HTMLElement).queryByRole("link")).not.toBeInTheDocument();
expect(within(row as HTMLElement).getAllByText("-").length).toBeGreaterThan(0);
});
});

View file

@ -0,0 +1,62 @@
"use client";
import { SortingState } from "@tanstack/react-table";
import { Inbox } from "lucide-react";
import React, { useMemo, useState } from "react";
import { DataTable } from "@/components/shared/DataTable";
import type { VectorStoreIndex } from "./IndexesTab";
import { getIndexesTableColumns } from "./IndexesTableColumns";
interface IndexesTableProps {
data: VectorStoreIndex[];
resolveVectorStoreId: (name: string) => string | undefined;
onViewVectorStore: (vectorStoreId: string) => void;
isLoading?: boolean;
}
const DEFAULT_SORTING: SortingState = [{ id: "created_at", desc: true }];
function EmptyState() {
return (
<div className="flex flex-col items-center gap-1 py-6">
<div className="mb-1 flex size-10 items-center justify-center rounded-lg bg-muted">
<Inbox className="size-5 text-muted-foreground" />
</div>
<div className="text-sm font-medium text-foreground">No indexes registered yet</div>
<div className="text-sm text-muted-foreground">Indexes registered on this proxy will appear here.</div>
</div>
);
}
const IndexesTable: React.FC<IndexesTableProps> = ({
data,
resolveVectorStoreId,
onViewVectorStore,
isLoading = false,
}) => {
const [sorting, setSorting] = useState<SortingState>(DEFAULT_SORTING);
const columns = useMemo(
() => getIndexesTableColumns({ resolveVectorStoreId, onViewVectorStore }),
[resolveVectorStoreId, onViewVectorStore],
);
return (
<DataTable
data={data}
columns={columns}
getRowId={(row, index) => row.id || String(index)}
sortingMode="client"
sorting={sorting}
onSortingChange={setSorting}
isLoading={isLoading}
loadingMessage="Loading indexes…"
noDataMessage={<EmptyState />}
size="compact"
/>
);
};
export default IndexesTable;

View file

@ -0,0 +1,108 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
import { DataTableSortHeader } from "@/components/shared/DataTable";
import { DateCell, IdentityCell } from "@/components/shared/table_cells";
import { userDetailHref } from "@/utils/entityLinks";
import type { VectorStoreIndex } from "./IndexesTab";
interface IndexesTableColumnsDeps {
resolveVectorStoreId: (name: string) => string | undefined;
onViewVectorStore: (vectorStoreId: string) => void;
}
export const getIndexesTableColumns = ({
resolveVectorStoreId,
onViewVectorStore,
}: IndexesTableColumnsDeps): ColumnDef<VectorStoreIndex>[] => [
{
id: "index_name",
accessorKey: "index_name",
meta: { title: "Index Name" },
header: ({ column }) => <DataTableSortHeader column={column} title="Index Name" />,
size: 220,
enableSorting: true,
cell: ({ row }) => (
<span className="block max-w-60 truncate text-sm font-medium" title={row.original.index_name}>
{row.original.index_name || "-"}
</span>
),
},
{
id: "vector_store_name",
accessorFn: (row) => row.litellm_params.vector_store_name,
meta: { title: "Vector Store" },
header: ({ column }) => <DataTableSortHeader column={column} title="Vector Store" />,
size: 200,
enableSorting: true,
cell: ({ row }) => {
const name = row.original.litellm_params.vector_store_name;
const vectorStoreId = name ? resolveVectorStoreId(name) : undefined;
if (vectorStoreId) {
return (
<IdentityCell
title={name}
titleClassName="font-normal"
className="max-w-60"
onClick={() => onViewVectorStore(vectorStoreId)}
/>
);
}
return (
<span className="block max-w-60 truncate text-sm" title={name}>
{name || "-"}
</span>
);
},
},
{
id: "vector_store_index",
accessorFn: (row) => row.litellm_params.vector_store_index,
meta: { title: "Provider Index" },
header: "Provider Index",
size: 220,
enableSorting: false,
cell: ({ row }) => (
<span
className="block max-w-60 truncate font-mono text-xs"
title={row.original.litellm_params.vector_store_index}
>
{row.original.litellm_params.vector_store_index || "-"}
</span>
),
},
{
id: "created_by",
accessorKey: "created_by",
meta: { title: "Created By" },
header: "Created By",
size: 160,
enableSorting: false,
cell: ({ row }) => {
const createdBy = row.original.created_by;
if (createdBy) {
return (
<IdentityCell
title={createdBy}
titleClassName="font-normal"
className="max-w-48"
href={userDetailHref(createdBy)}
/>
);
}
return <span className="block max-w-48 truncate text-sm">-</span>;
},
},
{
id: "created_at",
accessorKey: "created_at",
sortingFn: "datetime",
meta: { title: "Created At" },
header: ({ column }) => <DataTableSortHeader column={column} title="Created At" />,
size: 150,
enableSorting: true,
cell: ({ row }) => <DateCell value={row.original.created_at} precision="date" />,
},
];

View file

@ -1,8 +1,8 @@
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { vectorStoreListCall } from "@/components/networking";
import { credentialListCall, indexesListCall, vectorStoreListCall } from "@/components/networking";
import VectorStoreManagement from "./index";
@ -10,6 +10,7 @@ vi.mock("@/components/networking", () => ({
vectorStoreListCall: vi.fn(),
vectorStoreDeleteCall: vi.fn(),
credentialListCall: vi.fn(),
indexesListCall: vi.fn(),
}));
vi.mock("./VectorStoreTable", () => ({
@ -20,11 +21,18 @@ vi.mock("./VectorStoreTable", () => ({
}));
vi.mock("./VectorStoreForm", () => ({ __esModule: true, default: () => null }));
vi.mock("./vector_store_info", () => ({ __esModule: true, default: () => null }));
vi.mock("./vector_store_info", () => ({
__esModule: true,
default: ({ vectorStoreId }: { vectorStoreId: string }) => (
<div data-testid="vector-store-info-view">{vectorStoreId}</div>
),
}));
vi.mock("./CreateVectorStore", () => ({ __esModule: true, default: () => null }));
vi.mock("./TestVectorStoreTab", () => ({ __esModule: true, default: () => null }));
const mockVectorStoreListCall = vi.mocked(vectorStoreListCall);
const mockCredentialListCall = vi.mocked(credentialListCall);
const mockIndexesListCall = vi.mocked(indexesListCall);
const openManageTab = async (user: ReturnType<typeof userEvent.setup>) => {
await user.click(screen.getByRole("tab", { name: "Manage Vector Stores" }));
@ -60,3 +68,90 @@ describe("VectorStoreManagement loading state", () => {
expect(mockVectorStoreListCall).toHaveBeenCalledWith("sk-test");
});
});
describe("VectorStoreManagement Indexes tab", () => {
beforeEach(() => {
vi.clearAllMocks();
mockVectorStoreListCall.mockResolvedValue({ data: [] });
mockCredentialListCall.mockResolvedValue({ credentials: [] });
});
it("should render fetched indexes for a proxy admin after the Indexes tab is clicked", async () => {
const user = userEvent.setup();
mockIndexesListCall.mockResolvedValue({
object: "list",
data: [
{
id: "idx-1",
index_name: "support-docs-index",
litellm_params: { vector_store_index: "pinecone-support-docs", vector_store_name: "support-docs-store" },
},
],
});
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" />);
await user.click(screen.getByRole("tab", { name: "Indexes" }));
expect(await screen.findByText("support-docs-index")).toBeInTheDocument();
expect(screen.getByText("support-docs-store")).toBeInTheDocument();
expect(mockIndexesListCall).toHaveBeenCalledWith("sk-test");
});
it("should not render the Indexes tab for an Admin Viewer", async () => {
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin Viewer" />);
await waitFor(() => expect(mockVectorStoreListCall).toHaveBeenCalledWith("sk-test"));
expect(screen.getByRole("tab", { name: "Manage Vector Stores" })).toBeInTheDocument();
expect(screen.queryByRole("tab", { name: "Indexes" })).not.toBeInTheDocument();
});
it("should swap to the vector store info view when an index's vector store name is clicked", async () => {
const user = userEvent.setup();
mockVectorStoreListCall.mockResolvedValue({
data: [
{
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",
},
],
});
mockIndexesListCall.mockResolvedValue({
object: "list",
data: [
{
id: "idx-1",
index_name: "support-docs-index",
litellm_params: { vector_store_index: "pinecone-support-docs", vector_store_name: "support-docs-store" },
},
],
});
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" />);
await user.click(screen.getByRole("tab", { name: "Indexes" }));
await user.click(await screen.findByRole("button", { name: "support-docs-store" }));
expect(await screen.findByTestId("vector-store-info-view")).toHaveTextContent("vs-1");
expect(screen.queryByText("Vector Store Management")).not.toBeInTheDocument();
});
it("should link to the feature docs and a GitHub issue for unsupported providers on the Indexes tab", async () => {
const user = userEvent.setup();
mockIndexesListCall.mockResolvedValue({ object: "list", data: [] });
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" />);
await user.click(screen.getByRole("tab", { name: "Indexes" }));
expect(screen.getByRole("link", { name: "vector store index docs" })).toHaveAttribute(
"href",
"https://docs.litellm.ai/docs/providers/azure_ai/azure_ai_vector_stores_passthrough",
);
expect(screen.getByRole("link", { name: "file a GitHub issue" })).toHaveAttribute(
"href",
"https://github.com/BerriAI/litellm/issues",
);
expect(screen.getByText(/supported for Azure AI Search and Milvus today/)).toBeInTheDocument();
});
it("should not call indexesListCall until the Indexes tab is clicked", async () => {
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" />);
await waitFor(() => expect(mockVectorStoreListCall).toHaveBeenCalledWith("sk-test"));
expect(screen.getByRole("tab", { name: "Indexes" })).toBeInTheDocument();
expect(mockIndexesListCall).not.toHaveBeenCalled();
});
});

View file

@ -13,7 +13,8 @@ import DeleteResourceModal from "@/components/common_components/DeleteResourceMo
import VectorStoreInfoView from "./vector_store_info";
import CreateVectorStore from "./CreateVectorStore";
import TestVectorStoreTab from "./TestVectorStoreTab";
import { isAdminRole } from "@/utils/roles";
import IndexesTab from "./IndexesTab";
import { isAdminRole, isProxyAdminRole } from "@/utils/roles";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@ -163,6 +164,11 @@ const VectorStoreManagement: React.FC<VectorStoreProps> = ({ accessToken, userID
<TabsTrigger value="test" className="flex-none rounded-none px-4 py-2">
Test Vector Store
</TabsTrigger>
{isProxyAdminRole(userRole || "") && (
<TabsTrigger value="indexes" className="flex-none rounded-none px-4 py-2">
Indexes
</TabsTrigger>
)}
</TabsList>
<TabsContent keepMounted={hasVisited("create")} value="create">
@ -188,6 +194,12 @@ const VectorStoreManagement: React.FC<VectorStoreProps> = ({ accessToken, userID
<TabsContent keepMounted={hasVisited("test")} value="test">
<TestVectorStoreTab accessToken={accessToken} vectorStores={vectorStores} />
</TabsContent>
{isProxyAdminRole(userRole || "") && (
<TabsContent keepMounted={hasVisited("indexes")} value="indexes">
<IndexesTab accessToken={accessToken} vectorStores={vectorStores} onViewVectorStore={handleView} />
</TabsContent>
)}
</Tabs>
{/* Create Vector Store Modal */}

View file

@ -0,0 +1,81 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { credentialListCall, vectorStoreInfoCall } from "@/components/networking";
import VectorStoreInfoView from "./vector_store_info";
vi.mock("@/components/networking", () => ({
vectorStoreInfoCall: vi.fn(),
vectorStoreUpdateCall: vi.fn(),
credentialListCall: vi.fn(),
}));
vi.mock("./VectorStoreTester", () => ({ __esModule: true, default: () => null }));
const mockVectorStoreInfoCall = vi.mocked(vectorStoreInfoCall);
const mockCredentialListCall = vi.mocked(credentialListCall);
describe("VectorStoreInfoView", () => {
beforeEach(() => {
vi.clearAllMocks();
mockCredentialListCall.mockResolvedValue({ credentials: [] });
});
it("should render the store details once the fetch resolves", async () => {
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(
<VectorStoreInfoView
vectorStoreId="vs-1"
onClose={vi.fn()}
accessToken="sk-test"
is_admin={true}
editVectorStore={false}
/>,
);
expect(await screen.findByText("Vector Store ID: vs-1")).toBeInTheDocument();
});
it("should show a not-found state with a working back button when the fetch fails instead of loading forever", async () => {
const user = userEvent.setup();
const onClose = vi.fn();
mockVectorStoreInfoCall.mockRejectedValue(new Error("Vector store not found"));
render(
<VectorStoreInfoView
vectorStoreId="vs-gone"
onClose={onClose}
accessToken="sk-test"
is_admin={true}
editVectorStore={false}
/>,
);
expect(await screen.findByText("Vector store not found")).toBeInTheDocument();
expect(screen.getByText(/vs-gone could not be loaded/)).toBeInTheDocument();
expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /Back to Vector Stores/ }));
expect(onClose).toHaveBeenCalled();
});
it("should show the not-found state when the fetch resolves without a vector store", async () => {
mockVectorStoreInfoCall.mockResolvedValue({ vector_store: null });
render(
<VectorStoreInfoView
vectorStoreId="vs-gone"
onClose={vi.fn()}
accessToken="sk-test"
is_admin={true}
editVectorStore={false}
/>,
);
expect(await screen.findByText("Vector store not found")).toBeInTheDocument();
});
});

View file

@ -33,6 +33,7 @@ const VectorStoreInfoView: React.FC<VectorStoreInfoViewProps> = ({
}) => {
const [form] = Form.useForm();
const [vectorStoreDetails, setVectorStoreDetails] = useState<VectorStore | null>(null);
const [loadFailed, setLoadFailed] = useState<boolean>(false);
const [isEditing, setIsEditing] = useState<boolean>(editVectorStore);
const [metadataString, setMetadataString] = useState<string>("{}");
const [credentials, setCredentials] = useState<CredentialItem[]>([]);
@ -40,31 +41,35 @@ const VectorStoreInfoView: React.FC<VectorStoreInfoViewProps> = ({
const fetchVectorStoreDetails = async () => {
if (!accessToken) return;
try {
setLoadFailed(false);
const response = await vectorStoreInfoCall(accessToken, vectorStoreId);
if (response && response.vector_store) {
setVectorStoreDetails(response.vector_store);
if (!response || !response.vector_store) {
setLoadFailed(true);
return;
}
setVectorStoreDetails(response.vector_store);
// If metadata exists and is an object, stringify it for display/editing
if (response.vector_store.vector_store_metadata) {
const metadata =
typeof response.vector_store.vector_store_metadata === "string"
? JSON.parse(response.vector_store.vector_store_metadata)
: response.vector_store.vector_store_metadata;
setMetadataString(JSON.stringify(metadata, null, 2));
}
// If metadata exists and is an object, stringify it for display/editing
if (response.vector_store.vector_store_metadata) {
const metadata =
typeof response.vector_store.vector_store_metadata === "string"
? JSON.parse(response.vector_store.vector_store_metadata)
: response.vector_store.vector_store_metadata;
setMetadataString(JSON.stringify(metadata, null, 2));
}
if (editVectorStore) {
form.setFieldsValue({
vector_store_id: response.vector_store.vector_store_id,
custom_llm_provider: response.vector_store.custom_llm_provider,
vector_store_name: response.vector_store.vector_store_name,
vector_store_description: response.vector_store.vector_store_description,
});
}
if (editVectorStore) {
form.setFieldsValue({
vector_store_id: response.vector_store.vector_store_id,
custom_llm_provider: response.vector_store.custom_llm_provider,
vector_store_name: response.vector_store.vector_store_name,
vector_store_description: response.vector_store.vector_store_description,
});
}
} catch (error) {
console.error("Error fetching vector store details:", error);
NotificationsManager.fromBackend("Error fetching vector store details: " + error);
setLoadFailed(true);
}
};
@ -113,6 +118,20 @@ const VectorStoreInfoView: React.FC<VectorStoreInfoViewProps> = ({
}
};
if (loadFailed) {
return (
<div className="p-4 max-w-full">
<Button icon={ArrowLeftIcon} variant="light" className="mb-4" onClick={onClose}>
Back to Vector Stores
</Button>
<Title>Vector store not found</Title>
<Text className="text-gray-500">
Vector store {vectorStoreId} could not be loaded. It may have been deleted.
</Text>
</div>
);
}
if (!vectorStoreDetails) {
return <div>Loading...</div>;
}

View file

@ -66,6 +66,7 @@ import type {
} from "@/app/(dashboard)/caching/_components/coordination_redis_settings/types";
import { MCP_TOOLS_PREVIEW_FORBIDDEN_MESSAGE } from "./mcp_tools/constants";
import type { ComplexityRouterConfigPayload } from "./add_model/build_complexity_router_config";
import type { VectorStoreIndex } from "@/app/(dashboard)/vector-stores/_components/IndexesTab";
import type { RoutingDecision } from "./view_logs/LogDetailsDrawer/RoutingDecisionCard";
import {
createApiClient,
@ -5622,6 +5623,20 @@ export const vectorStoreListCall = async (
}
};
export interface IndexesListResponse {
object: string;
data: VectorStoreIndex[];
}
export const indexesListCall = async (accessToken: string): Promise<IndexesListResponse> => {
try {
return await apiClient.get<IndexesListResponse>(`/v1/indexes`, { accessToken });
} catch (error) {
console.error("Error listing indexes:", error);
throw error;
}
};
export const vectorStoreDeleteCall = async (accessToken: string, vectorStoreId: string): Promise<void> => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/vector_store/delete` : `/vector_store/delete`;

View file

@ -7,3 +7,7 @@ export function teamDetailHref(teamId: string): string {
export function keyDetailHref(keyToken: string): string {
return `${migratedHref("api-keys")}?key=${encodeURIComponent(keyToken)}`;
}
export function userDetailHref(userId: string): string {
return `${migratedHref("users")}?user=${encodeURIComponent(userId)}`;
}