feat(ui): migrate AIHub subtree to shadcn

- MakeAgentPublicForm, MakeMCPPublicForm, MakeModelPublicForm: drop
  antd Form + Steps shells. Replace with inline lucide-based Stepper
  component; step state stays plain React useState, dialog is already
  shadcn Dialog.
- ModelHubTable: replace antd Modal with shadcn Dialog, Tremor
  TabGroup/Tab/TabList/TabPanel with shadcn Tabs, Tremor Badge with
  shadcn Badge + semantic color helper, Tremor Card/Text/Title/Button
  with shadcn primitives + Tailwind, Ant icons with lucide Copy. Keep
  categorical palette (blue/green/purple/etc) classes for hub badges;
  file remains in .eslintrc allowlist.
- Tests: update mocks to stop stubbing antd (real shadcn Checkbox now
  exposes data-state="indeterminate" instead of the old
  data-indeterminate prop; tests assert against the Radix attribute).
  Split two-click "select/deselect" sequences into separate act()
  calls so state updates propagate between clicks.
- All 47 scoped tests pass (MakeAgentPublicForm, MakeMCPPublicForm,
  MakeModelPublicForm). ModelHubTable existing 8 tests still pass.

tsc --noEmit clean for migrated files. eslint clean.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-24 08:25:24 +00:00
parent a2ed3848aa
commit c2e8e4926b
No known key found for this signature in database
8 changed files with 804 additions and 1018 deletions

View file

@ -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",

File diff suppressed because it is too large Load diff

View file

@ -12,68 +12,6 @@ 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 ? (
<div data-testid="modal">
<div>{title}</div>
{children}
{footer}
</div>
) : null,
Form: Object.assign(({ children, form }: any) => <form data-testid="form">{children}</form>, {
useForm: () => [
{
resetFields: vi.fn(),
validateFields: vi.fn(),
getFieldsValue: vi.fn(),
setFieldsValue: vi.fn(),
},
vi.fn(),
],
Item: ({ children }: any) => <div>{children}</div>,
}),
Steps: Object.assign(
({ children, current, className }: any) => (
<div data-testid="steps" className={className}>
{children}
</div>
),
{
Step: ({ title }: any) => <div>{title}</div>,
},
),
Button: ({ children, onClick, disabled, loading, ...props }: any) => (
<button onClick={onClick} disabled={disabled || loading} data-loading={loading} {...props}>
{children}
</button>
),
Checkbox: ({ checked, indeterminate, onChange, children, disabled }: any) => (
<label>
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange({ target: { checked: e.target.checked } })}
disabled={disabled}
data-indeterminate={indeterminate}
/>
{children}
</label>
),
}));
// Mock @tremor/react components
vi.mock("@tremor/react", () => ({
Text: ({ children, className }: any) => <span className={className}>{children}</span>,
Title: ({ children }: any) => <h3>{children}</h3>,
Badge: ({ children, color, size }: any) => (
<span data-color={color} data-size={size}>
{children}
</span>
),
}));
describe("MakeAgentPublicForm", () => {
const mockProps = {
visible: true,
@ -123,15 +61,13 @@ describe("MakeAgentPublicForm", () => {
it("should initialize with correct state", () => {
render(<MakeAgentPublicForm {...mockProps} />);
// 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(<MakeAgentPublicForm {...mockProps} />);
// 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(<MakeAgentPublicForm {...mockProps} />);
// 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(<MakeAgentPublicForm {...mockProps} />);
// 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(<MakeAgentPublicForm {...mockProps} />);
// 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(<MakeAgentPublicForm {...mockProps} />);
// 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(<MakeAgentPublicForm {...mockProps} />);
// 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(<MakeAgentPublicForm {...propsWithManySkills} />);
// 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(<MakeAgentPublicForm {...mockProps} />);
// 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(<MakeAgentPublicForm {...mockProps} />);
// 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(<MakeAgentPublicForm {...invisibleProps} />);
// 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(<MakeAgentPublicForm {...mixedPublicProps} />);
// 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");
});
});

View file

@ -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 (
<ol className="flex items-center gap-2 mb-6">
{steps.map((label, i) => {
const active = i === current;
const completed = i < current;
return (
<li
key={label}
className="flex items-center gap-2 flex-1 min-w-0"
>
<div
className={cn(
"flex h-6 w-6 items-center justify-center rounded-full border text-xs font-medium",
completed
? "bg-primary text-primary-foreground border-primary"
: active
? "border-primary text-primary"
: "border-border text-muted-foreground",
)}
>
{completed ? <Check className="h-3 w-3" /> : i + 1}
</div>
<span
className={cn(
"text-sm truncate",
active || completed
? "text-foreground"
: "text-muted-foreground",
)}
>
{label}
</span>
{i < steps.length - 1 && (
<div className="h-px flex-1 bg-border" />
)}
</li>
);
})}
</ol>
);
}
const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
visible,
onClose,
@ -34,12 +75,10 @@ const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
const [currentStep, setCurrentStep] = useState(0);
const [selectedAgents, setSelectedAgents] = useState<Set<string>>(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<MakeAgentPublicFormProps> = ({
)}
{currentStep === 1 && (
<Button onClick={handleSubmit} disabled={loading}>
<Button
onClick={handleSubmit}
disabled={loading}
data-loading={loading ? "true" : undefined}
>
{loading ? "Making Public..." : "Make Public"}
</Button>
)}
@ -332,15 +375,9 @@ const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
<DialogHeader>
<DialogTitle>Make Agents Public</DialogTitle>
</DialogHeader>
<Form form={form} layout="vertical">
<Steps current={currentStep} className="mb-6">
<Step title="Select Agents" />
<Step title="Confirm" />
</Steps>
{renderStepContent()}
{renderStepButtons()}
</Form>
<Stepper current={currentStep} steps={["Select Agents", "Confirm"]} />
{renderStepContent()}
{renderStepButtons()}
</DialogContent>
</Dialog>
);

View file

@ -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 ? (
<div data-testid="modal">
<div>{title}</div>
{children}
{footer}
</div>
) : null,
Form: Object.assign(({ children, form }: any) => <form data-testid="form">{children}</form>, {
useForm: () => [
{
resetFields: vi.fn(),
validateFields: vi.fn(),
getFieldsValue: vi.fn(),
setFieldsValue: vi.fn(),
},
vi.fn(),
],
Item: ({ children }: any) => <div>{children}</div>,
}),
Steps: Object.assign(
({ children, current, className }: any) => (
<div data-testid="steps" className={className}>
{children}
</div>
),
{
Step: ({ title }: any) => <div>{title}</div>,
},
),
Button: ({ children, onClick, disabled, loading, ...props }: any) => (
<button onClick={onClick} disabled={disabled || loading} data-loading={loading} {...props}>
{children}
</button>
),
Checkbox: ({ checked, indeterminate, onChange, children, disabled }: any) => (
<label>
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange({ target: { checked: e.target.checked } })}
disabled={disabled}
data-indeterminate={indeterminate}
/>
{children}
</label>
),
}));
// Additional @tremor/react mocks (Button is already mocked globally)
vi.mock("@tremor/react", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tremor/react")>();
return {
...actual,
Text: ({ children, className }: any) => <span className={className}>{children}</span>,
Title: ({ children }: any) => <h3>{children}</h3>,
Badge: ({ children, color, size }: any) => (
<span data-color={color} data-size={size}>
{children}
</span>
),
};
});
describe("MakeMCPPublicForm", () => {
const mockProps = {
visible: true,
@ -150,15 +84,12 @@ describe("MakeMCPPublicForm", () => {
it("should initialize with correct state", () => {
render(<MakeMCPPublicForm {...mockProps} />);
// 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(<MakeMCPPublicForm {...mockProps} />);
// 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(<MakeMCPPublicForm {...mockProps} />);
// 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(<MakeMCPPublicForm {...mockProps} />);
// 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(<MakeMCPPublicForm {...mockProps} />);
// 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(<MakeMCPPublicForm {...mockProps} />);
// 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(<MakeMCPPublicForm {...mockProps} />);
// 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(<MakeMCPPublicForm {...propsWithManyTools} />);
// 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(<MakeMCPPublicForm {...mockProps} />);
// 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(<MakeMCPPublicForm {...mockProps} />);
// 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(<MakeMCPPublicForm {...invisibleProps} />);
// 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(<MakeMCPPublicForm {...mixedPublicProps} />);
// 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");
});
});

View file

@ -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 (
<ol className="flex items-center gap-2 mb-6">
{steps.map((label, i) => {
const active = i === current;
const completed = i < current;
return (
<li
key={label}
className="flex items-center gap-2 flex-1 min-w-0"
>
<div
className={cn(
"flex h-6 w-6 items-center justify-center rounded-full border text-xs font-medium",
completed
? "bg-primary text-primary-foreground border-primary"
: active
? "border-primary text-primary"
: "border-border text-muted-foreground",
)}
>
{completed ? <Check className="h-3 w-3" /> : i + 1}
</div>
<span
className={cn(
"text-sm truncate",
active || completed
? "text-foreground"
: "text-muted-foreground",
)}
>
{label}
</span>
{i < steps.length - 1 && (
<div className="h-px flex-1 bg-border" />
)}
</li>
);
})}
</ol>
);
}
const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
visible,
onClose,
@ -46,12 +87,10 @@ const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
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<MakeMCPPublicFormProps> = ({
)}
{currentStep === 1 && (
<Button onClick={handleSubmit} disabled={loading}>
<Button
onClick={handleSubmit}
disabled={loading}
data-loading={loading ? "true" : undefined}
>
{loading ? "Making Public..." : "Make Public"}
</Button>
)}
@ -379,15 +422,9 @@ const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
<DialogHeader>
<DialogTitle>Make MCP Servers Public</DialogTitle>
</DialogHeader>
<Form form={form} layout="vertical">
<Steps current={currentStep} className="mb-6">
<Step title="Select Servers" />
<Step title="Confirm" />
</Steps>
{renderStepContent()}
{renderStepButtons()}
</Form>
<Stepper current={currentStep} steps={["Select Servers", "Confirm"]} />
{renderStepContent()}
{renderStepButtons()}
</DialogContent>
</Dialog>
);

View file

@ -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 ? (
<div data-testid="modal">
<div>{title}</div>
{children}
{footer}
</div>
) : null,
Form: Object.assign(({ children, form }: any) => <form data-testid="form">{children}</form>, {
useForm: () => [
{
resetFields: vi.fn(),
validateFields: vi.fn(),
getFieldsValue: vi.fn(),
setFieldsValue: vi.fn(),
},
vi.fn(),
],
Item: ({ children }: any) => <div>{children}</div>,
}),
Steps: Object.assign(
({ children, current, className }: any) => (
<div data-testid="steps" className={className}>
{children}
</div>
),
{
Step: ({ title }: any) => <div>{title}</div>,
},
),
Button: ({ children, onClick, disabled, loading, ...props }: any) => (
<button onClick={onClick} disabled={disabled || loading} data-loading={loading} {...props}>
{children}
</button>
),
Checkbox: ({ checked, indeterminate, onChange, children, disabled }: any) => (
<label>
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange({ target: { checked: e.target.checked } })}
disabled={disabled}
data-indeterminate={indeterminate}
/>
{children}
</label>
),
}));
// Mock @tremor/react components
vi.mock("@tremor/react", () => ({
Text: ({ children, className }: any) => <span className={className}>{children}</span>,
Title: ({ children }: any) => <h3>{children}</h3>,
Badge: ({ children, color, size }: any) => (
<span data-color={color} data-size={size}>
{children}
</span>
),
}));
// 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(<MakeModelPublicForm {...mockProps} />);
// 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(<MakeModelPublicForm {...mockProps} />);
// 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(<MakeModelPublicForm {...mockProps} />);
// 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(<MakeModelPublicForm {...mockProps} />);
// 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(<MakeModelPublicForm {...mockProps} />);
// 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(<MakeModelPublicForm {...mockProps} />);
// 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(<MakeModelPublicForm {...mockProps} />);
// 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(<MakeModelPublicForm {...mockProps} />);
// 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(<MakeModelPublicForm {...mockProps} />);
// 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(<MakeModelPublicForm {...mockProps} />);
// 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(<MakeModelPublicForm {...invisibleProps} />);
// 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(<MakeModelPublicForm {...mixedPublicProps} />);
// 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(<MakeModelPublicForm {...mockProps} />);
// 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 `<strong>1</strong> 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(<MakeModelPublicForm {...mockProps} />);
// 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)
});
});

View file

@ -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 (
<ol className="flex items-center gap-2 mb-6">
{steps.map((label, i) => {
const active = i === current;
const completed = i < current;
return (
<li
key={label}
className="flex items-center gap-2 flex-1 min-w-0"
>
<div
className={cn(
"flex h-6 w-6 items-center justify-center rounded-full border text-xs font-medium",
completed
? "bg-primary text-primary-foreground border-primary"
: active
? "border-primary text-primary"
: "border-border text-muted-foreground",
)}
>
{completed ? <Check className="h-3 w-3" /> : i + 1}
</div>
<span
className={cn(
"text-sm truncate",
active || completed
? "text-foreground"
: "text-muted-foreground",
)}
>
{label}
</span>
{i < steps.length - 1 && (
<div className="h-px flex-1 bg-border" />
)}
</li>
);
})}
</ol>
);
}
const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
visible,
onClose,
@ -53,13 +95,11 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
const [selectedModels, setSelectedModels] = useState<Set<string>>(new Set());
const [filteredData, setFilteredData] = useState<ModelGroupInfo[]>([]);
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<MakeModelPublicFormProps> = ({
Users will still require a valid Virtual Key to use these models.
</p>
{/* Filters */}
<ModelFilters
modelHubData={modelHubData}
onFilteredDataChange={handleFilteredDataChange}
@ -332,7 +371,11 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
)}
{currentStep === 1 && (
<Button onClick={handleSubmit} disabled={loading}>
<Button
onClick={handleSubmit}
disabled={loading}
data-loading={loading ? "true" : undefined}
>
{loading ? "Making Public..." : "Make Public"}
</Button>
)}
@ -350,15 +393,9 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
<DialogHeader>
<DialogTitle>Make Models Public</DialogTitle>
</DialogHeader>
<Form form={form} layout="vertical">
<Steps current={currentStep} className="mb-6">
<Step title="Select Models" />
<Step title="Confirm" />
</Steps>
{renderStepContent()}
{renderStepButtons()}
</Form>
<Stepper current={currentStep} steps={["Select Models", "Confirm"]} />
{renderStepContent()}
{renderStepButtons()}
</DialogContent>
</Dialog>
);