diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 2be3bbdc737..59bf75ce9bd 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1709,54 +1709,26 @@ "no-nested-ternary": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "prefer-const": { "count": 4 } }, - "src/components/AIHub/SkillHubDashboard.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/AIHub/UsefulLinksManagement.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } }, "src/components/AIHub/forms/MakeAgentPublicForm.tsx": { - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/components/AIHub/forms/MakeMCPPublicForm.test.tsx": { - "react/display-name": { - "count": 1 - } - }, "src/components/AIHub/forms/MakeMCPPublicForm.tsx": { - "no-nested-ternary": { - "count": 2 - }, - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 1 } }, "src/components/AIHub/forms/MakeModelPublicForm.tsx": { - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 1 } diff --git a/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx index 0e4de3f244c..28dac9badcd 100644 --- a/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx @@ -23,10 +23,12 @@ import { import PublicModelHub from "@/components/public_model_hub"; import { copyToClipboard } from "@/utils/dataUtils"; import { isAdminRole, isProxyAdminRole } from "@/utils/roles"; -import { CopyOutlined } from "@ant-design/icons"; import { SortingState } from "@tanstack/react-table"; -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 { Copy, Inbox } from "lucide-react"; import { useRouter } from "next/navigation"; import React, { useCallback, useEffect, useMemo, useState } from "react"; @@ -393,7 +395,7 @@ 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. @@ -403,9 +405,9 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, )}

- Model Hub URL: +

Model Hub URL:

- {`${getProxyBaseUrl()}/ui/model_hub_table`} +

{`${getProxyBaseUrl()}/ui/model_hub_table`}

@@ -568,167 +570,162 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, setSkillHubData(response.plugins); }} /> - - - + +
+
) : ( - - Public Model Hub not enabled. + +

Public Model Hub not enabled.

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

)} {/* Public Page Modal */} - -
-
- Shareable Link: - - {`${getProxyBaseUrl()}/ui/model_hub_table`} - + !open && handleCancel()}> + + + {"Public Model Hub"} + +
+
+

Shareable Link:

+

+ {`${getProxyBaseUrl()}/ui/model_hub_table`} +

+
+
+ +
-
- -
-
- + + {/* Model Details Modal */} - - {selectedModel && ( -
- {/* Model Overview */} -
- Model Overview -
-
- Model Group: - {selectedModel.model_group} + !open && handleCancel()}> + + + {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", @@ -746,316 +743,310 @@ response = client.chat.completions.create( ) print(response.choices[0].message.content)`} - +
+
-
- )} - + )} + + {/* Agent Details Modal */} - - {selectedAgent && ( -
- {/* Agent Overview */} -
- Agent Overview -
-
- Name: - {selectedAgent.name} -
-
- Version: - v{selectedAgent.version} -
-
- Protocol Version: - {selectedAgent.protocolVersion} -
-
- URL: -
- {selectedAgent.url} - void copyToClipboard(selectedAgent.url)} - className="cursor-pointer text-gray-500 hover:text-blue-500" - /> + !open && handleCancel()}> + + + {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}

