- {isLoadingContents ? (
+ {isLoadingFile ? (
+
+ ) : errorFile ? (
+
+ Error loading file: {errorFile.message}
+
+ ) : isLoadingContents ? (
diff --git a/llama_stack/ui/app/logs/vector-stores/[id]/files/[fileId]/page.test.tsx b/llama_stack/ui/app/logs/vector-stores/[id]/files/[fileId]/page.test.tsx
new file mode 100644
index 000000000..2be26bf3f
--- /dev/null
+++ b/llama_stack/ui/app/logs/vector-stores/[id]/files/[fileId]/page.test.tsx
@@ -0,0 +1,458 @@
+import React from "react";
+import {
+ render,
+ screen,
+ fireEvent,
+ waitFor,
+ act,
+} from "@testing-library/react";
+import "@testing-library/jest-dom";
+import FileDetailPage from "./page";
+import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
+import type {
+ VectorStoreFile,
+ FileContentResponse,
+} from "llama-stack-client/resources/vector-stores/files";
+
+const mockPush = jest.fn();
+const mockParams = {
+ id: "vs_123",
+ fileId: "file_456",
+};
+
+jest.mock("next/navigation", () => ({
+ useParams: () => mockParams,
+ useRouter: () => ({
+ push: mockPush,
+ }),
+}));
+
+const mockClient = {
+ vectorStores: {
+ retrieve: jest.fn(),
+ files: {
+ retrieve: jest.fn(),
+ content: jest.fn(),
+ },
+ },
+};
+
+jest.mock("@/hooks/use-auth-client", () => ({
+ useAuthClient: () => mockClient,
+}));
+
+describe("FileDetailPage", () => {
+ const mockStore: VectorStore = {
+ id: "vs_123",
+ name: "Test Vector Store",
+ created_at: 1710000000,
+ status: "ready",
+ file_counts: { total: 5 },
+ usage_bytes: 1024,
+ metadata: {
+ provider_id: "test_provider",
+ },
+ };
+
+ const mockFile: VectorStoreFile = {
+ id: "file_456",
+ status: "completed",
+ created_at: 1710001000,
+ usage_bytes: 2048,
+ chunking_strategy: { type: "fixed_size" },
+ };
+
+ const mockFileContent: FileContentResponse = {
+ content: [
+ { text: "First chunk of file content." },
+ {
+ text: "Second chunk with more detailed information about the content.",
+ },
+ { text: "Third and final chunk of the file." },
+ ],
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ mockClient.vectorStores.retrieve.mockResolvedValue(mockStore);
+ mockClient.vectorStores.files.retrieve.mockResolvedValue(mockFile);
+ mockClient.vectorStores.files.content.mockResolvedValue(mockFileContent);
+ });
+
+ describe("Loading and Error States", () => {
+ test("renders loading skeleton while fetching store data", async () => {
+ mockClient.vectorStores.retrieve.mockImplementation(
+ () => new Promise(() => {})
+ );
+
+ await act(async () => {
+ await act(async () => {
+ render();
+ });
+ });
+
+ const skeletons = document.querySelectorAll('[data-slot="skeleton"]');
+ expect(skeletons.length).toBeGreaterThan(0);
+ });
+
+ test("renders error message when store API call fails", async () => {
+ const error = new Error("Failed to load store");
+ mockClient.vectorStores.retrieve.mockRejectedValue(error);
+
+ await act(async () => {
+ await act(async () => {
+ render();
+ });
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/Error loading details for ID vs_123/)
+ ).toBeInTheDocument();
+ expect(screen.getByText(/Failed to load store/)).toBeInTheDocument();
+ });
+ });
+
+ test("renders not found when store doesn't exist", async () => {
+ mockClient.vectorStores.retrieve.mockResolvedValue(null);
+
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/No details found for ID: vs_123/)
+ ).toBeInTheDocument();
+ });
+ });
+
+ test("renders file loading skeleton", async () => {
+ mockClient.vectorStores.files.retrieve.mockImplementation(
+ () => new Promise(() => {})
+ );
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(screen.getByText("File: file_456")).toBeInTheDocument();
+ });
+
+ const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
+ expect(skeletons.length).toBeGreaterThan(0);
+ });
+
+ test("renders file error message", async () => {
+ const error = new Error("Failed to load file");
+ mockClient.vectorStores.files.retrieve.mockRejectedValue(error);
+
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Error loading file: Failed to load file")
+ ).toBeInTheDocument();
+ });
+ });
+
+ test("renders content error message", async () => {
+ const error = new Error("Failed to load contents");
+ mockClient.vectorStores.files.content.mockRejectedValue(error);
+
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ "Error loading content summary: Failed to load contents"
+ )
+ ).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("File Information Display", () => {
+ test("renders file details correctly", async () => {
+ await act(async () => {
+ await act(async () => {
+ render();
+ });
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("File: file_456")).toBeInTheDocument();
+ expect(screen.getByText("File Information")).toBeInTheDocument();
+ expect(screen.getByText("File Details")).toBeInTheDocument();
+ });
+
+ const statusTexts = screen.getAllByText("Status:");
+ expect(statusTexts.length).toBeGreaterThan(0);
+ const completedTexts = screen.getAllByText("completed");
+ expect(completedTexts.length).toBeGreaterThan(0);
+ expect(screen.getByText("Size:")).toBeInTheDocument();
+ expect(screen.getByText("2048 bytes")).toBeInTheDocument();
+ const createdTexts = screen.getAllByText("Created:");
+ expect(createdTexts.length).toBeGreaterThan(0);
+ const dateTexts = screen.getAllByText(
+ new Date(1710001000 * 1000).toLocaleString()
+ );
+ expect(dateTexts.length).toBeGreaterThan(0);
+ const strategyTexts = screen.getAllByText("Content Strategy:");
+ expect(strategyTexts.length).toBeGreaterThan(0);
+ const fixedSizeTexts = screen.getAllByText("fixed_size");
+ expect(fixedSizeTexts.length).toBeGreaterThan(0);
+ });
+
+ test("handles missing file data", async () => {
+ mockClient.vectorStores.files.retrieve.mockResolvedValue(null);
+
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("File not found.")).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("Content Summary Display", () => {
+ test("renders content summary correctly", async () => {
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Content Summary")).toBeInTheDocument();
+ expect(screen.getByText("Content Items:")).toBeInTheDocument();
+ expect(screen.getByText("3")).toBeInTheDocument();
+ expect(screen.getByText("Total Characters:")).toBeInTheDocument();
+
+ const totalChars = mockFileContent.content.reduce(
+ (total, item) => total + item.text.length,
+ 0
+ );
+ expect(screen.getByText(totalChars.toString())).toBeInTheDocument();
+
+ expect(screen.getByText("Preview:")).toBeInTheDocument();
+ expect(
+ screen.getByText(/First chunk of file content\./)
+ ).toBeInTheDocument();
+ });
+ });
+
+ test("handles empty content", async () => {
+ mockClient.vectorStores.files.content.mockResolvedValue({
+ content: [],
+ });
+
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("No contents found for this file.")
+ ).toBeInTheDocument();
+ });
+ });
+
+ test("truncates long content preview", async () => {
+ const longContent = {
+ content: [
+ {
+ text: "This is a very long piece of content that should be truncated after 200 characters to ensure the preview doesn't take up too much space in the UI and remains readable and manageable for users viewing the file details page.",
+ },
+ ],
+ };
+
+ mockClient.vectorStores.files.content.mockResolvedValue(longContent);
+
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/This is a very long piece of content/)
+ ).toBeInTheDocument();
+ expect(screen.getByText(/\.\.\.$/)).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("Navigation and Actions", () => {
+ test("navigates to contents list when View Contents button is clicked", async () => {
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Actions")).toBeInTheDocument();
+ });
+
+ const viewContentsButton = screen.getByRole("button", {
+ name: /View Contents/,
+ });
+ fireEvent.click(viewContentsButton);
+
+ expect(mockPush).toHaveBeenCalledWith(
+ "/logs/vector-stores/vs_123/files/file_456/contents"
+ );
+ });
+
+ test("View Contents button is styled correctly", async () => {
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ const button = screen.getByRole("button", { name: /View Contents/ });
+ expect(button).toHaveClass("flex", "items-center", "gap-2");
+ });
+ });
+ });
+
+ describe("Breadcrumb Navigation", () => {
+ test("renders correct breadcrumb structure", async () => {
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ const vectorStoresTexts = screen.getAllByText("Vector Stores");
+ expect(vectorStoresTexts.length).toBeGreaterThan(0);
+ const storeNameTexts = screen.getAllByText("Test Vector Store");
+ expect(storeNameTexts.length).toBeGreaterThan(0);
+ const filesTexts = screen.getAllByText("Files");
+ expect(filesTexts.length).toBeGreaterThan(0);
+ const fileIdTexts = screen.getAllByText("file_456");
+ expect(fileIdTexts.length).toBeGreaterThan(0);
+ });
+ });
+
+ test("uses store ID when store name is not available", async () => {
+ const storeWithoutName = { ...mockStore, name: "" };
+ mockClient.vectorStores.retrieve.mockResolvedValue(storeWithoutName);
+
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ const storeIdTexts = screen.getAllByText("vs_123");
+ expect(storeIdTexts.length).toBeGreaterThan(0);
+ });
+ });
+ });
+
+ describe("Sidebar Properties", () => {
+ test.skip("renders file and store properties correctly", async () => {
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("File ID")).toBeInTheDocument();
+ const fileIdTexts = screen.getAllByText("file_456");
+ expect(fileIdTexts.length).toBeGreaterThan(0);
+ expect(screen.getByText("Vector Store ID")).toBeInTheDocument();
+ const storeIdTexts = screen.getAllByText("vs_123");
+ expect(storeIdTexts.length).toBeGreaterThan(0);
+ expect(screen.getByText("Status")).toBeInTheDocument();
+ const completedTexts = screen.getAllByText("completed");
+ expect(completedTexts.length).toBeGreaterThan(0);
+ expect(screen.getByText("Usage Bytes")).toBeInTheDocument();
+ const usageTexts = screen.getAllByText("2048");
+ expect(usageTexts.length).toBeGreaterThan(0);
+ expect(screen.getByText("Content Strategy")).toBeInTheDocument();
+ const fixedSizeTexts = screen.getAllByText("fixed_size");
+ expect(fixedSizeTexts.length).toBeGreaterThan(0);
+
+ expect(screen.getByText("Store Name")).toBeInTheDocument();
+ const storeNameTexts = screen.getAllByText("Test Vector Store");
+ expect(storeNameTexts.length).toBeGreaterThan(0);
+ expect(screen.getByText("Provider ID")).toBeInTheDocument();
+ expect(screen.getByText("test_provider")).toBeInTheDocument();
+ });
+ });
+
+ test("handles missing optional properties", async () => {
+ const minimalFile = {
+ id: "file_456",
+ status: "completed",
+ created_at: 1710001000,
+ usage_bytes: 2048,
+ chunking_strategy: { type: "fixed_size" },
+ };
+
+ const minimalStore = {
+ ...mockStore,
+ name: "",
+ metadata: {},
+ };
+
+ mockClient.vectorStores.files.retrieve.mockResolvedValue(minimalFile);
+ mockClient.vectorStores.retrieve.mockResolvedValue(minimalStore);
+
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ const fileIdTexts = screen.getAllByText("file_456");
+ expect(fileIdTexts.length).toBeGreaterThan(0);
+ const storeIdTexts = screen.getAllByText("vs_123");
+ expect(storeIdTexts.length).toBeGreaterThan(0);
+ });
+
+ expect(screen.getByText("File: file_456")).toBeInTheDocument();
+ });
+ });
+
+ describe("Loading States for Individual Sections", () => {
+ test("shows loading skeleton for content while file loads", async () => {
+ mockClient.vectorStores.files.content.mockImplementation(
+ () => new Promise(() => {})
+ );
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(screen.getByText("Content Summary")).toBeInTheDocument();
+ });
+
+ const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
+ expect(skeletons.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe("Error Handling", () => {
+ test("handles multiple simultaneous errors gracefully", async () => {
+ mockClient.vectorStores.files.retrieve.mockRejectedValue(
+ new Error("File error")
+ );
+ mockClient.vectorStores.files.content.mockRejectedValue(
+ new Error("Content error")
+ );
+
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Error loading file: File error")
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Error loading content summary: Content error")
+ ).toBeInTheDocument();
+ });
+ });
+ });
+});
diff --git a/llama_stack/ui/components/chat-playground/markdown-renderer.tsx b/llama_stack/ui/components/chat-playground/markdown-renderer.tsx
index bc6bf5122..b48b5e1ba 100644
--- a/llama_stack/ui/components/chat-playground/markdown-renderer.tsx
+++ b/llama_stack/ui/components/chat-playground/markdown-renderer.tsx
@@ -187,6 +187,7 @@ const COMPONENTS = {
code: ({
children,
className,
+ ...rest
}: {
children: React.ReactNode;
className?: string;
diff --git a/llama_stack/ui/components/vector-stores/vector-store-detail.test.tsx b/llama_stack/ui/components/vector-stores/vector-store-detail.test.tsx
new file mode 100644
index 000000000..08f90ac0d
--- /dev/null
+++ b/llama_stack/ui/components/vector-stores/vector-store-detail.test.tsx
@@ -0,0 +1,315 @@
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import { VectorStoreDetailView } from "./vector-store-detail";
+import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
+import type { VectorStoreFile } from "llama-stack-client/resources/vector-stores/files";
+
+const mockPush = jest.fn();
+jest.mock("next/navigation", () => ({
+ useRouter: () => ({
+ push: mockPush,
+ }),
+}));
+
+describe("VectorStoreDetailView", () => {
+ const defaultProps = {
+ store: null,
+ files: [],
+ isLoadingStore: false,
+ isLoadingFiles: false,
+ errorStore: null,
+ errorFiles: null,
+ id: "test_vector_store_id",
+ };
+
+ beforeEach(() => {
+ mockPush.mockClear();
+ });
+
+ describe("Loading States", () => {
+ test("renders loading skeleton when store is loading", () => {
+ const { container } = render(
+
+ );
+
+ const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
+ expect(skeletons.length).toBeGreaterThan(0);
+ });
+
+ test("renders files loading skeleton when files are loading", () => {
+ const mockStore: VectorStore = {
+ id: "vs_123",
+ name: "Test Vector Store",
+ created_at: 1710000000,
+ status: "ready",
+ file_counts: { total: 5 },
+ usage_bytes: 1024,
+ metadata: {
+ provider_id: "test_provider",
+ provider_vector_db_id: "test_db_id",
+ },
+ };
+
+ const { container } = render(
+
+ );
+
+ expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
+ expect(screen.getByText("Files")).toBeInTheDocument();
+ const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
+ expect(skeletons.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe("Error States", () => {
+ test("renders error message when store error occurs", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
+ expect(
+ screen.getByText(/Error loading details for ID test_vector_store_id/)
+ ).toBeInTheDocument();
+ expect(screen.getByText(/Failed to load store/)).toBeInTheDocument();
+ });
+
+ test("renders files error when files fail to load", () => {
+ const mockStore: VectorStore = {
+ id: "vs_123",
+ name: "Test Vector Store",
+ created_at: 1710000000,
+ status: "ready",
+ file_counts: { total: 5 },
+ usage_bytes: 1024,
+ metadata: {
+ provider_id: "test_provider",
+ provider_vector_db_id: "test_db_id",
+ },
+ };
+
+ render(
+
+ );
+
+ expect(screen.getByText("Files")).toBeInTheDocument();
+ expect(
+ screen.getByText("Error loading files: Failed to load files")
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Not Found State", () => {
+ test("renders not found message when store is null", () => {
+ render();
+
+ expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
+ expect(
+ screen.getByText(/No details found for ID: test_vector_store_id/)
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Store Data Rendering", () => {
+ const mockStore: VectorStore = {
+ id: "vs_123",
+ name: "Test Vector Store",
+ created_at: 1710000000,
+ status: "ready",
+ file_counts: { total: 3 },
+ usage_bytes: 2048,
+ metadata: {
+ provider_id: "test_provider",
+ provider_vector_db_id: "test_db_id",
+ },
+ };
+
+ test("renders store properties correctly", () => {
+ render();
+
+ expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
+ expect(screen.getByText("vs_123")).toBeInTheDocument();
+ expect(screen.getByText("Test Vector Store")).toBeInTheDocument();
+ expect(
+ screen.getByText(new Date(1710000000 * 1000).toLocaleString())
+ ).toBeInTheDocument();
+ expect(screen.getByText("ready")).toBeInTheDocument();
+ expect(screen.getByText("3")).toBeInTheDocument();
+ expect(screen.getByText("2048")).toBeInTheDocument();
+ expect(screen.getByText("test_provider")).toBeInTheDocument();
+ expect(screen.getByText("test_db_id")).toBeInTheDocument();
+ });
+
+ test("handles empty/missing optional fields", () => {
+ const minimalStore: VectorStore = {
+ id: "vs_minimal",
+ name: "",
+ created_at: 1710000000,
+ status: "ready",
+ file_counts: { total: 0 },
+ usage_bytes: 0,
+ metadata: {},
+ };
+
+ render();
+
+ expect(screen.getByText("vs_minimal")).toBeInTheDocument();
+ expect(screen.getByText("ready")).toBeInTheDocument();
+ const zeroTexts = screen.getAllByText("0");
+ expect(zeroTexts.length).toBeGreaterThanOrEqual(2);
+ });
+
+ test("shows empty files message when no files", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Files")).toBeInTheDocument();
+ expect(
+ screen.getByText("No files in this vector store.")
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Files Table", () => {
+ const mockStore: VectorStore = {
+ id: "vs_123",
+ name: "Test Vector Store",
+ created_at: 1710000000,
+ status: "ready",
+ file_counts: { total: 2 },
+ usage_bytes: 2048,
+ metadata: {},
+ };
+
+ const mockFiles: VectorStoreFile[] = [
+ {
+ id: "file_123",
+ status: "completed",
+ created_at: 1710001000,
+ usage_bytes: 1024,
+ },
+ {
+ id: "file_456",
+ status: "processing",
+ created_at: 1710002000,
+ usage_bytes: 512,
+ },
+ ];
+
+ test("renders files table with correct data", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Files")).toBeInTheDocument();
+ expect(
+ screen.getByText("Files in this vector store")
+ ).toBeInTheDocument();
+
+ expect(screen.getByText("ID")).toBeInTheDocument();
+ expect(screen.getByText("Status")).toBeInTheDocument();
+ expect(screen.getByText("Created")).toBeInTheDocument();
+ expect(screen.getByText("Usage Bytes")).toBeInTheDocument();
+
+ expect(screen.getByText("file_123")).toBeInTheDocument();
+ expect(screen.getByText("completed")).toBeInTheDocument();
+ expect(
+ screen.getByText(new Date(1710001000 * 1000).toLocaleString())
+ ).toBeInTheDocument();
+ expect(screen.getByText("1024")).toBeInTheDocument();
+
+ expect(screen.getByText("file_456")).toBeInTheDocument();
+ expect(screen.getByText("processing")).toBeInTheDocument();
+ expect(
+ screen.getByText(new Date(1710002000 * 1000).toLocaleString())
+ ).toBeInTheDocument();
+ expect(screen.getByText("512")).toBeInTheDocument();
+ });
+
+ test("file ID links are clickable and navigate correctly", () => {
+ render(
+
+ );
+
+ const fileButton = screen.getByRole("button", { name: "file_123" });
+ expect(fileButton).toBeInTheDocument();
+
+ fireEvent.click(fileButton);
+ expect(mockPush).toHaveBeenCalledWith(
+ "/logs/vector-stores/vs_123/files/file_123"
+ );
+ });
+
+ test("handles multiple file clicks correctly", () => {
+ render(
+
+ );
+
+ const file1Button = screen.getByRole("button", { name: "file_123" });
+ const file2Button = screen.getByRole("button", { name: "file_456" });
+
+ fireEvent.click(file1Button);
+ expect(mockPush).toHaveBeenCalledWith(
+ "/logs/vector-stores/vs_123/files/file_123"
+ );
+
+ fireEvent.click(file2Button);
+ expect(mockPush).toHaveBeenCalledWith(
+ "/logs/vector-stores/vs_123/files/file_456"
+ );
+
+ expect(mockPush).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe("Layout Structure", () => {
+ const mockStore: VectorStore = {
+ id: "vs_layout_test",
+ name: "Layout Test Store",
+ created_at: 1710000000,
+ status: "ready",
+ file_counts: { total: 1 },
+ usage_bytes: 1024,
+ metadata: {},
+ };
+
+ test("renders main content and sidebar in correct layout", () => {
+ render();
+
+ expect(screen.getByText("Files")).toBeInTheDocument();
+
+ expect(screen.getByText("vs_layout_test")).toBeInTheDocument();
+ expect(screen.getByText("Layout Test Store")).toBeInTheDocument();
+ expect(screen.getByText("ready")).toBeInTheDocument();
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.getByText("1024")).toBeInTheDocument();
+ });
+ });
+});