diff --git a/ui/litellm-dashboard/src/app/(dashboard)/model-hub/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/model-hub/page.tsx
index 842ecdce64a..c37a935976b 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/model-hub/page.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/model-hub/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import ModelHubTable from "@/components/AIHub/model_hub_table";
+import ModelHubTable from "@/components/AIHub/ModelHubTable";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const ModelHubPage = () => {
diff --git a/ui/litellm-dashboard/src/app/model_hub_table/page.tsx b/ui/litellm-dashboard/src/app/model_hub_table/page.tsx
index df454eb48e5..1df7019ad25 100644
--- a/ui/litellm-dashboard/src/app/model_hub_table/page.tsx
+++ b/ui/litellm-dashboard/src/app/model_hub_table/page.tsx
@@ -1,7 +1,7 @@
"use client";
import React, { useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
-import ModelHubTable from "@/components/AIHub/model_hub_table";
+import ModelHubTable from "@/components/AIHub/ModelHubTable";
export default function PublicModelHubTable() {
const searchParams = useSearchParams()!;
diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx
index 6d5384969ca..598266df9a2 100644
--- a/ui/litellm-dashboard/src/app/page.tsx
+++ b/ui/litellm-dashboard/src/app/page.tsx
@@ -15,7 +15,7 @@ import GeneralSettings from "@/components/general_settings";
import GuardrailsPanel from "@/components/guardrails";
import { Team } from "@/components/key_team_helpers/key_list";
import { MCPServers } from "@/components/mcp_tools";
-import ModelHubTable from "@/components/AIHub/model_hub_table";
+import ModelHubTable from "@/components/AIHub/ModelHubTable";
import Navbar from "@/components/navbar";
import { getUiConfig, Organization, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking";
import NewUsagePage from "@/components/UsagePage/components/UsagePageView";
diff --git a/ui/litellm-dashboard/src/components/AIHub/agent_hub_table_columns.tsx b/ui/litellm-dashboard/src/components/AIHub/AgentHubTableColumns.tsx
similarity index 93%
rename from ui/litellm-dashboard/src/components/AIHub/agent_hub_table_columns.tsx
rename to ui/litellm-dashboard/src/components/AIHub/AgentHubTableColumns.tsx
index 026165c0eb9..c6a8c0b9daa 100644
--- a/ui/litellm-dashboard/src/components/AIHub/agent_hub_table_columns.tsx
+++ b/ui/litellm-dashboard/src/components/AIHub/AgentHubTableColumns.tsx
@@ -28,7 +28,7 @@ export interface AgentHubData {
[key: string]: any;
}
-export const agentHubColumns = (
+export const getAgentHubTableColumns = (
showModal: (agent: AgentHubData) => void,
copyToClipboard: (text: string) => void,
publicPage: boolean = false,
@@ -69,11 +69,7 @@ export const agentHubColumns = (
cell: ({ row }) => {
const agent = row.original;
- return (
-
- {agent.description || "-"}
-
- );
+ return {agent.description || "-"};
},
meta: {
className: "hidden md:table-cell",
@@ -105,11 +101,7 @@ export const agentHubColumns = (
cell: ({ row }) => {
const agent = row.original;
- return (
-
- {agent.protocolVersion || "-"}
-
- );
+ return {agent.protocolVersion || "-"};
},
meta: {
className: "hidden lg:table-cell",
@@ -135,9 +127,7 @@ export const agentHubColumns = (
{skill.name}
))}
- {skills.length > 2 && (
- +{skills.length - 2}
- )}
+ {skills.length > 2 && +{skills.length - 2}}
)}
@@ -240,4 +230,3 @@ export const agentHubColumns = (
return allColumns;
};
-
diff --git a/ui/litellm-dashboard/src/components/AIHub/model_hub_table.test.tsx b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.test.tsx
similarity index 88%
rename from ui/litellm-dashboard/src/components/AIHub/model_hub_table.test.tsx
rename to ui/litellm-dashboard/src/components/AIHub/ModelHubTable.test.tsx
index be0b9a113f4..a88ce0d7938 100644
--- a/ui/litellm-dashboard/src/components/AIHub/model_hub_table.test.tsx
+++ b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.test.tsx
@@ -1,7 +1,7 @@
import * as networking from "@/components/networking";
import { render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
-import ModelHubTable from "./model_hub_table";
+import ModelHubTable from "./ModelHubTable";
vi.mock("@/components/networking", () => ({
getUiConfig: vi.fn(),
@@ -19,7 +19,7 @@ vi.mock("next/navigation", () => ({
}),
}));
-vi.mock("./public_model_hub", () => ({
+vi.mock("@/components/public_model_hub", () => ({
default: () =>
Public Model Hub
,
}));
@@ -51,7 +51,12 @@ describe("ModelHubTable", () => {
const getUiConfigMock = vi.mocked(networking.getUiConfig);
const modelHubPublicModelsCallMock = vi.mocked(networking.modelHubPublicModelsCall);
- getUiConfigMock.mockResolvedValue({ server_root_path: "/", proxy_base_url: "http://localhost:4000" });
+ getUiConfigMock.mockResolvedValue({
+ server_root_path: "/",
+ proxy_base_url: "http://localhost:4000",
+ auto_redirect_to_sso: false,
+ admin_ui_disabled: false,
+ });
modelHubPublicModelsCallMock.mockResolvedValue([]);
render();
diff --git a/ui/litellm-dashboard/src/components/AIHub/model_hub_table.tsx b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx
similarity index 98%
rename from ui/litellm-dashboard/src/components/AIHub/model_hub_table.tsx
rename to ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx
index d73be781cb8..7c538b618f1 100644
--- a/ui/litellm-dashboard/src/components/AIHub/model_hub_table.tsx
+++ b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx
@@ -1,10 +1,10 @@
-import { agentHubColumns, AgentHubData } from "@/components/AIHub/agent_hub_table_columns";
-import MakeAgentPublicForm from "@/components/AIHub/forms/make_agent_public_form";
-import MakeMCPPublicForm from "@/components/AIHub/forms/make_mcp_public_form";
-import MakeModelPublicForm from "@/components/AIHub/forms/make_model_public_form";
+import { AgentHubData, getAgentHubTableColumns } from "@/components/AIHub/AgentHubTableColumns";
+import MakeAgentPublicForm from "@/components/AIHub/forms/MakeAgentPublicForm";
+import MakeMCPPublicForm from "@/components/AIHub/forms/MakeMCPPublicForm";
+import MakeModelPublicForm from "@/components/AIHub/forms/MakeModelPublicForm";
import { mcpHubColumns, MCPServerData } from "@/components/mcp_hub_table_columns";
import { modelHubColumns } from "@/components/model_hub_table_columns";
-import UsefulLinksManagement from "@/components/AIHub/useful_links_management";
+import UsefulLinksManagement from "@/components/AIHub/UsefulLinksManagement";
import { ModelDataTable } from "@/components/model_dashboard/table";
import ModelFilters from "@/components/model_filters";
import NotificationsManager from "@/components/molecules/notifications_manager";
@@ -423,7 +423,7 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage,
{/* Agent Table */}
({
+vi.mock("@/components/networking", () => ({
getPublicModelHubInfo: vi.fn(),
updateUsefulLinksCall: vi.fn(),
getProxyBaseUrl: vi.fn(),
}));
-vi.mock("./molecules/notifications_manager", () => ({
+vi.mock("@/components/molecules/notifications_manager", () => ({
__esModule: true,
default: {
success: vi.fn(),
diff --git a/ui/litellm-dashboard/src/components/AIHub/useful_links_management.tsx b/ui/litellm-dashboard/src/components/AIHub/UsefulLinksManagement.tsx
similarity index 100%
rename from ui/litellm-dashboard/src/components/AIHub/useful_links_management.tsx
rename to ui/litellm-dashboard/src/components/AIHub/UsefulLinksManagement.tsx
diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.test.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.test.tsx
new file mode 100644
index 00000000000..67c6d7d6cc9
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.test.tsx
@@ -0,0 +1,505 @@
+import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import MakeAgentPublicForm from "./MakeAgentPublicForm";
+import { AgentHubData } from "@/components/AIHub/AgentHubTableColumns";
+
+// Mock the networking function
+vi.mock("../../networking", () => ({
+ makeAgentsPublicCall: vi.fn(),
+}));
+
+// Import the mocked function
+import { makeAgentsPublicCall } from "../../networking";
+const mockMakeAgentsPublicCall = vi.mocked(makeAgentsPublicCall);
+
+// Mock antd components
+vi.mock("antd", () => ({
+ Modal: ({ open, title, children, onCancel, footer }: any) =>
+ open ? (
+
+
{title}
+ {children}
+ {footer}
+
+ ) : null,
+ Form: Object.assign(({ children, form }: any) => , {
+ useForm: () => [
+ {
+ resetFields: vi.fn(),
+ validateFields: vi.fn(),
+ getFieldsValue: vi.fn(),
+ setFieldsValue: vi.fn(),
+ },
+ vi.fn(),
+ ],
+ Item: ({ children }: any) => {children}
,
+ }),
+ Steps: Object.assign(
+ ({ children, current, className }: any) => (
+
+ {children}
+
+ ),
+ {
+ Step: ({ title }: any) => {title}
,
+ },
+ ),
+ Button: ({ children, onClick, disabled, loading, ...props }: any) => (
+
+ ),
+ Checkbox: ({ checked, indeterminate, onChange, children, disabled }: any) => (
+
+ ),
+}));
+
+// Mock @tremor/react components
+vi.mock("@tremor/react", () => ({
+ Text: ({ children, className }: any) => {children},
+ Title: ({ children }: any) => {children}
,
+ Badge: ({ children, color, size }: any) => (
+
+ {children}
+
+ ),
+}));
+
+describe("MakeAgentPublicForm", () => {
+ const mockProps = {
+ visible: true,
+ onClose: vi.fn(),
+ accessToken: "test-token",
+ agentHubData: [
+ {
+ agent_id: "agent-1",
+ name: "Test Agent 1",
+ description: "Description 1",
+ version: "1.0",
+ is_public: false,
+ skills: [
+ { id: "skill-1", name: "Skill 1", description: "Skill desc" },
+ { id: "skill-2", name: "Skill 2", description: "Skill desc" },
+ ],
+ protocolVersion: "1.0",
+ },
+ {
+ agent_id: "agent-2",
+ name: "Test Agent 2",
+ description: "Description 2",
+ version: "2.0",
+ is_public: true,
+ skills: [],
+ protocolVersion: "1.0",
+ },
+ ] as AgentHubData[],
+ onSuccess: vi.fn(),
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ vi.resetAllMocks();
+ });
+
+ it("should render the component", () => {
+ render();
+
+ expect(screen.getByText("Make Agents Public")).toBeInTheDocument();
+ expect(screen.getByText("Select Agents to Make Public")).toBeInTheDocument();
+ });
+
+ it("should initialize with correct state", () => {
+ render();
+
+ // Check that the component renders with the correct title and content
+ expect(screen.getByText("Make Agents Public")).toBeInTheDocument();
+ expect(screen.getByText("Select Agents to Make Public")).toBeInTheDocument();
+
+ // Check that all agent checkboxes are present
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(3); // Select all + 2 agents
+
+ // Check that the Next button is enabled (agents are preselected)
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ expect(nextButton).not.toBeDisabled();
+ });
+
+ it("should handle agent selection and navigation", async () => {
+ render();
+
+ // Initially on step 1
+ expect(screen.getByText("Select Agents to Make Public")).toBeInTheDocument();
+
+ // Select all agents using the select all checkbox
+ const selectAllCheckbox = screen.getByLabelText("Select All (2)");
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // Verify Next button is enabled
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ expect(nextButton).not.toBeDisabled();
+
+ // Click Next
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Should move to step 2
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making Agents Public")).toBeInTheDocument();
+ });
+ });
+
+ it("should submit selected agents successfully", async () => {
+ mockMakeAgentsPublicCall.mockResolvedValueOnce({});
+
+ render();
+
+ // Select all agents
+ const selectAllCheckbox = screen.getByLabelText("Select All (2)");
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // Navigate to confirm step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Wait for navigation to complete
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making Agents Public")).toBeInTheDocument();
+ });
+
+ // Submit
+ const submitButton = screen.getByRole("button", { name: "Make Public" });
+ await act(async () => {
+ fireEvent.click(submitButton);
+ });
+
+ await waitFor(() => {
+ expect(mockMakeAgentsPublicCall).toHaveBeenCalledWith("test-token", ["agent-1", "agent-2"]);
+ expect(mockProps.onSuccess).toHaveBeenCalled();
+ expect(mockProps.onClose).toHaveBeenCalled();
+ });
+ });
+
+ it("should handle select all functionality", async () => {
+ render();
+
+ const checkboxes = screen.getAllByRole("checkbox");
+ const selectAllCheckbox = checkboxes[0];
+
+ // Select all
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // All checkboxes should be checked
+ checkboxes.forEach((checkbox) => {
+ expect(checkbox).toBeChecked();
+ });
+
+ // Deselect all
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // All checkboxes should be unchecked except the indeterminate state
+ expect(checkboxes[0]).not.toBeChecked();
+ expect(checkboxes[1]).not.toBeChecked();
+ expect(checkboxes[2]).not.toBeChecked();
+ });
+
+ it("should show error when no agents selected", async () => {
+ render();
+
+ // Deselect all agents first
+ const checkboxes = screen.getAllByRole("checkbox");
+ await act(async () => {
+ fireEvent.click(checkboxes[0]); // Click select all to select all
+ fireEvent.click(checkboxes[0]); // Click select all again to deselect all
+ });
+
+ // Try to go to next step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Should stay on same step
+ expect(screen.getByText("Select Agents to Make Public")).toBeInTheDocument();
+ });
+
+ it("should display empty state when no agents are available", () => {
+ const emptyProps = {
+ ...mockProps,
+ agentHubData: [] as AgentHubData[],
+ };
+
+ render();
+
+ expect(screen.getByText("No agents available.")).toBeInTheDocument();
+
+ // Select All checkbox should be disabled
+ const selectAllCheckbox = screen.getByLabelText("Select All");
+ expect(selectAllCheckbox).toBeDisabled();
+
+ // Next button should be disabled
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ expect(nextButton).toBeDisabled();
+ });
+
+ it("should handle Cancel button functionality", async () => {
+ render();
+
+ // Click Cancel button
+ const cancelButton = screen.getByRole("button", { name: "Cancel" });
+ await act(async () => {
+ fireEvent.click(cancelButton);
+ });
+
+ // Should call onClose
+ expect(mockProps.onClose).toHaveBeenCalled();
+ });
+
+ it("should handle Previous button functionality", async () => {
+ render();
+
+ // Navigate to step 1
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Verify we're on step 1
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making Agents Public")).toBeInTheDocument();
+ });
+
+ // Click Previous button
+ const previousButton = screen.getByRole("button", { name: "Previous" });
+ await act(async () => {
+ fireEvent.click(previousButton);
+ });
+
+ // Should go back to step 0
+ expect(screen.getByText("Select Agents to Make Public")).toBeInTheDocument();
+ });
+
+ it("should handle individual agent selection", async () => {
+ render();
+
+ // Get all checkboxes (select all + individual agents)
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(3); // Select all + 2 agents
+
+ // Initially, agent-2 should be selected (it's already public)
+ const agent1Checkbox = checkboxes[1]; // First agent checkbox
+ const agent2Checkbox = checkboxes[2]; // Second agent checkbox
+
+ expect(agent2Checkbox).toBeChecked(); // agent-2 is already public
+
+ // Select agent-1
+ await act(async () => {
+ fireEvent.click(agent1Checkbox);
+ });
+
+ expect(agent1Checkbox).toBeChecked();
+ expect(agent2Checkbox).toBeChecked();
+
+ // Deselect agent-2
+ await act(async () => {
+ fireEvent.click(agent2Checkbox);
+ });
+
+ expect(agent1Checkbox).toBeChecked();
+ expect(agent2Checkbox).not.toBeChecked();
+
+ // Select all should be indeterminate now
+ const selectAllCheckbox = checkboxes[0];
+ expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
+ });
+
+ it("should display skills overflow text when agent has more than 3 skills", () => {
+ const agentWithManySkills = {
+ ...mockProps.agentHubData[0],
+ skills: [
+ { id: "skill-1", name: "Skill 1", description: "Skill desc" },
+ { id: "skill-2", name: "Skill 2", description: "Skill desc" },
+ { id: "skill-3", name: "Skill 3", description: "Skill desc" },
+ { id: "skill-4", name: "Skill 4", description: "Skill desc" },
+ { id: "skill-5", name: "Skill 5", description: "Skill desc" },
+ ],
+ };
+
+ const propsWithManySkills = {
+ ...mockProps,
+ agentHubData: [agentWithManySkills],
+ };
+
+ render();
+
+ // Should show first 3 skills as badges
+ expect(screen.getByText("Skill 1")).toBeInTheDocument();
+ expect(screen.getByText("Skill 2")).toBeInTheDocument();
+ expect(screen.getByText("Skill 3")).toBeInTheDocument();
+
+ // Should show "+2 more" text for the remaining skills
+ expect(screen.getByText("+2 more")).toBeInTheDocument();
+ });
+
+ it("should handle submit error properly", async () => {
+ const errorMessage = "Network error";
+ mockMakeAgentsPublicCall.mockRejectedValueOnce(new Error(errorMessage));
+
+ render();
+
+ // Navigate to confirm step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making Agents Public")).toBeInTheDocument();
+ });
+
+ // Submit
+ const submitButton = screen.getByRole("button", { name: "Make Public" });
+ await act(async () => {
+ fireEvent.click(submitButton);
+ });
+
+ // Should handle error and show error notification
+ await waitFor(() => {
+ expect(mockMakeAgentsPublicCall).toHaveBeenCalledWith("test-token", ["agent-2"]);
+ });
+
+ // Should not call onSuccess or onClose on error
+ expect(mockProps.onSuccess).not.toHaveBeenCalled();
+ expect(mockProps.onClose).not.toHaveBeenCalled();
+ });
+
+ it("should show loading state during submit", async () => {
+ let resolvePromise: (value: any) => void = () => {};
+ const pendingPromise = new Promise((resolve) => {
+ resolvePromise = resolve;
+ });
+ mockMakeAgentsPublicCall.mockReturnValueOnce(pendingPromise);
+
+ render();
+
+ // Navigate to confirm step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making Agents Public")).toBeInTheDocument();
+ });
+
+ // Submit
+ const submitButton = screen.getByRole("button", { name: "Make Public" });
+ await act(async () => {
+ fireEvent.click(submitButton);
+ });
+
+ // Check loading state
+ expect(submitButton).toHaveAttribute("data-loading", "true");
+ expect(submitButton).toBeDisabled();
+
+ // Resolve the promise
+ resolvePromise({});
+ await waitFor(() => {
+ expect(mockProps.onSuccess).toHaveBeenCalled();
+ expect(mockProps.onClose).toHaveBeenCalled();
+ });
+ });
+
+ it("should not render modal when visible is false", () => {
+ const invisibleProps = {
+ ...mockProps,
+ visible: false,
+ };
+
+ render();
+
+ // Modal should not be rendered
+ expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
+ expect(screen.queryByText("Make Agents Public")).not.toBeInTheDocument();
+ });
+
+ it("should preselect already public agents when modal opens", () => {
+ // Test data where one agent is public and one is not
+ const mixedPublicProps = {
+ ...mockProps,
+ agentHubData: [
+ {
+ agent_id: "agent-1",
+ name: "Test Agent 1",
+ description: "Description 1",
+ url: "http://example.com/agent1",
+ version: "1.0",
+ is_public: false, // Not public
+ skills: [],
+ protocolVersion: "1.0",
+ },
+ {
+ agent_id: "agent-2",
+ name: "Test Agent 2",
+ description: "Description 2",
+ url: "http://example.com/agent2",
+ version: "2.0",
+ is_public: true, // Already public
+ skills: [],
+ protocolVersion: "1.0",
+ },
+ {
+ agent_id: "agent-3",
+ name: "Test Agent 3",
+ description: "Description 3",
+ url: "http://example.com/agent3",
+ version: "3.0",
+ is_public: true, // Already public
+ skills: [],
+ protocolVersion: "1.0",
+ },
+ ] as AgentHubData[],
+ };
+
+ render();
+
+ // Check that the correct checkboxes are selected
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(4); // Select all + 3 agents
+
+ // agent-2 and agent-3 should be checked (they're already public)
+ const agent1Checkbox = checkboxes[1];
+ const agent2Checkbox = checkboxes[2];
+ const agent3Checkbox = checkboxes[3];
+
+ expect(agent1Checkbox).not.toBeChecked(); // agent-1 is not public
+ expect(agent2Checkbox).toBeChecked(); // agent-2 is public
+ expect(agent3Checkbox).toBeChecked(); // agent-3 is public
+
+ // Select all should be indeterminate
+ const selectAllCheckbox = checkboxes[0];
+ expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/make_agent_public_form.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.tsx
similarity index 99%
rename from ui/litellm-dashboard/src/components/AIHub/forms/make_agent_public_form.tsx
rename to ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.tsx
index bcb23b99974..a38950b8fb7 100644
--- a/ui/litellm-dashboard/src/components/AIHub/forms/make_agent_public_form.tsx
+++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.tsx
@@ -3,7 +3,7 @@ import { Modal, Form, Steps, Button, Checkbox } from "antd";
import { Text, Title, Badge } from "@tremor/react";
import { makeAgentsPublicCall } from "../../networking";
import NotificationsManager from "../../molecules/notifications_manager";
-import { AgentHubData } from "@/components/AIHub/agent_hub_table_columns";
+import { AgentHubData } from "@/components/AIHub/AgentHubTableColumns";
const { Step } = Steps;
diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.test.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.test.tsx
new file mode 100644
index 00000000000..b0228e9e868
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.test.tsx
@@ -0,0 +1,562 @@
+import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import MakeMCPPublicForm from "./MakeMCPPublicForm";
+import { MCPServerData } from "../../mcp_hub_table_columns";
+
+// Mock the networking function
+vi.mock("../../networking", () => ({
+ makeMCPPublicCall: vi.fn(),
+}));
+
+// Import the mocked function
+import { makeMCPPublicCall } from "../../networking";
+const mockMakeMCPPublicCall = vi.mocked(makeMCPPublicCall);
+
+// Mock antd components
+vi.mock("antd", () => ({
+ Modal: ({ open, title, children, onCancel, footer }: any) =>
+ open ? (
+
+
{title}
+ {children}
+ {footer}
+
+ ) : null,
+ Form: Object.assign(({ children, form }: any) => , {
+ useForm: () => [
+ {
+ resetFields: vi.fn(),
+ validateFields: vi.fn(),
+ getFieldsValue: vi.fn(),
+ setFieldsValue: vi.fn(),
+ },
+ vi.fn(),
+ ],
+ Item: ({ children }: any) => {children}
,
+ }),
+ Steps: Object.assign(
+ ({ children, current, className }: any) => (
+
+ {children}
+
+ ),
+ {
+ Step: ({ title }: any) => {title}
,
+ },
+ ),
+ Button: ({ children, onClick, disabled, loading, ...props }: any) => (
+
+ ),
+ Checkbox: ({ checked, indeterminate, onChange, children, disabled }: any) => (
+
+ ),
+}));
+
+// Additional @tremor/react mocks (Button is already mocked globally)
+vi.mock("@tremor/react", async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ Text: ({ children, className }: any) => {children},
+ Title: ({ children }: any) => {children}
,
+ Badge: ({ children, color, size }: any) => (
+
+ {children}
+
+ ),
+ };
+});
+
+describe("MakeMCPPublicForm", () => {
+ const mockProps = {
+ visible: true,
+ onClose: vi.fn(),
+ accessToken: "test-token",
+ mcpHubData: [
+ {
+ server_id: "server-1",
+ server_name: "Test Server 1",
+ description: "Description 1",
+ url: "http://example.com/server1",
+ transport: "http",
+ status: "active",
+ mcp_info: { is_public: false },
+ allowed_tools: ["tool-1", "tool-2"],
+ auth_type: "bearer",
+ credentials: {},
+ created_at: "2024-01-01T00:00:00Z",
+ created_by: "user1",
+ updated_at: "2024-01-01T00:00:00Z",
+ updated_by: "user1",
+ teams: [],
+ mcp_access_groups: [],
+ extra_headers: [],
+ static_headers: {},
+ args: [],
+ env: {},
+ },
+ {
+ server_id: "server-2",
+ server_name: "Test Server 2",
+ description: "Description 2",
+ url: "http://example.com/server2",
+ transport: "websocket",
+ status: "inactive",
+ mcp_info: { is_public: true },
+ allowed_tools: [],
+ auth_type: "none",
+ credentials: {},
+ created_at: "2024-01-01T00:00:00Z",
+ created_by: "user2",
+ updated_at: "2024-01-01T00:00:00Z",
+ updated_by: "user2",
+ teams: [],
+ mcp_access_groups: [],
+ extra_headers: [],
+ static_headers: {},
+ args: [],
+ env: {},
+ },
+ ] as MCPServerData[],
+ onSuccess: vi.fn(),
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ vi.resetAllMocks();
+ });
+
+ it("should render the component", () => {
+ render();
+
+ expect(screen.getByText("Make MCP Servers Public")).toBeInTheDocument();
+ expect(screen.getByText("Select MCP Servers to Make Public")).toBeInTheDocument();
+ });
+
+ it("should initialize with correct state", () => {
+ render();
+
+ // Check that the component renders with the correct title and content
+ expect(screen.getByText("Make MCP Servers Public")).toBeInTheDocument();
+ expect(screen.getByText("Select MCP Servers to Make Public")).toBeInTheDocument();
+
+ // Check that all server checkboxes are present
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(3); // Select all + 2 servers
+
+ // Check that the Next button is enabled (servers are preselected)
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ expect(nextButton).not.toBeDisabled();
+ });
+
+ it("should handle server selection and navigation", async () => {
+ render();
+
+ // Initially on step 1
+ expect(screen.getByText("Select MCP Servers to Make Public")).toBeInTheDocument();
+
+ // Select all servers using the select all checkbox
+ const selectAllCheckbox = screen.getByLabelText("Select All (2)");
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // Verify Next button is enabled
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ expect(nextButton).not.toBeDisabled();
+
+ // Click Next
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Should move to step 2
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making MCP Servers Public")).toBeInTheDocument();
+ });
+ });
+
+ it("should submit selected servers successfully", async () => {
+ mockMakeMCPPublicCall.mockResolvedValueOnce({});
+
+ render();
+
+ // Select all servers
+ const selectAllCheckbox = screen.getByLabelText("Select All (2)");
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // Navigate to confirm step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Wait for navigation to complete
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making MCP Servers Public")).toBeInTheDocument();
+ });
+
+ // Submit
+ const submitButton = screen.getByRole("button", { name: "Make Public" });
+ await act(async () => {
+ fireEvent.click(submitButton);
+ });
+
+ await waitFor(() => {
+ expect(mockMakeMCPPublicCall).toHaveBeenCalledWith("test-token", ["server-1", "server-2"]);
+ expect(mockProps.onSuccess).toHaveBeenCalled();
+ expect(mockProps.onClose).toHaveBeenCalled();
+ });
+ });
+
+ it("should handle select all functionality", async () => {
+ render();
+
+ const checkboxes = screen.getAllByRole("checkbox");
+ const selectAllCheckbox = checkboxes[0];
+
+ // Select all
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // All checkboxes should be checked
+ checkboxes.forEach((checkbox) => {
+ expect(checkbox).toBeChecked();
+ });
+
+ // Deselect all
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // All checkboxes should be unchecked except the indeterminate state
+ expect(checkboxes[0]).not.toBeChecked();
+ expect(checkboxes[1]).not.toBeChecked();
+ expect(checkboxes[2]).not.toBeChecked();
+ });
+
+ it("should show error when no servers selected", async () => {
+ render();
+
+ // Deselect all servers first
+ const checkboxes = screen.getAllByRole("checkbox");
+ await act(async () => {
+ fireEvent.click(checkboxes[0]); // Click select all to select all
+ fireEvent.click(checkboxes[0]); // Click select all again to deselect all
+ });
+
+ // Try to go to next step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Should stay on same step
+ expect(screen.getByText("Select MCP Servers to Make Public")).toBeInTheDocument();
+ });
+
+ it("should display empty state when no servers are available", () => {
+ const emptyProps = {
+ ...mockProps,
+ mcpHubData: [] as MCPServerData[],
+ };
+
+ render();
+
+ expect(screen.getByText("No MCP servers available.")).toBeInTheDocument();
+
+ // Select All checkbox should be disabled
+ const selectAllCheckbox = screen.getByLabelText("Select All");
+ expect(selectAllCheckbox).toBeDisabled();
+
+ // Next button should be disabled
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ expect(nextButton).toBeDisabled();
+ });
+
+ it("should handle Cancel button functionality", async () => {
+ render();
+
+ // Click Cancel button
+ const cancelButton = screen.getByRole("button", { name: "Cancel" });
+ await act(async () => {
+ fireEvent.click(cancelButton);
+ });
+
+ // Should call onClose
+ expect(mockProps.onClose).toHaveBeenCalled();
+ });
+
+ it("should handle Previous button functionality", async () => {
+ render();
+
+ // Navigate to step 1
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Verify we're on step 1
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making MCP Servers Public")).toBeInTheDocument();
+ });
+
+ // Click Previous button
+ const previousButton = screen.getByRole("button", { name: "Previous" });
+ await act(async () => {
+ fireEvent.click(previousButton);
+ });
+
+ // Should go back to step 0
+ expect(screen.getByText("Select MCP Servers to Make Public")).toBeInTheDocument();
+ });
+
+ it("should handle individual server selection", async () => {
+ render();
+
+ // Get all checkboxes (select all + individual servers)
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(3); // Select all + 2 servers
+
+ // Initially, server-2 should be selected (it's already public)
+ const server1Checkbox = checkboxes[1]; // First server checkbox
+ const server2Checkbox = checkboxes[2]; // Second server checkbox
+
+ expect(server2Checkbox).toBeChecked(); // server-2 is already public
+
+ // Select server-1
+ await act(async () => {
+ fireEvent.click(server1Checkbox);
+ });
+
+ expect(server1Checkbox).toBeChecked();
+ expect(server2Checkbox).toBeChecked();
+
+ // Deselect server-2
+ await act(async () => {
+ fireEvent.click(server2Checkbox);
+ });
+
+ expect(server1Checkbox).toBeChecked();
+ expect(server2Checkbox).not.toBeChecked();
+
+ // Select all should be indeterminate now
+ const selectAllCheckbox = checkboxes[0];
+ expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
+ });
+
+ it("should display tools overflow text when server has more than 3 tools", () => {
+ const serverWithManyTools = {
+ ...mockProps.mcpHubData[0],
+ allowed_tools: ["tool-1", "tool-2", "tool-3", "tool-4", "tool-5"],
+ };
+
+ const propsWithManyTools = {
+ ...mockProps,
+ mcpHubData: [serverWithManyTools],
+ };
+
+ render();
+
+ // Should show first 3 tools as badges
+ expect(screen.getByText("tool-1")).toBeInTheDocument();
+ expect(screen.getByText("tool-2")).toBeInTheDocument();
+ expect(screen.getByText("tool-3")).toBeInTheDocument();
+
+ // Should show "+2 more" text for the remaining tools
+ expect(screen.getByText("+2 more")).toBeInTheDocument();
+ });
+
+ it("should handle submit error properly", async () => {
+ const errorMessage = "Network error";
+ mockMakeMCPPublicCall.mockRejectedValueOnce(new Error(errorMessage));
+
+ render();
+
+ // Navigate to confirm step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making MCP Servers Public")).toBeInTheDocument();
+ });
+
+ // Submit
+ const submitButton = screen.getByRole("button", { name: "Make Public" });
+ await act(async () => {
+ fireEvent.click(submitButton);
+ });
+
+ // Should handle error and show error notification
+ await waitFor(() => {
+ expect(mockMakeMCPPublicCall).toHaveBeenCalledWith("test-token", ["server-2"]);
+ });
+
+ // Should not call onSuccess or onClose on error
+ expect(mockProps.onSuccess).not.toHaveBeenCalled();
+ expect(mockProps.onClose).not.toHaveBeenCalled();
+ });
+
+ it("should show loading state during submit", async () => {
+ let resolvePromise: (value: any) => void = () => {};
+ const pendingPromise = new Promise((resolve) => {
+ resolvePromise = resolve;
+ });
+ mockMakeMCPPublicCall.mockReturnValueOnce(pendingPromise);
+
+ render();
+
+ // Navigate to confirm step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making MCP Servers Public")).toBeInTheDocument();
+ });
+
+ // Submit
+ const submitButton = screen.getByRole("button", { name: "Make Public" });
+ await act(async () => {
+ fireEvent.click(submitButton);
+ });
+
+ // Check loading state
+ expect(submitButton).toHaveAttribute("data-loading", "true");
+ expect(submitButton).toBeDisabled();
+
+ // Resolve the promise
+ resolvePromise({});
+ await waitFor(() => {
+ expect(mockProps.onSuccess).toHaveBeenCalled();
+ expect(mockProps.onClose).toHaveBeenCalled();
+ });
+ });
+
+ it("should not render modal when visible is false", () => {
+ const invisibleProps = {
+ ...mockProps,
+ visible: false,
+ };
+
+ render();
+
+ // Modal should not be rendered
+ expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
+ expect(screen.queryByText("Make MCP Servers Public")).not.toBeInTheDocument();
+ });
+
+ it("should preselect already public servers when modal opens", () => {
+ // Test data where one server is public and one is not
+ const mixedPublicProps = {
+ ...mockProps,
+ mcpHubData: [
+ {
+ server_id: "server-1",
+ server_name: "Test Server 1",
+ description: "Description 1",
+ url: "http://example.com/server1",
+ transport: "http",
+ status: "active",
+ mcp_info: { is_public: false }, // Not public
+ allowed_tools: [],
+ auth_type: "bearer",
+ credentials: {},
+ created_at: "2024-01-01T00:00:00Z",
+ created_by: "user1",
+ updated_at: "2024-01-01T00:00:00Z",
+ updated_by: "user1",
+ teams: [],
+ mcp_access_groups: [],
+ extra_headers: [],
+ static_headers: {},
+ args: [],
+ env: {},
+ },
+ {
+ server_id: "server-2",
+ server_name: "Test Server 2",
+ description: "Description 2",
+ url: "http://example.com/server2",
+ transport: "websocket",
+ status: "inactive",
+ mcp_info: { is_public: true }, // Already public
+ allowed_tools: [],
+ auth_type: "none",
+ credentials: {},
+ created_at: "2024-01-01T00:00:00Z",
+ created_by: "user2",
+ updated_at: "2024-01-01T00:00:00Z",
+ updated_by: "user2",
+ teams: [],
+ mcp_access_groups: [],
+ extra_headers: [],
+ static_headers: {},
+ args: [],
+ env: {},
+ },
+ {
+ server_id: "server-3",
+ server_name: "Test Server 3",
+ description: "Description 3",
+ url: "http://example.com/server3",
+ transport: "sse",
+ status: "healthy",
+ mcp_info: { is_public: true }, // Already public
+ allowed_tools: [],
+ auth_type: "oauth",
+ credentials: {},
+ created_at: "2024-01-01T00:00:00Z",
+ created_by: "user3",
+ updated_at: "2024-01-01T00:00:00Z",
+ updated_by: "user3",
+ teams: [],
+ mcp_access_groups: [],
+ extra_headers: [],
+ static_headers: {},
+ args: [],
+ env: {},
+ },
+ ] as MCPServerData[],
+ };
+
+ render();
+
+ // Check that the correct checkboxes are selected
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(4); // Select all + 3 servers
+
+ // server-2 and server-3 should be checked (they're already public)
+ const server1Checkbox = checkboxes[1];
+ const server2Checkbox = checkboxes[2];
+ const server3Checkbox = checkboxes[3];
+
+ expect(server1Checkbox).not.toBeChecked(); // server-1 is not public
+ expect(server2Checkbox).toBeChecked(); // server-2 is public
+ expect(server3Checkbox).toBeChecked(); // server-3 is public
+
+ // Select all should be indeterminate
+ const selectAllCheckbox = checkboxes[0];
+ expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/make_mcp_public_form.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.tsx
similarity index 100%
rename from ui/litellm-dashboard/src/components/AIHub/forms/make_mcp_public_form.tsx
rename to ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.tsx
diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.test.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.test.tsx
new file mode 100644
index 00000000000..2b57535f3ad
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.test.tsx
@@ -0,0 +1,557 @@
+import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import MakeModelPublicForm from "./MakeModelPublicForm";
+
+interface ModelGroupInfo {
+ model_group: string;
+ providers: string[];
+ max_input_tokens?: number;
+ max_output_tokens?: number;
+ input_cost_per_token?: number;
+ output_cost_per_token?: number;
+ mode?: string;
+ tpm?: number;
+ rpm?: number;
+ supports_parallel_function_calling: boolean;
+ supports_vision: boolean;
+ supports_function_calling: boolean;
+ supported_openai_params?: string[];
+ is_public_model_group: boolean;
+ [key: string]: any;
+}
+
+// Mock the networking function
+vi.mock("../../networking", () => ({
+ makeModelGroupPublic: vi.fn(),
+}));
+
+// Import the mocked function
+import { makeModelGroupPublic } from "../../networking";
+const mockMakeModelGroupPublic = vi.mocked(makeModelGroupPublic);
+
+// Mock antd components
+vi.mock("antd", () => ({
+ Modal: ({ open, title, children, onCancel, footer }: any) =>
+ open ? (
+
+
{title}
+ {children}
+ {footer}
+
+ ) : null,
+ Form: Object.assign(({ children, form }: any) => , {
+ useForm: () => [
+ {
+ resetFields: vi.fn(),
+ validateFields: vi.fn(),
+ getFieldsValue: vi.fn(),
+ setFieldsValue: vi.fn(),
+ },
+ vi.fn(),
+ ],
+ Item: ({ children }: any) => {children}
,
+ }),
+ Steps: Object.assign(
+ ({ children, current, className }: any) => (
+
+ {children}
+
+ ),
+ {
+ Step: ({ title }: any) => {title}
,
+ },
+ ),
+ Button: ({ children, onClick, disabled, loading, ...props }: any) => (
+
+ ),
+ Checkbox: ({ checked, indeterminate, onChange, children, disabled }: any) => (
+
+ ),
+}));
+
+// Mock @tremor/react components
+vi.mock("@tremor/react", () => ({
+ Text: ({ children, className }: any) => {children},
+ Title: ({ children }: any) => {children}
,
+ Badge: ({ children, color, size }: any) => (
+
+ {children}
+
+ ),
+}));
+
+// Mock ModelFilters component
+vi.mock("../../model_filters", () => ({
+ default: ({ onFilteredDataChange, modelHubData }: any) => (
+
+
+
+ ),
+}));
+
+// Mock NotificationsManager
+vi.mock("../../molecules/notifications_manager", () => ({
+ default: {
+ fromBackend: vi.fn(),
+ success: vi.fn(),
+ },
+}));
+
+describe("MakeModelPublicForm", () => {
+ const mockProps = {
+ visible: true,
+ onClose: vi.fn(),
+ accessToken: "test-token",
+ modelHubData: [
+ {
+ model_group: "gpt-4",
+ providers: ["openai"],
+ max_input_tokens: 8192,
+ max_output_tokens: 4096,
+ input_cost_per_token: 0.03,
+ output_cost_per_token: 0.06,
+ mode: "chat",
+ tpm: 10000,
+ rpm: 200,
+ supports_parallel_function_calling: true,
+ supports_vision: false,
+ supports_function_calling: true,
+ supported_openai_params: ["temperature", "max_tokens"],
+ is_public_model_group: false,
+ },
+ {
+ model_group: "gpt-3.5-turbo",
+ providers: ["openai"],
+ max_input_tokens: 4096,
+ max_output_tokens: 2048,
+ input_cost_per_token: 0.0015,
+ output_cost_per_token: 0.002,
+ mode: "chat",
+ tpm: 60000,
+ rpm: 3500,
+ supports_parallel_function_calling: false,
+ supports_vision: false,
+ supports_function_calling: true,
+ supported_openai_params: ["temperature", "max_tokens"],
+ is_public_model_group: true,
+ },
+ ] as ModelGroupInfo[],
+ onSuccess: vi.fn(),
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ vi.resetAllMocks();
+ });
+
+ it("should render the component", () => {
+ render();
+
+ expect(screen.getByText("Make Models Public")).toBeInTheDocument();
+ expect(screen.getByText("Select Models to Make Public")).toBeInTheDocument();
+ });
+
+ it("should initialize with correct state", () => {
+ render();
+
+ // Check that the component renders with the correct title and content
+ expect(screen.getByText("Make Models Public")).toBeInTheDocument();
+ expect(screen.getByText("Select Models to Make Public")).toBeInTheDocument();
+
+ // Check that all model checkboxes are present
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(3); // Select all + 2 models
+
+ // Check that the Next button is enabled (models are preselected)
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ expect(nextButton).not.toBeDisabled();
+ });
+
+ it("should handle model selection and navigation", async () => {
+ render();
+
+ // Initially on step 1
+ expect(screen.getByText("Select Models to Make Public")).toBeInTheDocument();
+
+ // Select all models using the select all checkbox
+ const selectAllCheckbox = screen.getByLabelText("Select All (2)");
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // Verify Next button is enabled
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ expect(nextButton).not.toBeDisabled();
+
+ // Click Next
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Should move to step 2
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument();
+ });
+ });
+
+ it("should submit selected models successfully", async () => {
+ mockMakeModelGroupPublic.mockResolvedValueOnce({});
+
+ render();
+
+ // Select all models
+ const selectAllCheckbox = screen.getByLabelText("Select All (2)");
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // Navigate to confirm step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Wait for navigation to complete
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument();
+ });
+
+ // Submit
+ const submitButton = screen.getByRole("button", { name: "Make Public" });
+ await act(async () => {
+ fireEvent.click(submitButton);
+ });
+
+ await waitFor(() => {
+ expect(mockMakeModelGroupPublic).toHaveBeenCalledWith("test-token", ["gpt-4", "gpt-3.5-turbo"]);
+ expect(mockProps.onSuccess).toHaveBeenCalled();
+ expect(mockProps.onClose).toHaveBeenCalled();
+ });
+ });
+
+ it("should handle select all functionality", async () => {
+ render();
+
+ const checkboxes = screen.getAllByRole("checkbox");
+ const selectAllCheckbox = checkboxes[0];
+
+ // Select all
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // All checkboxes should be checked
+ checkboxes.forEach((checkbox) => {
+ expect(checkbox).toBeChecked();
+ });
+
+ // Deselect all
+ await act(async () => {
+ fireEvent.click(selectAllCheckbox);
+ });
+
+ // All checkboxes should be unchecked except the indeterminate state
+ expect(checkboxes[0]).not.toBeChecked();
+ expect(checkboxes[1]).not.toBeChecked();
+ expect(checkboxes[2]).not.toBeChecked();
+ });
+
+ it("should show error when no models selected", async () => {
+ render();
+
+ // Deselect all models first
+ const checkboxes = screen.getAllByRole("checkbox");
+ await act(async () => {
+ fireEvent.click(checkboxes[0]); // Click select all to select all
+ fireEvent.click(checkboxes[0]); // Click select all again to deselect all
+ });
+
+ // Try to go to next step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Should stay on same step
+ expect(screen.getByText("Select Models to Make Public")).toBeInTheDocument();
+ });
+
+ it("should display empty state when no models are available", () => {
+ const emptyProps = {
+ ...mockProps,
+ modelHubData: [] as ModelGroupInfo[],
+ };
+
+ render();
+
+ expect(screen.getByText("No models match the current filters.")).toBeInTheDocument();
+
+ // Select All checkbox should be disabled
+ const selectAllCheckbox = screen.getByLabelText("Select All");
+ expect(selectAllCheckbox).toBeDisabled();
+
+ // Next button should be disabled
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ expect(nextButton).toBeDisabled();
+ });
+
+ it("should handle Cancel button functionality", async () => {
+ render();
+
+ // Click Cancel button
+ const cancelButton = screen.getByRole("button", { name: "Cancel" });
+ await act(async () => {
+ fireEvent.click(cancelButton);
+ });
+
+ // Should call onClose
+ expect(mockProps.onClose).toHaveBeenCalled();
+ });
+
+ it("should handle Previous button functionality", async () => {
+ render();
+
+ // Navigate to step 1
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ // Verify we're on step 1
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument();
+ });
+
+ // Click Previous button
+ const previousButton = screen.getByRole("button", { name: "Previous" });
+ await act(async () => {
+ fireEvent.click(previousButton);
+ });
+
+ // Should go back to step 0
+ expect(screen.getByText("Select Models to Make Public")).toBeInTheDocument();
+ });
+
+ it("should handle individual model selection", async () => {
+ render();
+
+ // Get all checkboxes (select all + individual models)
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(3); // Select all + 2 models
+
+ // Initially, gpt-3.5-turbo should be selected (it's already public)
+ const gpt4Checkbox = checkboxes[1]; // First model checkbox
+ const gpt35Checkbox = checkboxes[2]; // Second model checkbox
+
+ expect(gpt35Checkbox).toBeChecked(); // gpt-3.5-turbo is already public
+
+ // Select gpt-4
+ await act(async () => {
+ fireEvent.click(gpt4Checkbox);
+ });
+
+ expect(gpt4Checkbox).toBeChecked();
+ expect(gpt35Checkbox).toBeChecked();
+
+ // Deselect gpt-3.5-turbo
+ await act(async () => {
+ fireEvent.click(gpt35Checkbox);
+ });
+
+ expect(gpt4Checkbox).toBeChecked();
+ expect(gpt35Checkbox).not.toBeChecked();
+
+ // Select all should be indeterminate now
+ const selectAllCheckbox = checkboxes[0];
+ expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
+ });
+
+ it("should display model badges and information", () => {
+ render();
+
+ // Should show model names
+ expect(screen.getByText("gpt-4")).toBeInTheDocument();
+ expect(screen.getByText("gpt-3.5-turbo")).toBeInTheDocument();
+
+ // Should show mode badges
+ expect(screen.getAllByText("chat")).toHaveLength(2);
+
+ // Should show provider badges
+ expect(screen.getAllByText("openai")).toHaveLength(2);
+ });
+
+ it("should handle submit error properly", async () => {
+ const errorMessage = "Network error";
+ mockMakeModelGroupPublic.mockRejectedValueOnce(new Error(errorMessage));
+
+ render();
+
+ // Navigate to confirm step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument();
+ });
+
+ // Submit
+ const submitButton = screen.getByRole("button", { name: "Make Public" });
+ await act(async () => {
+ fireEvent.click(submitButton);
+ });
+
+ // Should handle error and show error notification
+ await waitFor(() => {
+ expect(mockMakeModelGroupPublic).toHaveBeenCalledWith("test-token", ["gpt-3.5-turbo"]);
+ });
+
+ // Should not call onSuccess or onClose on error
+ expect(mockProps.onSuccess).not.toHaveBeenCalled();
+ expect(mockProps.onClose).not.toHaveBeenCalled();
+ });
+
+ it("should show loading state during submit", async () => {
+ let resolvePromise: (value: any) => void = () => {};
+ const pendingPromise = new Promise((resolve) => {
+ resolvePromise = resolve;
+ });
+ mockMakeModelGroupPublic.mockReturnValueOnce(pendingPromise);
+
+ render();
+
+ // Navigate to confirm step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument();
+ });
+
+ // Submit
+ const submitButton = screen.getByRole("button", { name: "Make Public" });
+ await act(async () => {
+ fireEvent.click(submitButton);
+ });
+
+ // Check loading state
+ expect(submitButton).toHaveAttribute("data-loading", "true");
+ expect(submitButton).toBeDisabled();
+
+ // Resolve the promise
+ resolvePromise({});
+ await waitFor(() => {
+ expect(mockProps.onSuccess).toHaveBeenCalled();
+ expect(mockProps.onClose).toHaveBeenCalled();
+ });
+ });
+
+ it("should not render modal when visible is false", () => {
+ const invisibleProps = {
+ ...mockProps,
+ visible: false,
+ };
+
+ render();
+
+ // Modal should not be rendered
+ expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
+ expect(screen.queryByText("Make Models Public")).not.toBeInTheDocument();
+ });
+
+ it("should preselect already public models when modal opens", () => {
+ // Test data where one model is public and one is not
+ const mixedPublicProps = {
+ ...mockProps,
+ modelHubData: [
+ {
+ model_group: "private-model",
+ providers: ["openai"],
+ is_public_model_group: false,
+ mode: "chat",
+ },
+ {
+ model_group: "public-model",
+ providers: ["anthropic"],
+ is_public_model_group: true,
+ mode: "completion",
+ },
+ {
+ model_group: "another-public-model",
+ providers: ["cohere"],
+ is_public_model_group: true,
+ mode: "chat",
+ },
+ ] as ModelGroupInfo[],
+ };
+
+ render();
+
+ // Check that the correct checkboxes are selected
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(4); // Select all + 3 models
+
+ // private-model should not be checked, public models should be checked
+ const privateModelCheckbox = checkboxes[1];
+ const publicModelCheckbox = checkboxes[2];
+ const anotherPublicModelCheckbox = checkboxes[3];
+
+ expect(privateModelCheckbox).not.toBeChecked(); // private-model is not public
+ expect(publicModelCheckbox).toBeChecked(); // public-model is public
+ expect(anotherPublicModelCheckbox).toBeChecked(); // another-public-model is public
+
+ // Select all should be indeterminate
+ const selectAllCheckbox = checkboxes[0];
+ expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
+ });
+
+ it("should show selected count", () => {
+ render();
+
+ // Should show that 1 model is selected (gpt-3.5-turbo is preselected)
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.getByText("model selected")).toBeInTheDocument();
+ });
+
+ it("should show confirmation step with selected models", async () => {
+ render();
+
+ // Navigate to confirm step
+ const nextButton = screen.getByRole("button", { name: "Next" });
+ await act(async () => {
+ fireEvent.click(nextButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument();
+ });
+
+ // Should show the selected model
+ expect(screen.getByText("gpt-3.5-turbo")).toBeInTheDocument();
+
+ // Should show the warning message
+ expect(screen.getByText(/Warning:/)).toBeInTheDocument();
+ expect(screen.getByText(/model_hub_table/)).toBeInTheDocument();
+
+ // Should show total count (already verified by checking the presence of the confirmation step)
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/make_model_public_form.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.tsx
similarity index 100%
rename from ui/litellm-dashboard/src/components/AIHub/forms/make_model_public_form.tsx
rename to ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.tsx