+ void copyToClipboard(selectedAgent.url)} + className="size-3.5 shrink-0 cursor-pointer text-gray-500 hover:text-blue-500" + /> +
- 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 */} - - {selectedMcpServer && ( -
- {/* Server Overview */} -
- Server Overview -
-
- Server Name: - {selectedMcpServer.server_name} -
-
- Server ID: -
- {selectedMcpServer.server_id} - void copyToClipboard(selectedMcpServer.server_id)} - className="cursor-pointer text-gray-500 hover:text-blue-500" - /> + !open && handleCancel()}> + + + {selectedMcpServer?.server_name || "MCP Server Details"} + + {selectedMcpServer && ( +
+ {/* Server Overview */} +
+

Server Overview

+
+
+

Server Name:

+

{selectedMcpServer.server_name}

+
+
+

Server ID:

+
+

{selectedMcpServer.server_id}

+ void copyToClipboard(selectedMcpServer.server_id)} + className="size-3.5 shrink-0 cursor-pointer text-gray-500 hover:text-blue-500" + /> +
+
+ {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

+
+ {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 -
- {selectedMcpServer.command && ( -
- Command: - - {selectedMcpServer.command} - + {/* Teams */} + {selectedMcpServer.teams && selectedMcpServer.teams.length > 0 && ( +
+

Teams

+
+ {selectedMcpServer.teams.map((team, idx) => ( + + {team} + + ))}
- )} -
-
- - {/* 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 @@ -1088,11 +1079,12 @@ async def main(): if __name__ == "__main__": asyncio.run(main())`} - + +
-
- )} - + )} + +
{/* Make Model Public Form */} = ({ // Derived stats const totalSkills = skills.length; - const domains = useMemo(() => [...new Set(skills.map((s) => s.domain).filter(Boolean))], [skills]); + const domains = useMemo( + () => [...new Set(skills.map((s) => s.domain).filter((domain): domain is string => Boolean(domain)))], + [skills], + ); const namespaces = useMemo(() => [...new Set(skills.map((s) => s.namespace).filter(Boolean))], [skills]); // Filtered table data @@ -73,6 +78,11 @@ const SkillHubDashboard: React.FC = ({ const columns = useMemo(() => getSkillHubTableColumns({ onSkillClick: setSelectedSkill }), []); + const domainItems = useMemo( + () => [{ value: ALL_DOMAINS, label: "All Domains" }, ...domains.map((d) => ({ value: d, label: d }))], + [domains], + ); + const hasActiveFilter = search.trim().length > 0 || domainFilter != null; if (selectedSkill) { @@ -111,21 +121,43 @@ const SkillHubDashboard: React.FC = ({

All {publicPage ? "Public " : ""}Skills

} - placeholder="Search by name, namespace, or tag…" - value={search} - onChange={(e) => setSearch(e.target.value)} - style={{ width: 280 }} - allowClear - /> + items={domainItems} + value={domainFilter ?? ALL_DOMAINS} + onValueChange={(val) => setDomainFilter(val === null || val === ALL_DOMAINS ? undefined : val)} + > + + + + + {domainItems.map((item) => ( + + {item.label} + + ))} + + + + + + + setSearch(e.target.value)} + /> + {search !== "" && ( + + setSearch("")} + > + + + + )} +
= ({ accessTok }; return ( - +
setIsExpanded(!isExpanded)}>
- Link Management +

Link Management

Manage the links that are displayed under 'Useful Links' on the public model hub.

@@ -243,7 +244,7 @@ const UsefulLinksManagement: React.FC = ({ accessTok {isExpanded && (
- Add New Link +

Add New Link

@@ -288,7 +289,7 @@ const UsefulLinksManagement: React.FC = ({ accessTok
- Manage Existing Links +

Manage Existing Links

= ({ accessTok
- + - Display Name - URL - Actions + Display Name + URL + Actions - + {links.map((link, index) => ( diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.test.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.test.tsx index 67c6d7d6cc9..a55beaf517f 100644 --- a/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.test.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.test.tsx @@ -12,67 +12,8 @@ vi.mock("../../networking", () => ({ 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} - - ), -})); +const expectDisabledControl = (element: HTMLElement) => + expect(element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true").toBe(true); describe("MakeAgentPublicForm", () => { const mockProps = { @@ -143,7 +84,7 @@ describe("MakeAgentPublicForm", () => { expect(screen.getByText("Select Agents to Make Public")).toBeInTheDocument(); // Select all agents using the select all checkbox - const selectAllCheckbox = screen.getByLabelText("Select All (2)"); + const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All (2)" }); await act(async () => { fireEvent.click(selectAllCheckbox); }); @@ -169,12 +110,11 @@ describe("MakeAgentPublicForm", () => { render(); // Select all agents - const selectAllCheckbox = screen.getByLabelText("Select All (2)"); + const selectAllCheckbox = screen.getByRole("checkbox", { name: "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); @@ -185,7 +125,6 @@ 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); @@ -232,6 +171,8 @@ describe("MakeAgentPublicForm", () => { const checkboxes = screen.getAllByRole("checkbox"); await act(async () => { fireEvent.click(checkboxes[0]); // Click select all to select all + }); + await act(async () => { fireEvent.click(checkboxes[0]); // Click select all again to deselect all }); @@ -256,8 +197,8 @@ describe("MakeAgentPublicForm", () => { expect(screen.getByText("No agents available.")).toBeInTheDocument(); // Select All checkbox should be disabled - const selectAllCheckbox = screen.getByLabelText("Select All"); - expect(selectAllCheckbox).toBeDisabled(); + const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All" }); + expectDisabledControl(selectAllCheckbox); // Next button should be disabled const nextButton = screen.getByRole("button", { name: "Next" }); @@ -332,7 +273,7 @@ describe("MakeAgentPublicForm", () => { // Select all should be indeterminate now const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toBePartiallyChecked(); }); it("should display skills overflow text when agent has more than 3 skills", () => { @@ -369,7 +310,6 @@ describe("MakeAgentPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -379,7 +319,6 @@ 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); @@ -395,7 +334,7 @@ describe("MakeAgentPublicForm", () => { expect(mockProps.onClose).not.toHaveBeenCalled(); }); - it("should show loading state during submit", async () => { + it("should not complete the flow until the submit request resolves", async () => { let resolvePromise: (value: any) => void = () => {}; const pendingPromise = new Promise((resolve) => { resolvePromise = resolve; @@ -404,7 +343,6 @@ describe("MakeAgentPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -414,17 +352,20 @@ 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); }); - // Check loading state - expect(submitButton).toHaveAttribute("data-loading", "true"); - expect(submitButton).toBeDisabled(); + expectDisabledControl(submitButton); + await act(async () => { + fireEvent.click(submitButton); + }); + expect(mockMakeAgentsPublicCall).toHaveBeenCalledTimes(1); + expect(mockProps.onSuccess).not.toHaveBeenCalled(); + expect(mockProps.onClose).not.toHaveBeenCalled(); + expect(screen.getByText("Confirm Making Agents Public")).toBeInTheDocument(); - // Resolve the promise resolvePromise({}); await waitFor(() => { expect(mockProps.onSuccess).toHaveBeenCalled(); @@ -441,7 +382,7 @@ describe("MakeAgentPublicForm", () => { render(); // Modal should not be rendered - expect(screen.queryByTestId("modal")).not.toBeInTheDocument(); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(screen.queryByText("Make Agents Public")).not.toBeInTheDocument(); }); @@ -500,6 +441,6 @@ describe("MakeAgentPublicForm", () => { // Select all should be indeterminate const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toBePartiallyChecked(); }); }); diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.tsx index 0ed73872cee..82336206858 100644 --- a/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeAgentPublicForm.tsx @@ -1,11 +1,15 @@ import React, { useState, useEffect } from "react"; -import { Modal, Form, Steps, Button, Checkbox } from "antd"; -import { Text, Title, Badge } from "@tremor/react"; +import { Loader2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { cn } from "@/lib/cva.config"; import { makeAgentsPublicCall } from "../../networking"; import NotificationsManager from "../../molecules/notifications_manager"; import { AgentHubData } from "@/components/AIHub/AgentHubTableColumns"; -const { Step } = Steps; +const STEP_TITLES = ["Select Agents", "Confirm"]; interface MakeAgentPublicFormProps { visible: boolean; @@ -25,12 +29,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(); }; @@ -113,29 +115,30 @@ const MakeAgentPublicForm: React.FC = ({ return (
- Select Agents to Make Public +

Select Agents to Make Public

- handleSelectAll(e.target.checked)} - disabled={agentHubData.length === 0} - > +
- +

Select the agents you want to be visible on the public model hub. Users will still require a valid Virtual Key to use these agents. - +

{agentHubData.length === 0 ? (
- No agents available. +

No agents available.

) : ( agentHubData.map((agent) => { @@ -144,25 +147,23 @@ const MakeAgentPublicForm: React.FC = ({
handleAgentSelection(agentId, e.target.checked)} + onCheckedChange={(checked) => handleAgentSelection(agentId, checked === true)} /> -
+
- {agent.name} - - v{agent.version} - +

{agent.name}

+ v{agent.version}
- {agent.description} +

{agent.description}

{agent.skills && agent.skills.length > 0 && (
{agent.skills.slice(0, 3).map((skill) => ( - + {skill.name} ))} {agent.skills.length > 3 && ( - +{agent.skills.length - 3} more +

+{agent.skills.length - 3} more

)}
)} @@ -176,9 +177,9 @@ const MakeAgentPublicForm: React.FC = ({ {selectedAgents.size > 0 && (
- +

{selectedAgents.size} agent{selectedAgents.size !== 1 ? "s" : ""} selected - +

)}
@@ -188,33 +189,31 @@ const MakeAgentPublicForm: React.FC = ({ const renderStep2Content = () => { return (
- Confirm Making Agents Public +

Confirm Making Agents Public

- +

Warning: Once you make these agents public, anyone who can go to the{" "} /ui/model_hub_table will be able to know they exist on the proxy. - +

- Agents to be made public: +

Agents to be made public:

{Array.from(selectedAgents).map((agentId) => { const agent = agentHubData.find((a) => (a.agent_id || a.name) === agentId); return (
-
+
- {agent?.name || agentId} - {agent && ( - - v{agent.version} - - )} +

{agent?.name || agentId}

+ {agent && v{agent.version}}
- {agent?.description && {agent.description}} + {agent?.description && ( +

{agent.description}

+ )}
); @@ -224,10 +223,10 @@ const MakeAgentPublicForm: React.FC = ({
- +

Total: {selectedAgents.size} agent{selectedAgents.size !== 1 ? "s" : ""} will be made public - +

); @@ -247,7 +246,7 @@ const MakeAgentPublicForm: React.FC = ({ const renderStepButtons = () => { return (
- @@ -259,7 +258,8 @@ const MakeAgentPublicForm: React.FC = ({ )} {currentStep === 1 && ( - )} @@ -269,24 +269,42 @@ const MakeAgentPublicForm: React.FC = ({ }; return ( - -
- - - - + !open && handleClose()} disablePointerDismissal> + + + Make Agents Public + - {renderStepContent()} - {renderStepButtons()} - -
+
+
    + {STEP_TITLES.map((title, index) => ( +
  1. + + {index + 1} + + + {title} + +
  2. + ))} +
+ + {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 994a920b2e4..ff385b3ed7c 100644 --- a/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.test.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.test.tsx @@ -12,83 +12,8 @@ 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. -// NOTE: the comment used to say "Button is already mocked globally" — that was -// incorrect. A file-level vi.mock fully replaces the setup-level mock from -// tests/setupTests.ts, so we must re-apply the Button/Tooltip overrides here. -// Without them, the real Tremor Button leaks through and its useTooltip(300) -// schedules a native setTimeout that can fire post-teardown -> "window is not defined". -vi.mock("@tremor/react", async (importOriginal) => { - const actual = await importOriginal(); - const React = await import("react"); - return { - ...actual, - Text: ({ children, className }: any) => {children}, - Title: ({ children }: any) =>

{children}

, - Badge: ({ children, color, size }: any) => ( - - {children} - - ), - Button: React.forwardRef(({ children, ...props }, ref) => ( - - )), - Tooltip: ({ children }: any) => <>{children}, - }; -}); +const expectDisabledControl = (element: HTMLElement) => + expect(element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true").toBe(true); describe("MakeMCPPublicForm", () => { const mockProps = { @@ -182,7 +107,7 @@ describe("MakeMCPPublicForm", () => { 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)"); + const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All (2)" }); await act(async () => { fireEvent.click(selectAllCheckbox); }); @@ -208,12 +133,11 @@ describe("MakeMCPPublicForm", () => { render(); // Select all servers - const selectAllCheckbox = screen.getByLabelText("Select All (2)"); + const selectAllCheckbox = screen.getByRole("checkbox", { name: "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); @@ -224,7 +148,6 @@ 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); @@ -271,6 +194,8 @@ describe("MakeMCPPublicForm", () => { const checkboxes = screen.getAllByRole("checkbox"); await act(async () => { fireEvent.click(checkboxes[0]); // Click select all to select all + }); + await act(async () => { fireEvent.click(checkboxes[0]); // Click select all again to deselect all }); @@ -295,8 +220,8 @@ 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(); + const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All" }); + expectDisabledControl(selectAllCheckbox); // Next button should be disabled const nextButton = screen.getByRole("button", { name: "Next" }); @@ -371,7 +296,7 @@ describe("MakeMCPPublicForm", () => { // Select all should be indeterminate now const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toBePartiallyChecked(); }); it("should display tools overflow text when server has more than 3 tools", () => { @@ -402,7 +327,6 @@ describe("MakeMCPPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -412,7 +336,6 @@ 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); @@ -428,7 +351,7 @@ describe("MakeMCPPublicForm", () => { expect(mockProps.onClose).not.toHaveBeenCalled(); }); - it("should show loading state during submit", async () => { + it("should not complete the flow until the submit request resolves", async () => { let resolvePromise: (value: any) => void = () => {}; const pendingPromise = new Promise((resolve) => { resolvePromise = resolve; @@ -437,7 +360,6 @@ describe("MakeMCPPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -447,17 +369,20 @@ 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); }); - // Check loading state - expect(submitButton).toHaveAttribute("data-loading", "true"); - expect(submitButton).toBeDisabled(); + expectDisabledControl(submitButton); + await act(async () => { + fireEvent.click(submitButton); + }); + expect(mockMakeMCPPublicCall).toHaveBeenCalledTimes(1); + expect(mockProps.onSuccess).not.toHaveBeenCalled(); + expect(mockProps.onClose).not.toHaveBeenCalled(); + expect(screen.getByText("Confirm Making MCP Servers Public")).toBeInTheDocument(); - // Resolve the promise resolvePromise({}); await waitFor(() => { expect(mockProps.onSuccess).toHaveBeenCalled(); @@ -474,7 +399,7 @@ describe("MakeMCPPublicForm", () => { render(); // Modal should not be rendered - expect(screen.queryByTestId("modal")).not.toBeInTheDocument(); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(screen.queryByText("Make MCP Servers Public")).not.toBeInTheDocument(); }); @@ -569,6 +494,6 @@ describe("MakeMCPPublicForm", () => { // Select all should be indeterminate const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toBePartiallyChecked(); }); }); diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.tsx index b590c3cc1dd..7ef42883400 100644 --- a/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeMCPPublicForm.tsx @@ -1,11 +1,25 @@ import React, { useState, useEffect } from "react"; -import { Modal, Form, Steps, Button, Checkbox } from "antd"; -import { Text, Title, Badge } from "@tremor/react"; +import { Loader2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { cn } from "@/lib/cva.config"; import { makeMCPPublicCall } from "../../networking"; import NotificationsManager from "../../molecules/notifications_manager"; import { MCPServerData } from "@/components/AIHub/MCPHubTableColumns"; -const { Step } = Steps; +const STEP_TITLES = ["Select Servers", "Confirm"]; + +const statusVariant = (status?: string) => { + if (status === "active" || status === "healthy") { + return "default" as const; + } + if (status === "inactive" || status === "unhealthy") { + return "destructive" as const; + } + return "outline" as const; +}; interface MakeMCPPublicFormProps { visible: boolean; @@ -25,12 +39,10 @@ const MakeMCPPublicForm: React.FC = ({ const [currentStep, setCurrentStep] = useState(0); const [selectedServers, setSelectedServers] = useState>(new Set()); const [loading, setLoading] = useState(false); - const [form] = Form.useForm(); const handleClose = () => { setCurrentStep(0); setSelectedServers(new Set()); - form.resetFields(); onClose(); }; @@ -114,29 +126,30 @@ const MakeMCPPublicForm: React.FC = ({ return (
- Select MCP Servers to Make Public +

Select MCP Servers to Make Public

- handleSelectAll(e.target.checked)} - disabled={mcpHubData.length === 0} - > +
- +

Select the MCP servers you want to be visible on the public model hub. Users will still require a valid Virtual Key to use these servers. - +

{mcpHubData.length === 0 ? (
- No MCP servers available. +

No MCP servers available.

) : ( mcpHubData.map((server) => { @@ -148,42 +161,25 @@ const MakeMCPPublicForm: React.FC = ({ > handleServerSelection(server.server_id, e.target.checked)} + onCheckedChange={(checked) => handleServerSelection(server.server_id, checked === true)} /> -
-
- {server.server_name} - {isPublic && ( - - Public - - )} - - {server.transport} - - - {server.status || "unknown"} - +
+
+

{server.server_name}

+ {isPublic && Public} + {server.transport} + {server.status || "unknown"}
- {server.description || server.url} +

{server.description || server.url}

{server.allowed_tools && server.allowed_tools.length > 0 && (
{server.allowed_tools.slice(0, 3).map((tool, idx) => ( - + {tool} ))} {server.allowed_tools.length > 3 && ( - +{server.allowed_tools.length - 3} more +

+{server.allowed_tools.length - 3} more

)}
)} @@ -197,9 +193,9 @@ const MakeMCPPublicForm: React.FC = ({ {selectedServers.size > 0 && (
- +

{selectedServers.size} MCP server{selectedServers.size !== 1 ? "s" : ""} selected - +

)}
@@ -209,48 +205,37 @@ const MakeMCPPublicForm: React.FC = ({ const renderStep2Content = () => { return (
- Confirm Making MCP Servers Public +

Confirm Making MCP Servers Public

- +

Warning: Once you make these MCP servers public, anyone who can go to the{" "} /ui/model_hub_table will be able to know they exist on the proxy. - +

- MCP Servers to be made public: +

MCP Servers to be made public:

{Array.from(selectedServers).map((serverId) => { const server = mcpHubData.find((s) => s.server_id === serverId); return (
-
-
- {server?.server_name || serverId} +
+
+

{server?.server_name || serverId}

{server && ( <> - - {server.transport} - - - {server.status || "unknown"} - + {server.transport} + {server.status || "unknown"} )}
- {server?.description && {server.description}} - {server?.url && {server.url}} + {server?.description && ( +

{server.description}

+ )} + {server?.url &&

{server.url}

}
); @@ -260,10 +245,10 @@ const MakeMCPPublicForm: React.FC = ({
- +

Total: {selectedServers.size} MCP server{selectedServers.size !== 1 ? "s" : ""} will be made public - +

); @@ -283,7 +268,7 @@ const MakeMCPPublicForm: React.FC = ({ const renderStepButtons = () => { return (
- @@ -295,7 +280,8 @@ const MakeMCPPublicForm: React.FC = ({ )} {currentStep === 1 && ( - )} @@ -305,24 +291,42 @@ const MakeMCPPublicForm: React.FC = ({ }; return ( - -
- - - - + !open && handleClose()} disablePointerDismissal> + + + Make MCP Servers Public + - {renderStepContent()} - {renderStepButtons()} - -
+
+
    + {STEP_TITLES.map((title, index) => ( +
  1. + + {index + 1} + + + {title} + +
  2. + ))} +
+ + {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..ac0df137f6a 100644 --- a/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.test.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.test.tsx @@ -29,67 +29,8 @@ 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} - - ), -})); +const expectDisabledControl = (element: HTMLElement) => + expect(element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true").toBe(true); // Mock ModelFilters component vi.mock("../../model_filters", () => ({ @@ -190,7 +131,7 @@ describe("MakeModelPublicForm", () => { expect(screen.getByText("Select Models to Make Public")).toBeInTheDocument(); // Select all models using the select all checkbox - const selectAllCheckbox = screen.getByLabelText("Select All (2)"); + const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All (2)" }); await act(async () => { fireEvent.click(selectAllCheckbox); }); @@ -216,12 +157,11 @@ describe("MakeModelPublicForm", () => { render(); // Select all models - const selectAllCheckbox = screen.getByLabelText("Select All (2)"); + const selectAllCheckbox = screen.getByRole("checkbox", { name: "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); @@ -232,7 +172,6 @@ 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); @@ -279,6 +218,8 @@ describe("MakeModelPublicForm", () => { const checkboxes = screen.getAllByRole("checkbox"); await act(async () => { fireEvent.click(checkboxes[0]); // Click select all to select all + }); + await act(async () => { fireEvent.click(checkboxes[0]); // Click select all again to deselect all }); @@ -303,8 +244,8 @@ 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(); + const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All" }); + expectDisabledControl(selectAllCheckbox); // Next button should be disabled const nextButton = screen.getByRole("button", { name: "Next" }); @@ -379,7 +320,7 @@ describe("MakeModelPublicForm", () => { // Select all should be indeterminate now const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toBePartiallyChecked(); }); it("should display model badges and information", () => { @@ -402,7 +343,6 @@ describe("MakeModelPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -412,7 +352,6 @@ 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); @@ -428,7 +367,7 @@ describe("MakeModelPublicForm", () => { expect(mockProps.onClose).not.toHaveBeenCalled(); }); - it("should show loading state during submit", async () => { + it("should not complete the flow until the submit request resolves", async () => { let resolvePromise: (value: any) => void = () => {}; const pendingPromise = new Promise((resolve) => { resolvePromise = resolve; @@ -437,7 +376,6 @@ describe("MakeModelPublicForm", () => { render(); - // Navigate to confirm step const nextButton = screen.getByRole("button", { name: "Next" }); await act(async () => { fireEvent.click(nextButton); @@ -447,17 +385,20 @@ 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); }); - // Check loading state - expect(submitButton).toHaveAttribute("data-loading", "true"); - expect(submitButton).toBeDisabled(); + expectDisabledControl(submitButton); + await act(async () => { + fireEvent.click(submitButton); + }); + expect(mockMakeModelGroupPublic).toHaveBeenCalledTimes(1); + expect(mockProps.onSuccess).not.toHaveBeenCalled(); + expect(mockProps.onClose).not.toHaveBeenCalled(); + expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument(); - // Resolve the promise resolvePromise({}); await waitFor(() => { expect(mockProps.onSuccess).toHaveBeenCalled(); @@ -474,7 +415,7 @@ describe("MakeModelPublicForm", () => { render(); // Modal should not be rendered - expect(screen.queryByTestId("modal")).not.toBeInTheDocument(); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(screen.queryByText("Make Models Public")).not.toBeInTheDocument(); }); @@ -521,21 +462,19 @@ describe("MakeModelPublicForm", () => { // Select all should be indeterminate const selectAllCheckbox = checkboxes[0]; - expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true"); + expect(selectAllCheckbox).toBePartiallyChecked(); }); 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(); + expect(screen.getByText("model selected")).toHaveTextContent("1 model selected"); }); 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); diff --git a/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.tsx b/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.tsx index 2d0ae1a0e2b..28a34ee1f1a 100644 --- a/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/forms/MakeModelPublicForm.tsx @@ -1,11 +1,15 @@ import React, { useState, useCallback, useEffect } from "react"; -import { Modal, Form, Steps, Button, Checkbox } from "antd"; -import { Text, Title, Badge } from "@tremor/react"; +import { Loader2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { cn } from "@/lib/cva.config"; import { makeModelGroupPublic } from "../../networking"; import ModelFilters from "../../model_filters"; import NotificationsManager from "../../molecules/notifications_manager"; -const { Step } = Steps; +const STEP_TITLES = ["Select Models", "Confirm"]; interface ModelGroupInfo { model_group: string; @@ -44,13 +48,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(); }; @@ -138,23 +140,24 @@ const MakeModelPublicForm: React.FC = ({ return (
- Select Models to Make Public +

Select Models to Make Public

- handleSelectAll(e.target.checked)} - disabled={filteredData.length === 0} - > +
- +

Select the models you want to be visible on the public model hub. Users will still require a valid Virtual Key to use these models. - +

{/* Filters */} = ({
{filteredData.length === 0 ? (
- No models match the current filters. +

No models match the current filters.

) : ( filteredData.map((model) => ( @@ -178,20 +181,16 @@ const MakeModelPublicForm: React.FC = ({ > handleModelSelection(model.model_group, e.target.checked)} + onCheckedChange={(checked) => handleModelSelection(model.model_group, checked === true)} /> -
-
- {model.model_group} - {model.mode && ( - - {model.mode} - - )} +
+
+

{model.model_group}

+ {model.mode && {model.mode}}
{model.providers.map((provider) => ( - + {provider} ))} @@ -205,9 +204,9 @@ const MakeModelPublicForm: React.FC = ({ {selectedModels.size > 0 && (
- +

{selectedModels.size} model{selectedModels.size !== 1 ? "s" : ""} selected - +

)}
@@ -217,29 +216,29 @@ const MakeModelPublicForm: React.FC = ({ const renderStep2Content = () => { return (
- Confirm Making Models Public +

Confirm Making Models Public

- +

Warning: Once you make these models public, anyone who can go to the{" "} /ui/model_hub_table will be able to know they exist on the proxy. - +

- Models to be made public: +

Models to be made public:

{Array.from(selectedModels).map((modelGroup) => { const model = modelHubData.find((m) => m.model_group === modelGroup); return (
-
- {modelGroup} +
+

{modelGroup}

{model && (
{model.providers.map((provider) => ( - + {provider} ))} @@ -254,10 +253,10 @@ const MakeModelPublicForm: React.FC = ({
- +

Total: {selectedModels.size} model{selectedModels.size !== 1 ? "s" : ""} will be made public - +

); @@ -277,7 +276,7 @@ const MakeModelPublicForm: React.FC = ({ const renderStepButtons = () => { return (
- @@ -289,7 +288,8 @@ const MakeModelPublicForm: React.FC = ({ )} {currentStep === 1 && ( - )} @@ -299,24 +299,42 @@ const MakeModelPublicForm: React.FC = ({ }; return ( - -
- - - - + !open && handleClose()} disablePointerDismissal> + + + Make Models Public + - {renderStepContent()} - {renderStepButtons()} - -
+
+
    + {STEP_TITLES.map((title, index) => ( +
  1. + + {index + 1} + + + {title} + +
  2. + ))} +
+ + {renderStepContent()} + {renderStepButtons()} +
+ + ); };