mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
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:
parent
a2ed3848aa
commit
c2e8e4926b
8 changed files with 804 additions and 1018 deletions
|
|
@ -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
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue