diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index d9efd3808ab..08f0156a619 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -86,6 +86,7 @@ "src/components/AIHub/forms/MakeAgentPublicForm.tsx", "src/components/AIHub/forms/MakeMCPPublicForm.tsx", "src/components/AIHub/forms/MakeModelPublicForm.tsx", + "src/components/AIHub/ModelHubTable.tsx", "src/components/policies/policy_test_panel.tsx", "src/components/mcp_tools/McpCrudPermissionPanel.tsx", "src/components/mcp_tools/OAuthFormFields.tsx", diff --git a/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx index 94aeae4617a..a292a9452c3 100644 --- a/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx @@ -23,10 +23,17 @@ import { } from "@/components/networking"; import PublicModelHub from "@/components/public_model_hub"; import { isAdminRole } from "@/utils/roles"; -// eslint-disable-next-line litellm-ui/no-banned-ui-imports -import { Copy as CopyOutlined } from "lucide-react"; -import { Badge, Button, Card, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react"; -import { Modal } from "antd"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { cn } from "@/lib/utils"; import { Copy } from "lucide-react"; import { useRouter } from "next/navigation"; import React, { useCallback, useEffect, useState } from "react"; @@ -61,6 +68,26 @@ interface ModelGroupInfo { [key: string]: any; } +const BADGE_COLOR_CLASSES: Record = { + blue: "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300", + green: + "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300", + purple: + "bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300", + orange: + "bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300", + red: "bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300", + yellow: + "bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300", + gray: "bg-muted text-muted-foreground", +}; + +const colorBadge = ( + color: keyof typeof BADGE_COLOR_CLASSES | string, + extra?: string, +) => + cn(BADGE_COLOR_CLASSES[color] ?? BADGE_COLOR_CLASSES.gray, extra); + const ModelHubTable: React.FC = ({ accessToken, publicPage, premiumUser, userRole }) => { const [publicPageAllowed, setPublicPageAllowed] = useState(false); const [modelHubData, setModelHubData] = useState(null); @@ -258,8 +285,6 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, if (!accessToken) { return; } - - // Show the modal for selecting models to make public setIsMakePublicModalVisible(true); }; @@ -267,8 +292,6 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, if (!accessToken) { return; } - - // Show the modal for selecting agents to make public setIsMakeAgentPublicModalVisible(true); }; @@ -276,22 +299,10 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, if (!accessToken) { return; } - - // Show the modal for selecting MCP servers to make public setIsMakeMcpPublicModalVisible(true); }; - const handleOk = () => { - setIsModalVisible(false); - setIsPublicPageModalVisible(false); - setSelectedModel(null); - setIsAgentModalVisible(false); - setSelectedAgent(null); - setIsMcpModalVisible(false); - setSelectedMcpServer(null); - }; - - const handleCancel = () => { + const closeDetailDialogs = () => { setIsModalVisible(false); setIsPublicPageModalVisible(false); setSelectedModel(null); @@ -307,7 +318,6 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, }; const formatCapabilityName = (key: string) => { - // Remove 'supports_' prefix and convert snake_case to Title Case return key .replace(/^supports_/, "") .split("_") @@ -316,7 +326,6 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, }; const getModelCapabilities = (model: ModelGroupInfo) => { - // Find all properties that start with 'supports_' and are true return Object.entries(model) .filter(([key, value]) => key.startsWith("supports_") && value === true) .map(([key]) => key); @@ -327,7 +336,6 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, }; const handleMakePublicSuccess = () => { - // Refresh the model hub data after successful public operation if (accessToken) { const fetchData = async () => { try { @@ -342,7 +350,6 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, }; const handleMakeAgentPublicSuccess = () => { - // Refresh the agent hub data after successful public operation if (accessToken) { const fetchAgentData = async () => { try { @@ -363,7 +370,6 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, }; const handleMakeMcpPublicSuccess = () => { - // Refresh the MCP hub data after successful public operation if (accessToken) { const fetchMcpData = async () => { try { @@ -396,25 +402,25 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, {/* Header with Title, Description and URL */}
- AI Hub +

AI Hub

{isAdminRole(userRole || "") ? ( -

+

Make models, agents, and MCP servers public for developers to know what's available.

) : ( -

A list of all public model names personally available to you.

+

A list of all public model names personally available to you.

)}
- Model Hub URL: -
- {`${getProxyBaseUrl()}/ui/model_hub_table`} + Model Hub URL: +
+ {`${getProxyBaseUrl()}/ui/model_hub_table`}
@@ -428,278 +434,271 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, )} {/* Tab System for Model Hub, Agent Hub, MCP Hub, and Plugin Marketplace */} - - - Model Hub - Agent Hub - MCP Hub - Skill Hub - + + + Model Hub + Agent Hub + MCP Hub + Skill Hub + - - {/* Model Hub Tab */} - - {/* Model Filters and Table */} - - {/* Header with Make Public Button */} - {publicPage == false && isAdminRole(userRole || "") && ( -
- -
- )} - - {/* Filters */} - - - {/* Model Table */} - -
- -
- - Showing {filteredData.length} of {modelHubData?.length || 0} models - -
-
- - {/* Agent Hub Tab */} - - - {/* Header with Make Public Button */} - {publicPage == false && isAdminRole(userRole || "") && ( -
- -
- )} - - {/* Agent Table */} - -
- -
- - Showing {agentHubData?.length || 0} agent{agentHubData?.length !== 1 ? "s" : ""} - -
-
- - {/* MCP Hub Tab */} - - - {/* Header with Make Public Button */} - {publicPage == false && isAdminRole(userRole || "") && ( -
- -
- )} - - {/* MCP Server Table */} - -
- -
- - Showing {mcpHubData?.length || 0} MCP server{mcpHubData?.length !== 1 ? "s" : ""} - -
-
- - {/* Skill Hub Tab */} - + {/* Model Hub Tab */} + + {publicPage == false && isAdminRole(userRole || "") && (
- +
)} - { - const response = await getClaudeCodePluginsList(accessToken || "", publicPage); - setSkillHubData(response.plugins); - }} + + + + -
-
-
+ + +
+ + Showing {filteredData.length} of {modelHubData?.length || 0} models + +
+ + + {/* Agent Hub Tab */} + + + {publicPage == false && isAdminRole(userRole || "") && ( +
+ +
+ )} + + +
+ +
+ + Showing {agentHubData?.length || 0} agent{agentHubData?.length !== 1 ? "s" : ""} + +
+
+ + {/* MCP Hub Tab */} + + + {publicPage == false && isAdminRole(userRole || "") && ( +
+ +
+ )} + + +
+ +
+ + Showing {mcpHubData?.length || 0} MCP server{mcpHubData?.length !== 1 ? "s" : ""} + +
+
+ + {/* Skill Hub Tab */} + + {publicPage == false && isAdminRole(userRole || "") && ( +
+ +
+ )} + { + const response = await getClaudeCodePluginsList(accessToken || "", publicPage); + setSkillHubData(response.plugins); + }} + /> +
+
) : ( - - Public Model Hub not enabled. -

Ask your proxy admin to enable this on their Admin UI.

+ +

Public Model Hub not enabled.

+

+ Ask your proxy admin to enable this on their Admin UI. +

)} {/* Public Page Modal */} - (!o ? closeDetailDialogs() : undefined)} > -
-
- Shareable Link: - - {`${getProxyBaseUrl()}/ui/model_hub_table`} - + + + Public Model Hub + +
+
+ Shareable Link: + + {`${getProxyBaseUrl()}/ui/model_hub_table`} + +
+
+ +
-
- -
-
- + + {/* Model Details Modal */} - (!o ? closeDetailDialogs() : undefined)} > - {selectedModel && ( -
- {/* Model Overview */} -
- Model Overview -
-
- Model Group: - {selectedModel.model_group} + + + {selectedModel?.model_group || "Model Details"} + + {selectedModel && ( +
+ {/* Model Overview */} +
+

Model Overview

+
+
+

Model Group:

+

{selectedModel.model_group}

+
+
+

Mode:

+

{selectedModel.mode || "Not specified"}

+
+
+

Providers:

+
+ {selectedModel.providers.map((provider) => ( + + {provider} + + ))} +
+
-
- Mode: - {selectedModel.mode || "Not specified"} +
+ + {/* Token and Cost Information */} +
+

Token & Cost Information

+
+
+

Max Input Tokens:

+

{selectedModel.max_input_tokens?.toLocaleString() || "Not specified"}

+
+
+

Max Output Tokens:

+

{selectedModel.max_output_tokens?.toLocaleString() || "Not specified"}

+
+
+

Input Cost per 1M Tokens:

+

+ {selectedModel.input_cost_per_token + ? formatCost(selectedModel.input_cost_per_token) + : "Not specified"} +

+
+
+

Output Cost per 1M Tokens:

+

+ {selectedModel.output_cost_per_token + ? formatCost(selectedModel.output_cost_per_token) + : "Not specified"} +

+
+
+ + {/* Capabilities */} +
+

Capabilities

+
+ {(() => { + const capabilities = getModelCapabilities(selectedModel); + const colors = ["green", "blue", "purple", "orange", "red", "yellow"]; + + if (capabilities.length === 0) { + return

No special capabilities listed

; + } + + return capabilities.map((capability, index) => ( + + {formatCapabilityName(capability)} + + )); + })()} +
+
+ + {/* Rate Limits */} + {(selectedModel.tpm || selectedModel.rpm) && (
- Providers: -
- {selectedModel.providers.map((provider) => ( - - {provider} +

Rate Limits

+
+ {selectedModel.tpm && ( +
+

Tokens per Minute:

+

{selectedModel.tpm.toLocaleString()}

+
+ )} + {selectedModel.rpm && ( +
+

Requests per Minute:

+

{selectedModel.rpm.toLocaleString()}

+
+ )} +
+
+ )} + + {/* Supported OpenAI Parameters */} + {selectedModel.supported_openai_params && ( +
+

Supported OpenAI Parameters

+
+ {selectedModel.supported_openai_params.map((param) => ( + + {param} ))}
-
-
+ )} - {/* Token and Cost Information */} -
- Token & Cost Information -
-
- Max Input Tokens: - {selectedModel.max_input_tokens?.toLocaleString() || "Not specified"} -
-
- Max Output Tokens: - {selectedModel.max_output_tokens?.toLocaleString() || "Not specified"} -
-
- Input Cost per 1M Tokens: - - {selectedModel.input_cost_per_token - ? formatCost(selectedModel.input_cost_per_token) - : "Not specified"} - -
-
- Output Cost per 1M Tokens: - - {selectedModel.output_cost_per_token - ? formatCost(selectedModel.output_cost_per_token) - : "Not specified"} - -
-
-
- - {/* Capabilities */} -
- Capabilities -
- {(() => { - const capabilities = getModelCapabilities(selectedModel); - const colors = ["green", "blue", "purple", "orange", "red", "yellow"]; - - if (capabilities.length === 0) { - return No special capabilities listed; - } - - return capabilities.map((capability, index) => ( - - {formatCapabilityName(capability)} - - )); - })()} -
-
- - {/* Rate Limits */} - {(selectedModel.tpm || selectedModel.rpm) && ( + {/* Usage Example */}
- Rate Limits -
- {selectedModel.tpm && ( -
- Tokens per Minute: - {selectedModel.tpm.toLocaleString()} -
- )} - {selectedModel.rpm && ( -
- Requests per Minute: - {selectedModel.rpm.toLocaleString()} -
- )} -
-
- )} - - {/* Supported OpenAI Parameters */} - {selectedModel.supported_openai_params && ( -
- Supported OpenAI Parameters -
- {selectedModel.supported_openai_params.map((param) => ( - - {param} - - ))} -
-
- )} - - {/* Usage Example */} -
- Usage Example - - {`import openai +

Usage Example

+ + {`import openai client = openai.OpenAI( api_key="your_api_key", @@ -717,324 +716,333 @@ response = client.chat.completions.create( ) print(response.choices[0].message.content)`} - +
+
-
- )} - + )} + + {/* Agent Details Modal */} - (!o ? closeDetailDialogs() : undefined)} > - {selectedAgent && ( -
- {/* Agent Overview */} -
- Agent Overview -
-
- Name: - {selectedAgent.name} -
-
- Version: - v{selectedAgent.version} -
-
- Protocol Version: - {selectedAgent.protocolVersion} -
-
- URL: -
- {selectedAgent.url} - copyToClipboard(selectedAgent.url)} - className="cursor-pointer text-gray-500 hover:text-blue-500" - /> + + + {selectedAgent?.name || "Agent Details"} + + {selectedAgent && ( +
+ {/* Agent Overview */} +
+

Agent Overview

+
+
+

Name:

+

{selectedAgent.name}

-
-
-
- Description: - {selectedAgent.description} -
-
- - {/* Capabilities */} - {selectedAgent.capabilities && Object.keys(selectedAgent.capabilities).length > 0 && ( -
- Capabilities -
- {Object.entries(selectedAgent.capabilities) - .filter(([_, value]) => value === true) - .map(([key]) => ( - - {key} - - ))} -
-
- )} - - {/* Input/Output Modes */} -
- Input/Output Modes -
-
- Input Modes: -
- {selectedAgent.defaultInputModes?.map((mode) => ( - - {mode} - - )) || Not specified} +
+

Version:

+ v{selectedAgent.version} +
+
+

Protocol Version:

+

{selectedAgent.protocolVersion}

+
+
+

URL:

+
+ {selectedAgent.url} + copyToClipboard(selectedAgent.url)} + className="cursor-pointer text-muted-foreground hover:text-primary" + /> +
- Output Modes: -
- {selectedAgent.defaultOutputModes?.map((mode) => ( - - {mode} - - )) || Not specified} +

Description:

+

{selectedAgent.description}

+
+
+ + {/* Capabilities */} + {selectedAgent.capabilities && Object.keys(selectedAgent.capabilities).length > 0 && ( +
+

Capabilities

+
+ {Object.entries(selectedAgent.capabilities) + .filter(([_, value]) => value === true) + .map(([key]) => ( + + {key} + + ))} +
+
+ )} + + {/* Input/Output Modes */} +
+

Input/Output Modes

+
+
+

Input Modes:

+
+ {selectedAgent.defaultInputModes?.map((mode) => ( + + {mode} + + )) ||

Not specified

} +
+
+
+

Output Modes:

+
+ {selectedAgent.defaultOutputModes?.map((mode) => ( + + {mode} + + )) ||

Not specified

} +
-
- {/* Skills */} - {selectedAgent.skills && selectedAgent.skills.length > 0 && ( -
- Skills -
- {selectedAgent.skills.map((skill) => ( -
-
-
- {skill.name} - ID: {skill.id} + {/* Skills */} + {selectedAgent.skills && selectedAgent.skills.length > 0 && ( +
+

Skills

+
+ {selectedAgent.skills.map((skill) => ( +
+
+
+

{skill.name}

+

ID: {skill.id}

+
+ {skill.tags && skill.tags.length > 0 && ( +
+ {skill.tags.map((tag) => ( + + {tag} + + ))} +
+ )}
- {skill.tags && skill.tags.length > 0 && ( -
- {skill.tags.map((tag) => ( - - {tag} - - ))} +

{skill.description}

+ {skill.examples && skill.examples.length > 0 && ( +
+

Examples:

+
+ {skill.examples.map((example, idx) => ( + + {example} + + ))} +
)}
- {skill.description} - {skill.examples && skill.examples.length > 0 && ( -
- Examples: -
- {skill.examples.map((example, idx) => ( - - {example} - - ))} -
-
- )} -
- ))} + ))} +
-
- )} + )} - {/* Additional Properties */} - {selectedAgent.supportsAuthenticatedExtendedCard && ( -
- Additional Features - Supports Authenticated Extended Card -
- )} -
- )} - + {/* Additional Properties */} + {selectedAgent.supportsAuthenticatedExtendedCard && ( +
+

Additional Features

+ Supports Authenticated Extended Card +
+ )} +
+ )} + + {/* MCP Server Details Modal */} - (!o ? closeDetailDialogs() : undefined)} > - {selectedMcpServer && ( -
- {/* Server Overview */} -
- Server Overview -
-
- Server Name: - {selectedMcpServer.server_name} -
-
- Server ID: -
- {selectedMcpServer.server_id} - copyToClipboard(selectedMcpServer.server_id)} - className="cursor-pointer text-gray-500 hover:text-blue-500" - /> + + + {selectedMcpServer?.server_name || "MCP Server Details"} + + {selectedMcpServer && ( +
+ {/* Server Overview */} +
+

Server Overview

+
+
+

Server Name:

+

{selectedMcpServer.server_name}

+
+
+

Server ID:

+
+ {selectedMcpServer.server_id} + copyToClipboard(selectedMcpServer.server_id)} + className="cursor-pointer text-muted-foreground hover:text-primary" + /> +
+
+ {selectedMcpServer.alias && ( +
+

Alias:

+

{selectedMcpServer.alias}

+
+ )} +
+

Transport:

+ {selectedMcpServer.transport} +
+
+

Auth Type:

+ + {selectedMcpServer.auth_type} + +
+
+

Status:

+ + {selectedMcpServer.status || "unknown"} +
- {selectedMcpServer.alias && ( -
- Alias: - {selectedMcpServer.alias} + {selectedMcpServer.description && ( +
+

Description:

+

{selectedMcpServer.description}

)} -
- Transport: - {selectedMcpServer.transport} -
-
- Auth Type: - - {selectedMcpServer.auth_type} - -
-
- Status: - - {selectedMcpServer.status || "unknown"} - +
+ + {/* Connection Details */} +
+

Connection Details

+
+
+

URL:

+
+ {selectedMcpServer.url} + copyToClipboard(selectedMcpServer.url)} + className="cursor-pointer text-muted-foreground hover:text-primary flex-shrink-0" + /> +
+
+ {selectedMcpServer.command && ( +
+

Command:

+

{selectedMcpServer.command}

+
+ )}
- {selectedMcpServer.description && ( -
- Description: - {selectedMcpServer.description} + + {/* Tools */} + {selectedMcpServer.allowed_tools && selectedMcpServer.allowed_tools.length > 0 && ( +
+

Allowed Tools

+
+ {selectedMcpServer.allowed_tools.map((tool, idx) => ( + + {tool} + + ))} +
)} -
- {/* Connection Details */} -
- Connection Details -
+ {/* Teams */} + {selectedMcpServer.teams && selectedMcpServer.teams.length > 0 && (
- URL: -
- {selectedMcpServer.url} - copyToClipboard(selectedMcpServer.url)} - className="cursor-pointer text-gray-500 hover:text-blue-500 flex-shrink-0" - /> +

Teams

+
+ {selectedMcpServer.teams.map((team, idx) => ( + + {team} + + ))}
- {selectedMcpServer.command && ( -
- Command: - {selectedMcpServer.command} -
- )} -
-
- - {/* Tools */} - {selectedMcpServer.allowed_tools && selectedMcpServer.allowed_tools.length > 0 && ( -
- Allowed Tools -
- {selectedMcpServer.allowed_tools.map((tool, idx) => ( - - {tool} - - ))} -
-
- )} - - {/* Teams */} - {selectedMcpServer.teams && selectedMcpServer.teams.length > 0 && ( -
- Teams -
- {selectedMcpServer.teams.map((team, idx) => ( - - {team} - - ))} -
-
- )} - - {/* Access Groups */} - {selectedMcpServer.mcp_access_groups && selectedMcpServer.mcp_access_groups.length > 0 && ( -
- Access Groups -
- {selectedMcpServer.mcp_access_groups.map((group, idx) => ( - - {group} - - ))} -
-
- )} - - {/* Metadata */} -
- Metadata -
-
- Created By: - {selectedMcpServer.created_by} -
-
- Updated By: - {selectedMcpServer.updated_by} -
-
- Created At: - {new Date(selectedMcpServer.created_at).toLocaleString()} -
-
- Updated At: - {new Date(selectedMcpServer.updated_at).toLocaleString()} -
- {selectedMcpServer.last_health_check && ( -
- Last Health Check: - {new Date(selectedMcpServer.last_health_check).toLocaleString()} -
- )} -
- {selectedMcpServer.health_check_error && ( -
- Health Check Error: - {selectedMcpServer.health_check_error} -
)} -
- {/* Usage Example */} -
- Usage Example - - {`from fastmcp import Client + {/* Access Groups */} + {selectedMcpServer.mcp_access_groups && selectedMcpServer.mcp_access_groups.length > 0 && ( +
+

Access Groups

+
+ {selectedMcpServer.mcp_access_groups.map((group, idx) => ( + + {group} + + ))} +
+
+ )} + + {/* Metadata */} +
+

Metadata

+
+
+

Created By:

+

{selectedMcpServer.created_by}

+
+
+

Updated By:

+

{selectedMcpServer.updated_by}

+
+
+

Created At:

+

{new Date(selectedMcpServer.created_at).toLocaleString()}

+
+
+

Updated At:

+

{new Date(selectedMcpServer.updated_at).toLocaleString()}

+
+ {selectedMcpServer.last_health_check && ( +
+

Last Health Check:

+

{new Date(selectedMcpServer.last_health_check).toLocaleString()}

+
+ )} +
+ {selectedMcpServer.health_check_error && ( +
+

Health Check Error:

+

{selectedMcpServer.health_check_error}

+
+ )} +
+ + {/* Usage Example */} +
+

Usage Example

+ + {`from fastmcp import Client import asyncio # Standard MCP configuration @@ -1067,11 +1075,12 @@ async def main(): if __name__ == "__main__": asyncio.run(main())`} - + +
-
- )} - + )} + + {/* Make Model Public Form */} ({ 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) =>
{children}
, { - 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, @@ -123,15 +61,13 @@ describe("MakeAgentPublicForm", () => { 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 + // Select all + 2 agents const checkboxes = screen.getAllByRole("checkbox"); - expect(checkboxes).toHaveLength(3); // Select all + 2 agents + expect(checkboxes).toHaveLength(3); - // Check that the Next button is enabled (agents are preselected) const nextButton = screen.getByRole("button", { name: "Next" }); expect(nextButton).not.toBeDisabled(); }); @@ -139,25 +75,20 @@ describe("MakeAgentPublicForm", () => { 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(); }); @@ -168,24 +99,20 @@ describe("MakeAgentPublicForm", () => { 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); @@ -209,7 +136,6 @@ describe("MakeAgentPublicForm", () => { fireEvent.click(selectAllCheckbox); }); - // All checkboxes should be checked checkboxes.forEach((checkbox) => { expect(checkbox).toBeChecked(); }); @@ -219,7 +145,6 @@ describe("MakeAgentPublicForm", () => { 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(); @@ -228,20 +153,20 @@ describe("MakeAgentPublicForm", () => { 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 + fireEvent.click(checkboxes[0]); // select all + }); + await act(async () => { + fireEvent.click(checkboxes[0]); // 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 + // Stays on same step expect(screen.getByText("Select Agents to Make Public")).toBeInTheDocument(); }); @@ -255,11 +180,9 @@ describe("MakeAgentPublicForm", () => { 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(); }); @@ -267,54 +190,46 @@ describe("MakeAgentPublicForm", () => { 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 + expect(checkboxes).toHaveLength(3); - // Initially, agent-2 should be selected (it's already public) - const agent1Checkbox = checkboxes[1]; // First agent checkbox - const agent2Checkbox = checkboxes[2]; // Second agent checkbox + const agent1Checkbox = checkboxes[1]; + const agent2Checkbox = checkboxes[2]; - expect(agent2Checkbox).toBeChecked(); // agent-2 is already public + // agent-2 preselected (already public) + expect(agent2Checkbox).toBeChecked(); - // Select agent-1 await act(async () => { fireEvent.click(agent1Checkbox); }); @@ -322,7 +237,6 @@ describe("MakeAgentPublicForm", () => { expect(agent1Checkbox).toBeChecked(); expect(agent2Checkbox).toBeChecked(); - // Deselect agent-2 await act(async () => { fireEvent.click(agent2Checkbox); }); @@ -330,9 +244,9 @@ describe("MakeAgentPublicForm", () => { expect(agent1Checkbox).toBeChecked(); expect(agent2Checkbox).not.toBeChecked(); - // Select all should be indeterminate now + // Select all should be indeterminate (Radix uses data-state) const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toHaveAttribute("data-state", "indeterminate"); }); it("should display skills overflow text when agent has more than 3 skills", () => { @@ -354,12 +268,9 @@ describe("MakeAgentPublicForm", () => { 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(); }); @@ -369,7 +280,6 @@ describe("MakeAgentPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -379,18 +289,15 @@ describe("MakeAgentPublicForm", () => { 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(); }); @@ -404,7 +311,6 @@ describe("MakeAgentPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -414,17 +320,14 @@ describe("MakeAgentPublicForm", () => { expect(screen.getByText("Confirm Making Agents Public")).toBeInTheDocument(); }); - // Submit - const submitButton = screen.getByRole("button", { name: "Make Public" }); await act(async () => { - fireEvent.click(submitButton); + fireEvent.click(screen.getByRole("button", { name: "Make Public" })); }); - // Check loading state - expect(submitButton).toHaveAttribute("data-loading", "true"); - expect(submitButton).toBeDisabled(); + const loadingButton = screen.getByRole("button", { name: "Making Public..." }); + expect(loadingButton).toHaveAttribute("data-loading", "true"); + expect(loadingButton).toBeDisabled(); - // Resolve the promise resolvePromise({}); await waitFor(() => { expect(mockProps.onSuccess).toHaveBeenCalled(); @@ -440,13 +343,10 @@ describe("MakeAgentPublicForm", () => { 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: [ @@ -456,7 +356,7 @@ describe("MakeAgentPublicForm", () => { description: "Description 1", url: "http://example.com/agent1", version: "1.0", - is_public: false, // Not public + is_public: false, skills: [], protocolVersion: "1.0", }, @@ -466,7 +366,7 @@ describe("MakeAgentPublicForm", () => { description: "Description 2", url: "http://example.com/agent2", version: "2.0", - is_public: true, // Already public + is_public: true, skills: [], protocolVersion: "1.0", }, @@ -476,7 +376,7 @@ describe("MakeAgentPublicForm", () => { description: "Description 3", url: "http://example.com/agent3", version: "3.0", - is_public: true, // Already public + is_public: true, skills: [], protocolVersion: "1.0", }, @@ -485,21 +385,18 @@ describe("MakeAgentPublicForm", () => { render(); - // Check that the correct checkboxes are selected const checkboxes = screen.getAllByRole("checkbox"); - expect(checkboxes).toHaveLength(4); // Select all + 3 agents + expect(checkboxes).toHaveLength(4); - // 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 + expect(agent1Checkbox).not.toBeChecked(); + expect(agent2Checkbox).toBeChecked(); + expect(agent3Checkbox).toBeChecked(); - // Select all should be indeterminate const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toHaveAttribute("data-state", "indeterminate"); }); }); diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.tsx index 8cdc1c95218..7dc1d046caa 100644 --- a/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from "react"; -import { Form, Steps } from "antd"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -10,12 +9,11 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { cn } from "@/lib/utils"; +import { Check } from "lucide-react"; import { makeAgentsPublicCall } from "../../networking"; import NotificationsManager from "../../molecules/notifications_manager"; import { AgentHubData } from "@/components/AIHub/AgentHubTableColumns"; -const { Step } = Steps; - interface MakeAgentPublicFormProps { visible: boolean; onClose: () => void; @@ -24,6 +22,49 @@ interface MakeAgentPublicFormProps { onSuccess: () => void; } +function Stepper({ current, steps }: { current: number; steps: string[] }) { + return ( +
    + {steps.map((label, i) => { + const active = i === current; + const completed = i < current; + return ( +
  1. +
    + {completed ? : i + 1} +
    + + {label} + + {i < steps.length - 1 && ( +
    + )} +
  2. + ); + })} +
+ ); +} + const MakeAgentPublicForm: React.FC = ({ visible, onClose, @@ -34,12 +75,10 @@ const MakeAgentPublicForm: React.FC = ({ const [currentStep, setCurrentStep] = useState(0); const [selectedAgents, setSelectedAgents] = useState>(new Set()); const [loading, setLoading] = useState(false); - const [form] = Form.useForm(); const handleClose = () => { setCurrentStep(0); setSelectedAgents(new Set()); - form.resetFields(); onClose(); }; @@ -314,7 +353,11 @@ const MakeAgentPublicForm: React.FC = ({ )} {currentStep === 1 && ( - )} @@ -332,15 +375,9 @@ const MakeAgentPublicForm: React.FC = ({ Make Agents Public -
- - - - - - {renderStepContent()} - {renderStepButtons()} -
+ + {renderStepContent()} + {renderStepButtons()} ); diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.test.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.test.tsx index b0228e9e868..f9c2b6f124d 100644 --- a/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.test.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.test.tsx @@ -12,72 +12,6 @@ vi.mock("../../networking", () => ({ 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) =>
{children}
, { - 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, @@ -150,15 +84,12 @@ describe("MakeMCPPublicForm", () => { 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 + expect(checkboxes).toHaveLength(3); - // Check that the Next button is enabled (servers are preselected) const nextButton = screen.getByRole("button", { name: "Next" }); expect(nextButton).not.toBeDisabled(); }); @@ -166,25 +97,20 @@ describe("MakeMCPPublicForm", () => { 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(); }); @@ -195,24 +121,20 @@ describe("MakeMCPPublicForm", () => { 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); @@ -231,22 +153,18 @@ describe("MakeMCPPublicForm", () => { 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(); @@ -255,20 +173,19 @@ describe("MakeMCPPublicForm", () => { 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 + fireEvent.click(checkboxes[0]); + }); + await act(async () => { + fireEvent.click(checkboxes[0]); }); - // 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(); }); @@ -282,11 +199,9 @@ describe("MakeMCPPublicForm", () => { 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(); }); @@ -294,54 +209,45 @@ describe("MakeMCPPublicForm", () => { 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 + expect(checkboxes).toHaveLength(3); - // Initially, server-2 should be selected (it's already public) - const server1Checkbox = checkboxes[1]; // First server checkbox - const server2Checkbox = checkboxes[2]; // Second server checkbox + const server1Checkbox = checkboxes[1]; + const server2Checkbox = checkboxes[2]; - expect(server2Checkbox).toBeChecked(); // server-2 is already public + expect(server2Checkbox).toBeChecked(); - // Select server-1 await act(async () => { fireEvent.click(server1Checkbox); }); @@ -349,7 +255,6 @@ describe("MakeMCPPublicForm", () => { expect(server1Checkbox).toBeChecked(); expect(server2Checkbox).toBeChecked(); - // Deselect server-2 await act(async () => { fireEvent.click(server2Checkbox); }); @@ -357,9 +262,8 @@ describe("MakeMCPPublicForm", () => { expect(server1Checkbox).toBeChecked(); expect(server2Checkbox).not.toBeChecked(); - // Select all should be indeterminate now const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toHaveAttribute("data-state", "indeterminate"); }); it("should display tools overflow text when server has more than 3 tools", () => { @@ -375,12 +279,9 @@ describe("MakeMCPPublicForm", () => { 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(); }); @@ -390,7 +291,6 @@ describe("MakeMCPPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -400,18 +300,15 @@ describe("MakeMCPPublicForm", () => { 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(); }); @@ -425,7 +322,6 @@ describe("MakeMCPPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -435,17 +331,14 @@ describe("MakeMCPPublicForm", () => { expect(screen.getByText("Confirm Making MCP Servers Public")).toBeInTheDocument(); }); - // Submit - const submitButton = screen.getByRole("button", { name: "Make Public" }); await act(async () => { - fireEvent.click(submitButton); + fireEvent.click(screen.getByRole("button", { name: "Make Public" })); }); - // Check loading state - expect(submitButton).toHaveAttribute("data-loading", "true"); - expect(submitButton).toBeDisabled(); + const loadingButton = screen.getByRole("button", { name: "Making Public..." }); + expect(loadingButton).toHaveAttribute("data-loading", "true"); + expect(loadingButton).toBeDisabled(); - // Resolve the promise resolvePromise({}); await waitFor(() => { expect(mockProps.onSuccess).toHaveBeenCalled(); @@ -461,13 +354,10 @@ describe("MakeMCPPublicForm", () => { 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: [ @@ -478,7 +368,7 @@ describe("MakeMCPPublicForm", () => { url: "http://example.com/server1", transport: "http", status: "active", - mcp_info: { is_public: false }, // Not public + mcp_info: { is_public: false }, allowed_tools: [], auth_type: "bearer", credentials: {}, @@ -500,7 +390,7 @@ describe("MakeMCPPublicForm", () => { url: "http://example.com/server2", transport: "websocket", status: "inactive", - mcp_info: { is_public: true }, // Already public + mcp_info: { is_public: true }, allowed_tools: [], auth_type: "none", credentials: {}, @@ -522,7 +412,7 @@ describe("MakeMCPPublicForm", () => { url: "http://example.com/server3", transport: "sse", status: "healthy", - mcp_info: { is_public: true }, // Already public + mcp_info: { is_public: true }, allowed_tools: [], auth_type: "oauth", credentials: {}, @@ -542,21 +432,18 @@ describe("MakeMCPPublicForm", () => { render(); - // Check that the correct checkboxes are selected const checkboxes = screen.getAllByRole("checkbox"); - expect(checkboxes).toHaveLength(4); // Select all + 3 servers + expect(checkboxes).toHaveLength(4); - // 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 + expect(server1Checkbox).not.toBeChecked(); + expect(server2Checkbox).toBeChecked(); + expect(server3Checkbox).toBeChecked(); - // Select all should be indeterminate const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toHaveAttribute("data-state", "indeterminate"); }); }); diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.tsx index 5a44885fa42..78951d6cd73 100644 --- a/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from "react"; -import { Form, Steps } from "antd"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -10,12 +9,11 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { cn } from "@/lib/utils"; +import { Check } from "lucide-react"; import { makeMCPPublicCall } from "../../networking"; import NotificationsManager from "../../molecules/notifications_manager"; import { MCPServerData } from "@/components/mcp_hub_table_columns"; -const { Step } = Steps; - interface MakeMCPPublicFormProps { visible: boolean; onClose: () => void; @@ -34,6 +32,49 @@ const STATUS_BADGE_CLASSES = (status?: string): string => { return "bg-muted text-muted-foreground"; }; +function Stepper({ current, steps }: { current: number; steps: string[] }) { + return ( +
    + {steps.map((label, i) => { + const active = i === current; + const completed = i < current; + return ( +
  1. +
    + {completed ? : i + 1} +
    + + {label} + + {i < steps.length - 1 && ( +
    + )} +
  2. + ); + })} +
+ ); +} + const MakeMCPPublicForm: React.FC = ({ visible, onClose, @@ -46,12 +87,10 @@ const MakeMCPPublicForm: React.FC = ({ new Set(), ); const [loading, setLoading] = useState(false); - const [form] = Form.useForm(); const handleClose = () => { setCurrentStep(0); setSelectedServers(new Set()); - form.resetFields(); onClose(); }; @@ -361,7 +400,11 @@ const MakeMCPPublicForm: React.FC = ({ )} {currentStep === 1 && ( - )} @@ -379,15 +422,9 @@ const MakeMCPPublicForm: React.FC = ({ Make MCP Servers Public -
- - - - - - {renderStepContent()} - {renderStepButtons()} -
+ + {renderStepContent()} + {renderStepButtons()} ); diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.test.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.test.tsx index 2b57535f3ad..14a19b812ef 100644 --- a/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.test.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.test.tsx @@ -29,68 +29,6 @@ vi.mock("../../networking", () => ({ 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) =>
{children}
, { - 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) => ( @@ -102,14 +40,6 @@ vi.mock("../../model_filters", () => ({ ), })); -// Mock NotificationsManager -vi.mock("../../molecules/notifications_manager", () => ({ - default: { - fromBackend: vi.fn(), - success: vi.fn(), - }, -})); - describe("MakeModelPublicForm", () => { const mockProps = { visible: true, @@ -170,15 +100,12 @@ describe("MakeModelPublicForm", () => { 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 + expect(checkboxes).toHaveLength(3); - // Check that the Next button is enabled (models are preselected) const nextButton = screen.getByRole("button", { name: "Next" }); expect(nextButton).not.toBeDisabled(); }); @@ -186,25 +113,20 @@ describe("MakeModelPublicForm", () => { 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(); }); @@ -215,24 +137,20 @@ describe("MakeModelPublicForm", () => { 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); @@ -251,22 +169,18 @@ describe("MakeModelPublicForm", () => { 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(); @@ -275,20 +189,19 @@ describe("MakeModelPublicForm", () => { 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 + fireEvent.click(checkboxes[0]); + }); + await act(async () => { + fireEvent.click(checkboxes[0]); }); - // 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(); }); @@ -302,11 +215,9 @@ describe("MakeModelPublicForm", () => { 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(); }); @@ -314,54 +225,45 @@ describe("MakeModelPublicForm", () => { 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 + expect(checkboxes).toHaveLength(3); - // 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 + const gpt4Checkbox = checkboxes[1]; + const gpt35Checkbox = checkboxes[2]; - expect(gpt35Checkbox).toBeChecked(); // gpt-3.5-turbo is already public + expect(gpt35Checkbox).toBeChecked(); - // Select gpt-4 await act(async () => { fireEvent.click(gpt4Checkbox); }); @@ -369,7 +271,6 @@ describe("MakeModelPublicForm", () => { expect(gpt4Checkbox).toBeChecked(); expect(gpt35Checkbox).toBeChecked(); - // Deselect gpt-3.5-turbo await act(async () => { fireEvent.click(gpt35Checkbox); }); @@ -377,22 +278,17 @@ describe("MakeModelPublicForm", () => { expect(gpt4Checkbox).toBeChecked(); expect(gpt35Checkbox).not.toBeChecked(); - // Select all should be indeterminate now const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toHaveAttribute("data-state", "indeterminate"); }); 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); }); @@ -402,7 +298,6 @@ describe("MakeModelPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -412,18 +307,15 @@ describe("MakeModelPublicForm", () => { 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(); }); @@ -437,7 +329,6 @@ describe("MakeModelPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -447,17 +338,14 @@ describe("MakeModelPublicForm", () => { expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument(); }); - // Submit - const submitButton = screen.getByRole("button", { name: "Make Public" }); await act(async () => { - fireEvent.click(submitButton); + fireEvent.click(screen.getByRole("button", { name: "Make Public" })); }); - // Check loading state - expect(submitButton).toHaveAttribute("data-loading", "true"); - expect(submitButton).toBeDisabled(); + const loadingButton = screen.getByRole("button", { name: "Making Public..." }); + expect(loadingButton).toHaveAttribute("data-loading", "true"); + expect(loadingButton).toBeDisabled(); - // Resolve the promise resolvePromise({}); await waitFor(() => { expect(mockProps.onSuccess).toHaveBeenCalled(); @@ -473,13 +361,10 @@ describe("MakeModelPublicForm", () => { 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: [ @@ -506,36 +391,36 @@ describe("MakeModelPublicForm", () => { render(); - // Check that the correct checkboxes are selected const checkboxes = screen.getAllByRole("checkbox"); - expect(checkboxes).toHaveLength(4); // Select all + 3 models + expect(checkboxes).toHaveLength(4); - // 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 + expect(privateModelCheckbox).not.toBeChecked(); + expect(publicModelCheckbox).toBeChecked(); + expect(anotherPublicModelCheckbox).toBeChecked(); - // Select all should be indeterminate const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toHaveAttribute("data-state", "indeterminate"); }); 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(); + // "1 model selected" is rendered as `1 model selected` + const banner = screen.getByText((_content, node) => { + if (!node) return false; + if (node.tagName !== "P") return false; + return (node.textContent ?? "").trim().replace(/\s+/g, " ") === "1 model selected"; + }); + expect(banner).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); @@ -545,13 +430,9 @@ describe("MakeModelPublicForm", () => { 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/MakeModelPublicForm.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.tsx index 95ef399f1a2..69417793582 100644 --- a/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.tsx @@ -1,5 +1,4 @@ import React, { useState, useCallback, useEffect } from "react"; -import { Form, Steps } from "antd"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -9,12 +8,12 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; +import { Check } from "lucide-react"; import { makeModelGroupPublic } from "../../networking"; import ModelFilters from "../../model_filters"; import NotificationsManager from "../../molecules/notifications_manager"; -const { Step } = Steps; - interface ModelGroupInfo { model_group: string; providers: string[]; @@ -42,6 +41,49 @@ interface MakeModelPublicFormProps { onSuccess: () => void; } +function Stepper({ current, steps }: { current: number; steps: string[] }) { + return ( +
    + {steps.map((label, i) => { + const active = i === current; + const completed = i < current; + return ( +
  1. +
    + {completed ? : i + 1} +
    + + {label} + + {i < steps.length - 1 && ( +
    + )} +
  2. + ); + })} +
+ ); +} + const MakeModelPublicForm: React.FC = ({ visible, onClose, @@ -53,13 +95,11 @@ const MakeModelPublicForm: React.FC = ({ const [selectedModels, setSelectedModels] = useState>(new Set()); const [filteredData, setFilteredData] = useState([]); const [loading, setLoading] = useState(false); - const [form] = Form.useForm(); const handleClose = () => { setCurrentStep(0); setSelectedModels(new Set()); setFilteredData([]); - form.resetFields(); onClose(); }; @@ -181,7 +221,6 @@ const MakeModelPublicForm: React.FC = ({ Users will still require a valid Virtual Key to use these models.

- {/* Filters */} = ({ )} {currentStep === 1 && ( - )} @@ -350,15 +393,9 @@ const MakeModelPublicForm: React.FC = ({ Make Models Public -
- - - - - - {renderStepContent()} - {renderStepButtons()} -
+ + {renderStepContent()} + {renderStepButtons()} );