mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge pull request #36908 from BerriAI/litellm_shadcn_aihub_0814
refactor(ui): migrate AI Hub off antd and tremor to shadcn
This commit is contained in:
commit
de57ebbad9
10 changed files with 815 additions and 973 deletions
|
|
@ -1709,54 +1709,26 @@
|
|||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"prefer-const": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"src/components/AIHub/SkillHubDashboard.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/AIHub/UsefulLinksManagement.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/AIHub/forms/MakeAgentPublicForm.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/AIHub/forms/MakeMCPPublicForm.test.tsx": {
|
||||
"react/display-name": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/AIHub/forms/MakeMCPPublicForm.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 2
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/AIHub/forms/MakeModelPublicForm.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,14 @@
|
|||
import React, { useMemo, useState } from "react";
|
||||
import { SearchOutlined } from "@ant-design/icons";
|
||||
import { SortingState } from "@tanstack/react-table";
|
||||
import { Input, Select } from "antd";
|
||||
import { Inbox } from "lucide-react";
|
||||
import { Inbox, Search, X } from "lucide-react";
|
||||
import { Plugin } from "@/components/claude_code_plugins/types";
|
||||
import { DataTable } from "@/components/shared/DataTable";
|
||||
import { getSkillHubTableColumns } from "@/components/AIHub/SkillHubTableColumns";
|
||||
import SkillDetail from "@/components/claude_code_plugins/skill_detail";
|
||||
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
const ALL_DOMAINS = "__all_domains__";
|
||||
|
||||
interface SkillHubDashboardProps {
|
||||
skills: Plugin[];
|
||||
|
|
@ -48,7 +50,10 @@ const SkillHubDashboard: React.FC<SkillHubDashboardProps> = ({
|
|||
|
||||
// Derived stats
|
||||
const totalSkills = skills.length;
|
||||
const domains = useMemo(() => [...new Set(skills.map((s) => s.domain).filter(Boolean))], [skills]);
|
||||
const domains = useMemo(
|
||||
() => [...new Set(skills.map((s) => s.domain).filter((domain): domain is string => Boolean(domain)))],
|
||||
[skills],
|
||||
);
|
||||
const namespaces = useMemo(() => [...new Set(skills.map((s) => s.namespace).filter(Boolean))], [skills]);
|
||||
|
||||
// Filtered table data
|
||||
|
|
@ -73,6 +78,11 @@ const SkillHubDashboard: React.FC<SkillHubDashboardProps> = ({
|
|||
|
||||
const columns = useMemo(() => getSkillHubTableColumns({ onSkillClick: setSelectedSkill }), []);
|
||||
|
||||
const domainItems = useMemo(
|
||||
() => [{ value: ALL_DOMAINS, label: "All Domains" }, ...domains.map((d) => ({ value: d, label: d }))],
|
||||
[domains],
|
||||
);
|
||||
|
||||
const hasActiveFilter = search.trim().length > 0 || domainFilter != null;
|
||||
|
||||
if (selectedSkill) {
|
||||
|
|
@ -111,21 +121,43 @@ const SkillHubDashboard: React.FC<SkillHubDashboardProps> = ({
|
|||
<h3 className="text-sm font-semibold text-gray-700">All {publicPage ? "Public " : ""}Skills</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
placeholder="All Domains"
|
||||
allowClear
|
||||
value={domainFilter}
|
||||
onChange={(val) => setDomainFilter(val)}
|
||||
style={{ width: 160 }}
|
||||
options={domains.map((d) => ({ label: d, value: d }))}
|
||||
/>
|
||||
<Input
|
||||
prefix={<SearchOutlined className="text-gray-400" />}
|
||||
placeholder="Search by name, namespace, or tag…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
style={{ width: 280 }}
|
||||
allowClear
|
||||
/>
|
||||
items={domainItems}
|
||||
value={domainFilter ?? ALL_DOMAINS}
|
||||
onValueChange={(val) => setDomainFilter(val === null || val === ALL_DOMAINS ? undefined : val)}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{domainItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputGroup className="w-[280px]">
|
||||
<InputGroupAddon>
|
||||
<Search className="size-4 text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
placeholder="Search by name, namespace, or tag…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
{search !== "" && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
aria-label="Clear search"
|
||||
onClick={() => setSearch("")}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
)}
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import TableIconActionButton from "@/components/common_components/IconActionButt
|
|||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { isAdminRole } from "@/utils/roles";
|
||||
import { ChevronDownIcon, ChevronRightIcon, ExternalLinkIcon, PlusCircleIcon } from "@heroicons/react/outline";
|
||||
import { Card, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Text, Title } from "@tremor/react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getProxyBaseUrl, getPublicModelHubInfo, updateUsefulLinksCall } from "../networking";
|
||||
|
|
@ -223,10 +224,10 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessTok
|
|||
};
|
||||
|
||||
return (
|
||||
<Card className="mb-6">
|
||||
<Card className="mb-6 px-6">
|
||||
<div className="flex items-center justify-between cursor-pointer" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<div className="flex flex-col">
|
||||
<Title className="mb-0">Link Management</Title>
|
||||
<h3 className="mb-0 text-lg font-semibold">Link Management</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
Manage the links that are displayed under 'Useful Links' on the public model hub.
|
||||
</p>
|
||||
|
|
@ -243,7 +244,7 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessTok
|
|||
{isExpanded && (
|
||||
<div className="mt-4">
|
||||
<div className="mb-6">
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">Add New Link</Text>
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">Add New Link</p>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">Display Name</label>
|
||||
|
|
@ -288,7 +289,7 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessTok
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Text className="text-sm font-medium text-gray-700">Manage Existing Links</Text>
|
||||
<p className="text-sm font-medium text-gray-700">Manage Existing Links</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link
|
||||
href={`${getProxyBaseUrl()}/ui/model_hub_table`}
|
||||
|
|
@ -328,13 +329,13 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessTok
|
|||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
<TableHead>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell className="py-1 h-8">Display Name</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">URL</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">Actions</TableHeaderCell>
|
||||
<TableHead className="py-1 h-8">Display Name</TableHead>
|
||||
<TableHead className="py-1 h-8">URL</TableHead>
|
||||
<TableHead className="py-1 h-8">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{links.map((link, index) => (
|
||||
<TableRow key={link.id} className="h-8">
|
||||
|
|
|
|||
|
|
@ -12,67 +12,8 @@ vi.mock("../../networking", () => ({
|
|||
import { makeAgentsPublicCall } from "../../networking";
|
||||
const mockMakeAgentsPublicCall = vi.mocked(makeAgentsPublicCall);
|
||||
|
||||
// Mock antd components
|
||||
vi.mock("antd", () => ({
|
||||
Modal: ({ open, title, children, onCancel, footer }: any) =>
|
||||
open ? (
|
||||
<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>
|
||||
),
|
||||
}));
|
||||
const expectDisabledControl = (element: HTMLElement) =>
|
||||
expect(element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true").toBe(true);
|
||||
|
||||
describe("MakeAgentPublicForm", () => {
|
||||
const mockProps = {
|
||||
|
|
@ -143,7 +84,7 @@ describe("MakeAgentPublicForm", () => {
|
|||
expect(screen.getByText("Select Agents to Make Public")).toBeInTheDocument();
|
||||
|
||||
// Select all agents using the select all checkbox
|
||||
const selectAllCheckbox = screen.getByLabelText("Select All (2)");
|
||||
const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All (2)" });
|
||||
await act(async () => {
|
||||
fireEvent.click(selectAllCheckbox);
|
||||
});
|
||||
|
|
@ -169,12 +110,11 @@ describe("MakeAgentPublicForm", () => {
|
|||
render(<MakeAgentPublicForm {...mockProps} />);
|
||||
|
||||
// Select all agents
|
||||
const selectAllCheckbox = screen.getByLabelText("Select All (2)");
|
||||
const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All (2)" });
|
||||
await act(async () => {
|
||||
fireEvent.click(selectAllCheckbox);
|
||||
});
|
||||
|
||||
// Navigate to confirm step
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
await act(async () => {
|
||||
fireEvent.click(nextButton);
|
||||
|
|
@ -185,7 +125,6 @@ describe("MakeAgentPublicForm", () => {
|
|||
expect(screen.getByText("Confirm Making Agents Public")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Submit
|
||||
const submitButton = screen.getByRole("button", { name: "Make Public" });
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
|
|
@ -232,6 +171,8 @@ describe("MakeAgentPublicForm", () => {
|
|||
const checkboxes = screen.getAllByRole("checkbox");
|
||||
await act(async () => {
|
||||
fireEvent.click(checkboxes[0]); // Click select all to select all
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(checkboxes[0]); // Click select all again to deselect all
|
||||
});
|
||||
|
||||
|
|
@ -256,8 +197,8 @@ describe("MakeAgentPublicForm", () => {
|
|||
expect(screen.getByText("No agents available.")).toBeInTheDocument();
|
||||
|
||||
// Select All checkbox should be disabled
|
||||
const selectAllCheckbox = screen.getByLabelText("Select All");
|
||||
expect(selectAllCheckbox).toBeDisabled();
|
||||
const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All" });
|
||||
expectDisabledControl(selectAllCheckbox);
|
||||
|
||||
// Next button should be disabled
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
|
|
@ -332,7 +273,7 @@ describe("MakeAgentPublicForm", () => {
|
|||
|
||||
// Select all should be indeterminate now
|
||||
const selectAllCheckbox = checkboxes[0];
|
||||
expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
|
||||
expect(selectAllCheckbox).toBePartiallyChecked();
|
||||
});
|
||||
|
||||
it("should display skills overflow text when agent has more than 3 skills", () => {
|
||||
|
|
@ -369,7 +310,6 @@ describe("MakeAgentPublicForm", () => {
|
|||
|
||||
render(<MakeAgentPublicForm {...mockProps} />);
|
||||
|
||||
// Navigate to confirm step
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
await act(async () => {
|
||||
fireEvent.click(nextButton);
|
||||
|
|
@ -379,7 +319,6 @@ describe("MakeAgentPublicForm", () => {
|
|||
expect(screen.getByText("Confirm Making Agents Public")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Submit
|
||||
const submitButton = screen.getByRole("button", { name: "Make Public" });
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
|
|
@ -395,7 +334,7 @@ describe("MakeAgentPublicForm", () => {
|
|||
expect(mockProps.onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show loading state during submit", async () => {
|
||||
it("should not complete the flow until the submit request resolves", async () => {
|
||||
let resolvePromise: (value: any) => void = () => {};
|
||||
const pendingPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
|
|
@ -404,7 +343,6 @@ describe("MakeAgentPublicForm", () => {
|
|||
|
||||
render(<MakeAgentPublicForm {...mockProps} />);
|
||||
|
||||
// Navigate to confirm step
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
await act(async () => {
|
||||
fireEvent.click(nextButton);
|
||||
|
|
@ -414,17 +352,20 @@ describe("MakeAgentPublicForm", () => {
|
|||
expect(screen.getByText("Confirm Making Agents Public")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Submit
|
||||
const submitButton = screen.getByRole("button", { name: "Make Public" });
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
// Check loading state
|
||||
expect(submitButton).toHaveAttribute("data-loading", "true");
|
||||
expect(submitButton).toBeDisabled();
|
||||
expectDisabledControl(submitButton);
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
expect(mockMakeAgentsPublicCall).toHaveBeenCalledTimes(1);
|
||||
expect(mockProps.onSuccess).not.toHaveBeenCalled();
|
||||
expect(mockProps.onClose).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("Confirm Making Agents Public")).toBeInTheDocument();
|
||||
|
||||
// Resolve the promise
|
||||
resolvePromise({});
|
||||
await waitFor(() => {
|
||||
expect(mockProps.onSuccess).toHaveBeenCalled();
|
||||
|
|
@ -441,7 +382,7 @@ describe("MakeAgentPublicForm", () => {
|
|||
render(<MakeAgentPublicForm {...invisibleProps} />);
|
||||
|
||||
// Modal should not be rendered
|
||||
expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Make Agents Public")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -500,6 +441,6 @@ describe("MakeAgentPublicForm", () => {
|
|||
|
||||
// Select all should be indeterminate
|
||||
const selectAllCheckbox = checkboxes[0];
|
||||
expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
|
||||
expect(selectAllCheckbox).toBePartiallyChecked();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Form, Steps, Button, Checkbox } from "antd";
|
||||
import { Text, Title, Badge } from "@tremor/react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/cva.config";
|
||||
import { makeAgentsPublicCall } from "../../networking";
|
||||
import NotificationsManager from "../../molecules/notifications_manager";
|
||||
import { AgentHubData } from "@/components/AIHub/AgentHubTableColumns";
|
||||
|
||||
const { Step } = Steps;
|
||||
const STEP_TITLES = ["Select Agents", "Confirm"];
|
||||
|
||||
interface MakeAgentPublicFormProps {
|
||||
visible: boolean;
|
||||
|
|
@ -25,12 +29,10 @@ const MakeAgentPublicForm: React.FC<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();
|
||||
};
|
||||
|
||||
|
|
@ -113,29 +115,30 @@ const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title>Select Agents to Make Public</Title>
|
||||
<h3 className="text-lg font-semibold">Select Agents to Make Public</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={allAgentsSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
disabled={agentHubData.length === 0}
|
||||
>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
checked={allAgentsSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onCheckedChange={(checked) => handleSelectAll(checked === true)}
|
||||
disabled={agentHubData.length === 0}
|
||||
/>
|
||||
Select All {agentHubData.length > 0 && `(${agentHubData.length})`}
|
||||
</Checkbox>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Text className="text-sm text-gray-600">
|
||||
<p className="text-sm text-gray-600">
|
||||
Select the agents you want to be visible on the public model hub. Users will still require a valid Virtual Key
|
||||
to use these agents.
|
||||
</Text>
|
||||
</p>
|
||||
|
||||
<div className="max-h-96 overflow-y-auto border rounded-lg p-4">
|
||||
<div className="space-y-3">
|
||||
{agentHubData.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Text>No agents available.</Text>
|
||||
<p>No agents available.</p>
|
||||
</div>
|
||||
) : (
|
||||
agentHubData.map((agent) => {
|
||||
|
|
@ -144,25 +147,23 @@ const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
|
|||
<div key={agentId} className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50">
|
||||
<Checkbox
|
||||
checked={selectedAgents.has(agentId)}
|
||||
onChange={(e) => handleAgentSelection(agentId, e.target.checked)}
|
||||
onCheckedChange={(checked) => handleAgentSelection(agentId, checked === true)}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium">{agent.name}</Text>
|
||||
<Badge color="blue" size="sm">
|
||||
v{agent.version}
|
||||
</Badge>
|
||||
<p className="font-medium break-words">{agent.name}</p>
|
||||
<Badge variant="secondary">v{agent.version}</Badge>
|
||||
</div>
|
||||
<Text className="text-xs text-gray-600 mt-1">{agent.description}</Text>
|
||||
<p className="text-xs text-gray-600 mt-1 break-words">{agent.description}</p>
|
||||
{agent.skills && agent.skills.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{agent.skills.slice(0, 3).map((skill) => (
|
||||
<Badge key={skill.id} color="purple" size="xs">
|
||||
<Badge key={skill.id} variant="outline">
|
||||
{skill.name}
|
||||
</Badge>
|
||||
))}
|
||||
{agent.skills.length > 3 && (
|
||||
<Text className="text-xs text-gray-500">+{agent.skills.length - 3} more</Text>
|
||||
<p className="text-xs text-gray-500">+{agent.skills.length - 3} more</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -176,9 +177,9 @@ const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
|
|||
|
||||
{selectedAgents.size > 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<Text className="text-sm text-blue-800">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>{selectedAgents.size}</strong> agent{selectedAgents.size !== 1 ? "s" : ""} selected
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -188,33 +189,31 @@ const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
|
|||
const renderStep2Content = () => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Title>Confirm Making Agents Public</Title>
|
||||
<h3 className="text-lg font-semibold">Confirm Making Agents Public</h3>
|
||||
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<Text className="text-sm text-yellow-800">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Warning:</strong> Once you make these agents public, anyone who can go to the{" "}
|
||||
<code>/ui/model_hub_table</code> will be able to know they exist on the proxy.
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Text className="font-medium">Agents to be made public:</Text>
|
||||
<p className="font-medium">Agents to be made public:</p>
|
||||
<div className="max-h-48 overflow-y-auto border rounded-lg p-3">
|
||||
<div className="space-y-2">
|
||||
{Array.from(selectedAgents).map((agentId) => {
|
||||
const agent = agentHubData.find((a) => (a.agent_id || a.name) === agentId);
|
||||
return (
|
||||
<div key={agentId} className="flex items-center justify-between p-2 bg-gray-50 rounded-sm">
|
||||
<div className="flex-1">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium">{agent?.name || agentId}</Text>
|
||||
{agent && (
|
||||
<Badge color="blue" size="xs">
|
||||
v{agent.version}
|
||||
</Badge>
|
||||
)}
|
||||
<p className="font-medium break-words">{agent?.name || agentId}</p>
|
||||
{agent && <Badge variant="secondary">v{agent.version}</Badge>}
|
||||
</div>
|
||||
{agent?.description && <Text className="text-xs text-gray-600 mt-1">{agent.description}</Text>}
|
||||
{agent?.description && (
|
||||
<p className="text-xs text-gray-600 mt-1 break-words">{agent.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -224,10 +223,10 @@ const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<Text className="text-sm text-blue-800">
|
||||
<p className="text-sm text-blue-800">
|
||||
Total: <strong>{selectedAgents.size}</strong> agent{selectedAgents.size !== 1 ? "s" : ""} will be made
|
||||
public
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -247,7 +246,7 @@ const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
|
|||
const renderStepButtons = () => {
|
||||
return (
|
||||
<div className="flex justify-between mt-6">
|
||||
<Button onClick={currentStep === 0 ? handleClose : handlePrevious}>
|
||||
<Button variant="outline" onClick={currentStep === 0 ? handleClose : handlePrevious}>
|
||||
{currentStep === 0 ? "Cancel" : "Previous"}
|
||||
</Button>
|
||||
|
||||
|
|
@ -259,7 +258,8 @@ const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
|
|||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<Button onClick={handleSubmit} loading={loading}>
|
||||
<Button onClick={handleSubmit} disabled={loading}>
|
||||
{loading && <Loader2 className="size-4 animate-spin" />}
|
||||
Make Public
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -269,24 +269,42 @@ const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Make Agents Public"
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={1200}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Steps current={currentStep} className="mb-6">
|
||||
<Step title="Select Agents" />
|
||||
<Step title="Confirm" />
|
||||
</Steps>
|
||||
<Dialog open={visible} onOpenChange={(open) => !open && handleClose()} disablePointerDismissal>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[1200px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Make Agents Public</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{renderStepContent()}
|
||||
{renderStepButtons()}
|
||||
</Form>
|
||||
</Modal>
|
||||
<div>
|
||||
<ol className="mb-6 flex items-center gap-6">
|
||||
{STEP_TITLES.map((title, index) => (
|
||||
<li
|
||||
key={title}
|
||||
className="flex items-center gap-2"
|
||||
aria-current={currentStep === index ? "step" : undefined}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-6 items-center justify-center rounded-full border text-xs",
|
||||
currentStep === index
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-border text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className={cn("text-sm", currentStep === index ? "font-medium" : "text-muted-foreground")}>
|
||||
{title}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{renderStepContent()}
|
||||
{renderStepButtons()}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,83 +12,8 @@ vi.mock("../../networking", () => ({
|
|||
import { makeMCPPublicCall } from "../../networking";
|
||||
const mockMakeMCPPublicCall = vi.mocked(makeMCPPublicCall);
|
||||
|
||||
// Mock antd components
|
||||
vi.mock("antd", () => ({
|
||||
Modal: ({ open, title, children, onCancel, footer }: any) =>
|
||||
open ? (
|
||||
<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.
|
||||
// NOTE: the comment used to say "Button is already mocked globally" — that was
|
||||
// incorrect. A file-level vi.mock fully replaces the setup-level mock from
|
||||
// tests/setupTests.ts, so we must re-apply the Button/Tooltip overrides here.
|
||||
// Without them, the real Tremor Button leaks through and its useTooltip(300)
|
||||
// schedules a native setTimeout that can fire post-teardown -> "window is not defined".
|
||||
vi.mock("@tremor/react", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@tremor/react")>();
|
||||
const React = await import("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>
|
||||
),
|
||||
Button: React.forwardRef<HTMLButtonElement, any>(({ children, ...props }, ref) => (
|
||||
<button {...props} ref={ref}>
|
||||
{children}
|
||||
</button>
|
||||
)),
|
||||
Tooltip: ({ children }: any) => <>{children}</>,
|
||||
};
|
||||
});
|
||||
const expectDisabledControl = (element: HTMLElement) =>
|
||||
expect(element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true").toBe(true);
|
||||
|
||||
describe("MakeMCPPublicForm", () => {
|
||||
const mockProps = {
|
||||
|
|
@ -182,7 +107,7 @@ describe("MakeMCPPublicForm", () => {
|
|||
expect(screen.getByText("Select MCP Servers to Make Public")).toBeInTheDocument();
|
||||
|
||||
// Select all servers using the select all checkbox
|
||||
const selectAllCheckbox = screen.getByLabelText("Select All (2)");
|
||||
const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All (2)" });
|
||||
await act(async () => {
|
||||
fireEvent.click(selectAllCheckbox);
|
||||
});
|
||||
|
|
@ -208,12 +133,11 @@ describe("MakeMCPPublicForm", () => {
|
|||
render(<MakeMCPPublicForm {...mockProps} />);
|
||||
|
||||
// Select all servers
|
||||
const selectAllCheckbox = screen.getByLabelText("Select All (2)");
|
||||
const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All (2)" });
|
||||
await act(async () => {
|
||||
fireEvent.click(selectAllCheckbox);
|
||||
});
|
||||
|
||||
// Navigate to confirm step
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
await act(async () => {
|
||||
fireEvent.click(nextButton);
|
||||
|
|
@ -224,7 +148,6 @@ describe("MakeMCPPublicForm", () => {
|
|||
expect(screen.getByText("Confirm Making MCP Servers Public")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Submit
|
||||
const submitButton = screen.getByRole("button", { name: "Make Public" });
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
|
|
@ -271,6 +194,8 @@ describe("MakeMCPPublicForm", () => {
|
|||
const checkboxes = screen.getAllByRole("checkbox");
|
||||
await act(async () => {
|
||||
fireEvent.click(checkboxes[0]); // Click select all to select all
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(checkboxes[0]); // Click select all again to deselect all
|
||||
});
|
||||
|
||||
|
|
@ -295,8 +220,8 @@ describe("MakeMCPPublicForm", () => {
|
|||
expect(screen.getByText("No MCP servers available.")).toBeInTheDocument();
|
||||
|
||||
// Select All checkbox should be disabled
|
||||
const selectAllCheckbox = screen.getByLabelText("Select All");
|
||||
expect(selectAllCheckbox).toBeDisabled();
|
||||
const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All" });
|
||||
expectDisabledControl(selectAllCheckbox);
|
||||
|
||||
// Next button should be disabled
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
|
|
@ -371,7 +296,7 @@ describe("MakeMCPPublicForm", () => {
|
|||
|
||||
// Select all should be indeterminate now
|
||||
const selectAllCheckbox = checkboxes[0];
|
||||
expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
|
||||
expect(selectAllCheckbox).toBePartiallyChecked();
|
||||
});
|
||||
|
||||
it("should display tools overflow text when server has more than 3 tools", () => {
|
||||
|
|
@ -402,7 +327,6 @@ describe("MakeMCPPublicForm", () => {
|
|||
|
||||
render(<MakeMCPPublicForm {...mockProps} />);
|
||||
|
||||
// Navigate to confirm step
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
await act(async () => {
|
||||
fireEvent.click(nextButton);
|
||||
|
|
@ -412,7 +336,6 @@ describe("MakeMCPPublicForm", () => {
|
|||
expect(screen.getByText("Confirm Making MCP Servers Public")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Submit
|
||||
const submitButton = screen.getByRole("button", { name: "Make Public" });
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
|
|
@ -428,7 +351,7 @@ describe("MakeMCPPublicForm", () => {
|
|||
expect(mockProps.onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show loading state during submit", async () => {
|
||||
it("should not complete the flow until the submit request resolves", async () => {
|
||||
let resolvePromise: (value: any) => void = () => {};
|
||||
const pendingPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
|
|
@ -437,7 +360,6 @@ describe("MakeMCPPublicForm", () => {
|
|||
|
||||
render(<MakeMCPPublicForm {...mockProps} />);
|
||||
|
||||
// Navigate to confirm step
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
await act(async () => {
|
||||
fireEvent.click(nextButton);
|
||||
|
|
@ -447,17 +369,20 @@ describe("MakeMCPPublicForm", () => {
|
|||
expect(screen.getByText("Confirm Making MCP Servers Public")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Submit
|
||||
const submitButton = screen.getByRole("button", { name: "Make Public" });
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
// Check loading state
|
||||
expect(submitButton).toHaveAttribute("data-loading", "true");
|
||||
expect(submitButton).toBeDisabled();
|
||||
expectDisabledControl(submitButton);
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
expect(mockMakeMCPPublicCall).toHaveBeenCalledTimes(1);
|
||||
expect(mockProps.onSuccess).not.toHaveBeenCalled();
|
||||
expect(mockProps.onClose).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("Confirm Making MCP Servers Public")).toBeInTheDocument();
|
||||
|
||||
// Resolve the promise
|
||||
resolvePromise({});
|
||||
await waitFor(() => {
|
||||
expect(mockProps.onSuccess).toHaveBeenCalled();
|
||||
|
|
@ -474,7 +399,7 @@ describe("MakeMCPPublicForm", () => {
|
|||
render(<MakeMCPPublicForm {...invisibleProps} />);
|
||||
|
||||
// Modal should not be rendered
|
||||
expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Make MCP Servers Public")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -569,6 +494,6 @@ describe("MakeMCPPublicForm", () => {
|
|||
|
||||
// Select all should be indeterminate
|
||||
const selectAllCheckbox = checkboxes[0];
|
||||
expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
|
||||
expect(selectAllCheckbox).toBePartiallyChecked();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,25 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Form, Steps, Button, Checkbox } from "antd";
|
||||
import { Text, Title, Badge } from "@tremor/react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/cva.config";
|
||||
import { makeMCPPublicCall } from "../../networking";
|
||||
import NotificationsManager from "../../molecules/notifications_manager";
|
||||
import { MCPServerData } from "@/components/AIHub/MCPHubTableColumns";
|
||||
|
||||
const { Step } = Steps;
|
||||
const STEP_TITLES = ["Select Servers", "Confirm"];
|
||||
|
||||
const statusVariant = (status?: string) => {
|
||||
if (status === "active" || status === "healthy") {
|
||||
return "default" as const;
|
||||
}
|
||||
if (status === "inactive" || status === "unhealthy") {
|
||||
return "destructive" as const;
|
||||
}
|
||||
return "outline" as const;
|
||||
};
|
||||
|
||||
interface MakeMCPPublicFormProps {
|
||||
visible: boolean;
|
||||
|
|
@ -25,12 +39,10 @@ const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
|
|||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [selectedServers, setSelectedServers] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleClose = () => {
|
||||
setCurrentStep(0);
|
||||
setSelectedServers(new Set());
|
||||
form.resetFields();
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
|
@ -114,29 +126,30 @@ const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title>Select MCP Servers to Make Public</Title>
|
||||
<h3 className="text-lg font-semibold">Select MCP Servers to Make Public</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={allServersSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
disabled={mcpHubData.length === 0}
|
||||
>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
checked={allServersSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onCheckedChange={(checked) => handleSelectAll(checked === true)}
|
||||
disabled={mcpHubData.length === 0}
|
||||
/>
|
||||
Select All {mcpHubData.length > 0 && `(${mcpHubData.length})`}
|
||||
</Checkbox>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Text className="text-sm text-gray-600">
|
||||
<p className="text-sm text-gray-600">
|
||||
Select the MCP servers you want to be visible on the public model hub. Users will still require a valid
|
||||
Virtual Key to use these servers.
|
||||
</Text>
|
||||
</p>
|
||||
|
||||
<div className="max-h-96 overflow-y-auto border rounded-lg p-4">
|
||||
<div className="space-y-3">
|
||||
{mcpHubData.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Text>No MCP servers available.</Text>
|
||||
<p>No MCP servers available.</p>
|
||||
</div>
|
||||
) : (
|
||||
mcpHubData.map((server) => {
|
||||
|
|
@ -148,42 +161,25 @@ const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
|
|||
>
|
||||
<Checkbox
|
||||
checked={selectedServers.has(server.server_id)}
|
||||
onChange={(e) => handleServerSelection(server.server_id, e.target.checked)}
|
||||
onCheckedChange={(checked) => handleServerSelection(server.server_id, checked === true)}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium">{server.server_name}</Text>
|
||||
{isPublic && (
|
||||
<Badge color="emerald" size="sm">
|
||||
Public
|
||||
</Badge>
|
||||
)}
|
||||
<Badge color="blue" size="sm">
|
||||
{server.transport}
|
||||
</Badge>
|
||||
<Badge
|
||||
color={
|
||||
server.status === "active" || server.status === "healthy"
|
||||
? "green"
|
||||
: server.status === "inactive" || server.status === "unhealthy"
|
||||
? "red"
|
||||
: "gray"
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
{server.status || "unknown"}
|
||||
</Badge>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="font-medium break-words">{server.server_name}</p>
|
||||
{isPublic && <Badge>Public</Badge>}
|
||||
<Badge variant="secondary">{server.transport}</Badge>
|
||||
<Badge variant={statusVariant(server.status)}>{server.status || "unknown"}</Badge>
|
||||
</div>
|
||||
<Text className="text-xs text-gray-600 mt-1">{server.description || server.url}</Text>
|
||||
<p className="text-xs text-gray-600 mt-1 break-words">{server.description || server.url}</p>
|
||||
{server.allowed_tools && server.allowed_tools.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{server.allowed_tools.slice(0, 3).map((tool, idx) => (
|
||||
<Badge key={idx} color="purple" size="xs">
|
||||
<Badge key={idx} variant="outline">
|
||||
{tool}
|
||||
</Badge>
|
||||
))}
|
||||
{server.allowed_tools.length > 3 && (
|
||||
<Text className="text-xs text-gray-500">+{server.allowed_tools.length - 3} more</Text>
|
||||
<p className="text-xs text-gray-500">+{server.allowed_tools.length - 3} more</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -197,9 +193,9 @@ const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
|
|||
|
||||
{selectedServers.size > 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<Text className="text-sm text-blue-800">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>{selectedServers.size}</strong> MCP server{selectedServers.size !== 1 ? "s" : ""} selected
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -209,48 +205,37 @@ const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
|
|||
const renderStep2Content = () => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Title>Confirm Making MCP Servers Public</Title>
|
||||
<h3 className="text-lg font-semibold">Confirm Making MCP Servers Public</h3>
|
||||
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<Text className="text-sm text-yellow-800">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Warning:</strong> Once you make these MCP servers public, anyone who can go to the{" "}
|
||||
<code>/ui/model_hub_table</code> will be able to know they exist on the proxy.
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Text className="font-medium">MCP Servers to be made public:</Text>
|
||||
<p className="font-medium">MCP Servers to be made public:</p>
|
||||
<div className="max-h-48 overflow-y-auto border rounded-lg p-3">
|
||||
<div className="space-y-2">
|
||||
{Array.from(selectedServers).map((serverId) => {
|
||||
const server = mcpHubData.find((s) => s.server_id === serverId);
|
||||
return (
|
||||
<div key={serverId} className="flex items-center justify-between p-2 bg-gray-50 rounded-sm">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium">{server?.server_name || serverId}</Text>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="font-medium break-words">{server?.server_name || serverId}</p>
|
||||
{server && (
|
||||
<>
|
||||
<Badge color="blue" size="xs">
|
||||
{server.transport}
|
||||
</Badge>
|
||||
<Badge
|
||||
color={
|
||||
server.status === "active" || server.status === "healthy"
|
||||
? "green"
|
||||
: server.status === "inactive" || server.status === "unhealthy"
|
||||
? "red"
|
||||
: "gray"
|
||||
}
|
||||
size="xs"
|
||||
>
|
||||
{server.status || "unknown"}
|
||||
</Badge>
|
||||
<Badge variant="secondary">{server.transport}</Badge>
|
||||
<Badge variant={statusVariant(server.status)}>{server.status || "unknown"}</Badge>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{server?.description && <Text className="text-xs text-gray-600 mt-1">{server.description}</Text>}
|
||||
{server?.url && <Text className="text-xs text-gray-500 mt-1">{server.url}</Text>}
|
||||
{server?.description && (
|
||||
<p className="text-xs text-gray-600 mt-1 break-words">{server.description}</p>
|
||||
)}
|
||||
{server?.url && <p className="text-xs text-gray-500 mt-1 break-words">{server.url}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -260,10 +245,10 @@ const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<Text className="text-sm text-blue-800">
|
||||
<p className="text-sm text-blue-800">
|
||||
Total: <strong>{selectedServers.size}</strong> MCP server{selectedServers.size !== 1 ? "s" : ""} will be
|
||||
made public
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -283,7 +268,7 @@ const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
|
|||
const renderStepButtons = () => {
|
||||
return (
|
||||
<div className="flex justify-between mt-6">
|
||||
<Button onClick={currentStep === 0 ? handleClose : handlePrevious}>
|
||||
<Button variant="outline" onClick={currentStep === 0 ? handleClose : handlePrevious}>
|
||||
{currentStep === 0 ? "Cancel" : "Previous"}
|
||||
</Button>
|
||||
|
||||
|
|
@ -295,7 +280,8 @@ const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
|
|||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<Button onClick={handleSubmit} loading={loading}>
|
||||
<Button onClick={handleSubmit} disabled={loading}>
|
||||
{loading && <Loader2 className="size-4 animate-spin" />}
|
||||
Make Public
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -305,24 +291,42 @@ const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Make MCP Servers Public"
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={1200}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Steps current={currentStep} className="mb-6">
|
||||
<Step title="Select Servers" />
|
||||
<Step title="Confirm" />
|
||||
</Steps>
|
||||
<Dialog open={visible} onOpenChange={(open) => !open && handleClose()} disablePointerDismissal>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[1200px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Make MCP Servers Public</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{renderStepContent()}
|
||||
{renderStepButtons()}
|
||||
</Form>
|
||||
</Modal>
|
||||
<div>
|
||||
<ol className="mb-6 flex items-center gap-6">
|
||||
{STEP_TITLES.map((title, index) => (
|
||||
<li
|
||||
key={title}
|
||||
className="flex items-center gap-2"
|
||||
aria-current={currentStep === index ? "step" : undefined}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-6 items-center justify-center rounded-full border text-xs",
|
||||
currentStep === index
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-border text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className={cn("text-sm", currentStep === index ? "font-medium" : "text-muted-foreground")}>
|
||||
{title}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{renderStepContent()}
|
||||
{renderStepButtons()}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -29,67 +29,8 @@ vi.mock("../../networking", () => ({
|
|||
import { makeModelGroupPublic } from "../../networking";
|
||||
const mockMakeModelGroupPublic = vi.mocked(makeModelGroupPublic);
|
||||
|
||||
// Mock antd components
|
||||
vi.mock("antd", () => ({
|
||||
Modal: ({ open, title, children, onCancel, footer }: any) =>
|
||||
open ? (
|
||||
<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>
|
||||
),
|
||||
}));
|
||||
const expectDisabledControl = (element: HTMLElement) =>
|
||||
expect(element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true").toBe(true);
|
||||
|
||||
// Mock ModelFilters component
|
||||
vi.mock("../../model_filters", () => ({
|
||||
|
|
@ -190,7 +131,7 @@ describe("MakeModelPublicForm", () => {
|
|||
expect(screen.getByText("Select Models to Make Public")).toBeInTheDocument();
|
||||
|
||||
// Select all models using the select all checkbox
|
||||
const selectAllCheckbox = screen.getByLabelText("Select All (2)");
|
||||
const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All (2)" });
|
||||
await act(async () => {
|
||||
fireEvent.click(selectAllCheckbox);
|
||||
});
|
||||
|
|
@ -216,12 +157,11 @@ describe("MakeModelPublicForm", () => {
|
|||
render(<MakeModelPublicForm {...mockProps} />);
|
||||
|
||||
// Select all models
|
||||
const selectAllCheckbox = screen.getByLabelText("Select All (2)");
|
||||
const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All (2)" });
|
||||
await act(async () => {
|
||||
fireEvent.click(selectAllCheckbox);
|
||||
});
|
||||
|
||||
// Navigate to confirm step
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
await act(async () => {
|
||||
fireEvent.click(nextButton);
|
||||
|
|
@ -232,7 +172,6 @@ describe("MakeModelPublicForm", () => {
|
|||
expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Submit
|
||||
const submitButton = screen.getByRole("button", { name: "Make Public" });
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
|
|
@ -279,6 +218,8 @@ describe("MakeModelPublicForm", () => {
|
|||
const checkboxes = screen.getAllByRole("checkbox");
|
||||
await act(async () => {
|
||||
fireEvent.click(checkboxes[0]); // Click select all to select all
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(checkboxes[0]); // Click select all again to deselect all
|
||||
});
|
||||
|
||||
|
|
@ -303,8 +244,8 @@ describe("MakeModelPublicForm", () => {
|
|||
expect(screen.getByText("No models match the current filters.")).toBeInTheDocument();
|
||||
|
||||
// Select All checkbox should be disabled
|
||||
const selectAllCheckbox = screen.getByLabelText("Select All");
|
||||
expect(selectAllCheckbox).toBeDisabled();
|
||||
const selectAllCheckbox = screen.getByRole("checkbox", { name: "Select All" });
|
||||
expectDisabledControl(selectAllCheckbox);
|
||||
|
||||
// Next button should be disabled
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
|
|
@ -379,7 +320,7 @@ describe("MakeModelPublicForm", () => {
|
|||
|
||||
// Select all should be indeterminate now
|
||||
const selectAllCheckbox = checkboxes[0];
|
||||
expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
|
||||
expect(selectAllCheckbox).toBePartiallyChecked();
|
||||
});
|
||||
|
||||
it("should display model badges and information", () => {
|
||||
|
|
@ -402,7 +343,6 @@ describe("MakeModelPublicForm", () => {
|
|||
|
||||
render(<MakeModelPublicForm {...mockProps} />);
|
||||
|
||||
// Navigate to confirm step
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
await act(async () => {
|
||||
fireEvent.click(nextButton);
|
||||
|
|
@ -412,7 +352,6 @@ describe("MakeModelPublicForm", () => {
|
|||
expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Submit
|
||||
const submitButton = screen.getByRole("button", { name: "Make Public" });
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
|
|
@ -428,7 +367,7 @@ describe("MakeModelPublicForm", () => {
|
|||
expect(mockProps.onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show loading state during submit", async () => {
|
||||
it("should not complete the flow until the submit request resolves", async () => {
|
||||
let resolvePromise: (value: any) => void = () => {};
|
||||
const pendingPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
|
|
@ -437,7 +376,6 @@ describe("MakeModelPublicForm", () => {
|
|||
|
||||
render(<MakeModelPublicForm {...mockProps} />);
|
||||
|
||||
// Navigate to confirm step
|
||||
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||
await act(async () => {
|
||||
fireEvent.click(nextButton);
|
||||
|
|
@ -447,17 +385,20 @@ describe("MakeModelPublicForm", () => {
|
|||
expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Submit
|
||||
const submitButton = screen.getByRole("button", { name: "Make Public" });
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
// Check loading state
|
||||
expect(submitButton).toHaveAttribute("data-loading", "true");
|
||||
expect(submitButton).toBeDisabled();
|
||||
expectDisabledControl(submitButton);
|
||||
await act(async () => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
expect(mockMakeModelGroupPublic).toHaveBeenCalledTimes(1);
|
||||
expect(mockProps.onSuccess).not.toHaveBeenCalled();
|
||||
expect(mockProps.onClose).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("Confirm Making Models Public")).toBeInTheDocument();
|
||||
|
||||
// Resolve the promise
|
||||
resolvePromise({});
|
||||
await waitFor(() => {
|
||||
expect(mockProps.onSuccess).toHaveBeenCalled();
|
||||
|
|
@ -474,7 +415,7 @@ describe("MakeModelPublicForm", () => {
|
|||
render(<MakeModelPublicForm {...invisibleProps} />);
|
||||
|
||||
// Modal should not be rendered
|
||||
expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Make Models Public")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -521,21 +462,19 @@ describe("MakeModelPublicForm", () => {
|
|||
|
||||
// Select all should be indeterminate
|
||||
const selectAllCheckbox = checkboxes[0];
|
||||
expect(selectAllCheckbox).toHaveAttribute("data-indeterminate", "true");
|
||||
expect(selectAllCheckbox).toBePartiallyChecked();
|
||||
});
|
||||
|
||||
it("should show selected count", () => {
|
||||
render(<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();
|
||||
expect(screen.getByText("model selected")).toHaveTextContent("1 model selected");
|
||||
});
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { Modal, Form, Steps, Button, Checkbox } from "antd";
|
||||
import { Text, Title, Badge } from "@tremor/react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/cva.config";
|
||||
import { makeModelGroupPublic } from "../../networking";
|
||||
import ModelFilters from "../../model_filters";
|
||||
import NotificationsManager from "../../molecules/notifications_manager";
|
||||
|
||||
const { Step } = Steps;
|
||||
const STEP_TITLES = ["Select Models", "Confirm"];
|
||||
|
||||
interface ModelGroupInfo {
|
||||
model_group: string;
|
||||
|
|
@ -44,13 +48,11 @@ const MakeModelPublicForm: React.FC<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();
|
||||
};
|
||||
|
||||
|
|
@ -138,23 +140,24 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title>Select Models to Make Public</Title>
|
||||
<h3 className="text-lg font-semibold">Select Models to Make Public</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={allModelsSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
disabled={filteredData.length === 0}
|
||||
>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
checked={allModelsSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onCheckedChange={(checked) => handleSelectAll(checked === true)}
|
||||
disabled={filteredData.length === 0}
|
||||
/>
|
||||
Select All {filteredData.length > 0 && `(${filteredData.length})`}
|
||||
</Checkbox>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Text className="text-sm text-gray-600">
|
||||
<p className="text-sm text-gray-600">
|
||||
Select the models you want to be visible on the public model hub. Users will still require a valid Virtual Key
|
||||
to use these models.
|
||||
</Text>
|
||||
</p>
|
||||
|
||||
{/* Filters */}
|
||||
<ModelFilters
|
||||
|
|
@ -168,7 +171,7 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
|
|||
<div className="space-y-3">
|
||||
{filteredData.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Text>No models match the current filters.</Text>
|
||||
<p>No models match the current filters.</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredData.map((model) => (
|
||||
|
|
@ -178,20 +181,16 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
|
|||
>
|
||||
<Checkbox
|
||||
checked={selectedModels.has(model.model_group)}
|
||||
onChange={(e) => handleModelSelection(model.model_group, e.target.checked)}
|
||||
onCheckedChange={(checked) => handleModelSelection(model.model_group, checked === true)}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium">{model.model_group}</Text>
|
||||
{model.mode && (
|
||||
<Badge color="green" size="sm">
|
||||
{model.mode}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="font-medium break-words">{model.model_group}</p>
|
||||
{model.mode && <Badge>{model.mode}</Badge>}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{model.providers.map((provider) => (
|
||||
<Badge key={provider} color="blue" size="xs">
|
||||
<Badge key={provider} variant="secondary">
|
||||
{provider}
|
||||
</Badge>
|
||||
))}
|
||||
|
|
@ -205,9 +204,9 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
|
|||
|
||||
{selectedModels.size > 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<Text className="text-sm text-blue-800">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>{selectedModels.size}</strong> model{selectedModels.size !== 1 ? "s" : ""} selected
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -217,29 +216,29 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
|
|||
const renderStep2Content = () => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Title>Confirm Making Models Public</Title>
|
||||
<h3 className="text-lg font-semibold">Confirm Making Models Public</h3>
|
||||
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<Text className="text-sm text-yellow-800">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Warning:</strong> Once you make these models public, anyone who can go to the{" "}
|
||||
<code>/ui/model_hub_table</code> will be able to know they exist on the proxy.
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Text className="font-medium">Models to be made public:</Text>
|
||||
<p className="font-medium">Models to be made public:</p>
|
||||
<div className="max-h-48 overflow-y-auto border rounded-lg p-3">
|
||||
<div className="space-y-2">
|
||||
{Array.from(selectedModels).map((modelGroup) => {
|
||||
const model = modelHubData.find((m) => m.model_group === modelGroup);
|
||||
return (
|
||||
<div key={modelGroup} className="flex items-center justify-between p-2 bg-gray-50 rounded-sm">
|
||||
<div>
|
||||
<Text className="font-medium">{modelGroup}</Text>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium break-words">{modelGroup}</p>
|
||||
{model && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{model.providers.map((provider) => (
|
||||
<Badge key={provider} color="blue" size="xs">
|
||||
<Badge key={provider} variant="secondary">
|
||||
{provider}
|
||||
</Badge>
|
||||
))}
|
||||
|
|
@ -254,10 +253,10 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<Text className="text-sm text-blue-800">
|
||||
<p className="text-sm text-blue-800">
|
||||
Total: <strong>{selectedModels.size}</strong> model{selectedModels.size !== 1 ? "s" : ""} will be made
|
||||
public
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -277,7 +276,7 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
|
|||
const renderStepButtons = () => {
|
||||
return (
|
||||
<div className="flex justify-between mt-6">
|
||||
<Button onClick={currentStep === 0 ? handleClose : handlePrevious}>
|
||||
<Button variant="outline" onClick={currentStep === 0 ? handleClose : handlePrevious}>
|
||||
{currentStep === 0 ? "Cancel" : "Previous"}
|
||||
</Button>
|
||||
|
||||
|
|
@ -289,7 +288,8 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
|
|||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<Button onClick={handleSubmit} loading={loading}>
|
||||
<Button onClick={handleSubmit} disabled={loading}>
|
||||
{loading && <Loader2 className="size-4 animate-spin" />}
|
||||
Make Public
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -299,24 +299,42 @@ const MakeModelPublicForm: React.FC<MakeModelPublicFormProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Make Models Public"
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={1200}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Steps current={currentStep} className="mb-6">
|
||||
<Step title="Select Models" />
|
||||
<Step title="Confirm" />
|
||||
</Steps>
|
||||
<Dialog open={visible} onOpenChange={(open) => !open && handleClose()} disablePointerDismissal>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[1200px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Make Models Public</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{renderStepContent()}
|
||||
{renderStepButtons()}
|
||||
</Form>
|
||||
</Modal>
|
||||
<div>
|
||||
<ol className="mb-6 flex items-center gap-6">
|
||||
{STEP_TITLES.map((title, index) => (
|
||||
<li
|
||||
key={title}
|
||||
className="flex items-center gap-2"
|
||||
aria-current={currentStep === index ? "step" : undefined}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-6 items-center justify-center rounded-full border text-xs",
|
||||
currentStep === index
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-border text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className={cn("text-sm", currentStep === index ? "font-medium" : "text-muted-foreground")}>
|
||||
{title}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{renderStepContent()}
|
||||
{renderStepButtons()}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue