diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json
index 508015dcac8..9de171397db 100644
--- a/ui/litellm-dashboard/eslint-suppressions.json
+++ b/ui/litellm-dashboard/eslint-suppressions.json
@@ -1085,14 +1085,6 @@
"count": 2
}
},
- "src/app/(dashboard)/prompts/_components/prompt_table.tsx": {
- "no-nested-ternary": {
- "count": 1
- },
- "no-restricted-imports": {
- "count": 1
- }
- },
"src/app/(dashboard)/router-settings/_components/general_settings.tsx": {
"no-nested-ternary": {
"count": 3
@@ -1140,14 +1132,6 @@
"count": 1
}
},
- "src/app/(dashboard)/skills/_components/plugin_table.tsx": {
- "no-nested-ternary": {
- "count": 1
- },
- "no-restricted-imports": {
- "count": 1
- }
- },
"src/app/(dashboard)/tag-management/_components/components/CreateTagModal.tsx": {
"no-restricted-imports": {
"count": 1
@@ -1307,11 +1291,6 @@
"count": 1
}
},
- "src/app/(dashboard)/vector-stores/_components/VectorStoreTable.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"src/app/(dashboard)/vector-stores/_components/index.tsx": {
"no-restricted-imports": {
"count": 1
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTable.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTable.test.tsx
new file mode 100644
index 00000000000..52efd6407f8
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTable.test.tsx
@@ -0,0 +1,104 @@
+import { render, screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { PromptSpec } from "@/components/networking";
+
+import PromptTable from "./PromptTable";
+
+vi.mock("@/components/networking", () => ({
+ modelHubCall: vi.fn().mockResolvedValue({ data: [] }),
+}));
+
+const mockPrompts: PromptSpec[] = [
+ {
+ prompt_id: "prompt-newer",
+ litellm_params: { prompt_id: "prompt-newer" },
+ prompt_info: { prompt_type: "dotprompt" },
+ created_at: "2025-01-15T10:30:00Z",
+ updated_at: "2025-01-15T11:00:00Z",
+ environment: "production",
+ created_by: "user-1",
+ },
+ {
+ prompt_id: "prompt-older",
+ litellm_params: { prompt_id: "prompt-older" },
+ prompt_info: { prompt_type: "dotprompt" },
+ created_at: "2024-01-10T09:15:00Z",
+ updated_at: "2024-01-12T14:20:00Z",
+ },
+];
+
+const mockOnPromptClick = vi.fn();
+const mockOnDeleteClick = vi.fn();
+
+const defaultProps = {
+ promptsList: mockPrompts,
+ isLoading: false,
+ onPromptClick: mockOnPromptClick,
+ onDeleteClick: mockOnDeleteClick,
+ accessToken: null,
+ isAdmin: true,
+};
+
+describe("PromptTable", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("should render every column header", () => {
+ render();
+ for (const header of ["Prompt ID", "Model", "Created At", "Updated At", "Environment", "Created By", "Type"]) {
+ expect(screen.getByText(header)).toBeInTheDocument();
+ }
+ });
+
+ it("should display the empty state when data is empty", () => {
+ render();
+ expect(screen.getByText("No prompts yet")).toBeInTheDocument();
+ });
+
+ it("should sort by created date descending by default", () => {
+ render();
+ const rows = screen.getAllByRole("row").slice(1);
+ expect(within(rows[0]).getByText("prompt-newer")).toBeInTheDocument();
+ expect(within(rows[1]).getByText("prompt-older")).toBeInTheDocument();
+ });
+
+ it("should call onPromptClick when the prompt ID is clicked", async () => {
+ const user = userEvent.setup();
+ render();
+ await user.click(screen.getByRole("button", { name: "prompt-newer" }));
+ expect(mockOnPromptClick).toHaveBeenCalledWith("prompt-newer");
+ });
+
+ it("should label the environment and default missing environments to development", () => {
+ render();
+ expect(screen.getByText("production")).toBeInTheDocument();
+ expect(screen.getByText("development")).toBeInTheDocument();
+ });
+
+ it("should delete a prompt through the actions menu when admin", async () => {
+ const user = userEvent.setup();
+ render();
+ await user.click(screen.getByTestId("prompt-actions-prompt-newer"));
+ await user.click(await screen.findByTestId("prompt-action-delete"));
+ expect(mockOnDeleteClick).toHaveBeenCalledWith("prompt-newer", "prompt-newer");
+ });
+
+ it("should copy the prompt ID through the actions menu", async () => {
+ const user = userEvent.setup();
+ render();
+ await user.click(screen.getByTestId("prompt-actions-prompt-newer"));
+ await user.click(await screen.findByTestId("prompt-action-copy"));
+ expect(await window.navigator.clipboard.readText()).toBe("prompt-newer");
+ });
+
+ it("should hide the delete action for non-admins but keep copy available", async () => {
+ const user = userEvent.setup();
+ render();
+ await user.click(screen.getByTestId("prompt-actions-prompt-newer"));
+ expect(await screen.findByTestId("prompt-action-copy")).toBeInTheDocument();
+ expect(screen.queryByTestId("prompt-action-delete")).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTable.tsx
new file mode 100644
index 00000000000..47d4f64f254
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/PromptTable.tsx
@@ -0,0 +1,89 @@
+"use client";
+
+import { SortingState } from "@tanstack/react-table";
+import { Inbox } from "lucide-react";
+import React, { useEffect, useMemo, useState } from "react";
+
+import { DataTable } from "@/components/shared/DataTable";
+import { modelHubCall, PromptSpec } from "@/components/networking";
+
+import { getPromptTableColumns } from "./PromptTableColumns";
+import { ModelGroupInfo } from "./prompt_utils";
+
+interface PromptTableProps {
+ promptsList: PromptSpec[];
+ isLoading: boolean;
+ onPromptClick?: (id: string) => void;
+ onDeleteClick?: (id: string, name: string) => void;
+ accessToken: string | null;
+ isAdmin: boolean;
+}
+
+const DEFAULT_SORTING: SortingState = [{ id: "created_at", desc: true }];
+
+function EmptyState() {
+ return (
+
+
+
+
+
No prompts yet
+
Add a prompt to start managing reusable templates.
+
+ );
+}
+
+const PromptTable: React.FC = ({
+ promptsList,
+ isLoading,
+ onPromptClick,
+ onDeleteClick,
+ accessToken,
+ isAdmin,
+}) => {
+ const [sorting, setSorting] = useState(DEFAULT_SORTING);
+ const [modelHubData, setModelHubData] = useState