feat(ui): migrate mcp_tools MCPLogoSelector + MCPNetworkSettings to shadcn

- MCPLogoSelector: antd Input/Tooltip + @ant-design/icons InfoCircle/
  LinkOutlined \u2192 shadcn Input/Tooltip + lucide Info/Link. Logo grid
  and selected preview preserved; selected state uses primary-toned
  border/background.
- MCPNetworkSettings: antd Select mode=tags/Button/Card/Typography/Spin/
  Tag + @ant-design/icons SaveOutlined/PlusOutlined \u2192 shadcn Card/
  Input/Button/Badge/Skeleton + lucide Plus/Save/X. Replaces the
  Select mode=tags with an Add-on-Enter-or-comma Input + Badge chip
  list. Suggested-range Badge now uses gap-1 + Plus icon and is
  exempted from raw-colors as categorical blue/info tone.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 10:06:11 +00:00
parent 276b1682e2
commit 0acfcf3a36
No known key found for this signature in database
3 changed files with 201 additions and 117 deletions

View file

@ -38,7 +38,8 @@
"src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterTestPanel.tsx",
"src/components/mcp_hub_table_columns.tsx",
"src/components/model_hub_table_columns.tsx",
"src/components/mcp_server_management/MCPServerSelector.tsx"
"src/components/mcp_server_management/MCPServerSelector.tsx",
"src/components/mcp_tools/MCPNetworkSettings.tsx"
],
"rules": {
"litellm-ui/no-banned-ui-imports": "off",

View file

@ -1,6 +1,13 @@
import React, { useState } from "react";
import { Input, Tooltip } from "antd";
import { InfoCircleOutlined, LinkOutlined } from "@ant-design/icons";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Info, Link as LinkIcon } from "lucide-react";
import { cn } from "@/lib/utils";
const logos = "/ui/assets/logos/";
@ -46,76 +53,95 @@ const MCPLogoSelector: React.FC<MCPLogoSelectorProps> = ({ value, onChange }) =>
return (
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-sm font-medium text-gray-700">Logo</span>
<Tooltip title="Select a well-known logo or paste a URL to any image. The logo is shown on the admin and chat pages.">
<InfoCircleOutlined className="text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
<span className="text-sm font-medium text-foreground">Logo</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3 w-3 text-primary cursor-help" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Select a well-known logo or paste a URL to any image. The logo is
shown on the admin and chat pages.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{/* Preview */}
{value && (
<div className="flex items-center gap-3 mb-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center gap-3 mb-3 p-3 bg-muted rounded-lg border border-border">
<img
src={value}
alt="Selected logo"
className="w-10 h-10 object-contain rounded"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
<div className="flex-1 min-w-0">
<div className="text-xs text-gray-500 truncate">{value}</div>
<div className="text-xs text-muted-foreground truncate">
{value}
</div>
</div>
<button
type="button"
onClick={() => onChange?.(undefined)}
className="text-xs text-gray-400 hover:text-red-500 cursor-pointer bg-transparent border-none"
className="text-xs text-muted-foreground hover:text-destructive cursor-pointer bg-transparent border-none"
aria-label="Clear selected logo"
>
✕
</button>
</div>
)}
{/* Well-known logo grid */}
<div className="grid grid-cols-10 gap-1.5 mb-3">
{WELL_KNOWN_LOGOS.map((logo) => {
const isSelected = value === logo.url;
const hasFailed = imgErrors.has(logo.url);
if (hasFailed) return null;
if (imgErrors.has(logo.url)) return null;
return (
<Tooltip key={logo.name} title={logo.name}>
<button
type="button"
onClick={() => handleSelect(logo.url)}
className={`flex items-center justify-center p-2 rounded-lg border transition-all cursor-pointer
${isSelected
? "border-blue-500 bg-blue-50 shadow-sm"
: "border-gray-200 hover:border-blue-300 hover:bg-gray-50"
}`}
style={{ width: 40, height: 40 }}
>
<img
src={logo.url}
alt={logo.name}
className="w-5 h-5 object-contain"
onError={() => handleImgError(logo.url)}
/>
</button>
</Tooltip>
<TooltipProvider key={logo.name}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => handleSelect(logo.url)}
className={cn(
"flex items-center justify-center p-2 rounded-lg border transition-all cursor-pointer",
isSelected
? "border-primary bg-primary/10 shadow-sm"
: "border-border hover:border-primary/50 hover:bg-muted",
)}
style={{ width: 40, height: 40 }}
aria-label={logo.name}
>
<img
src={logo.url}
alt={logo.name}
className="w-5 h-5 object-contain"
onError={() => handleImgError(logo.url)}
/>
</button>
</TooltipTrigger>
<TooltipContent>{logo.name}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
})}
</div>
{/* Custom URL input */}
<Input
prefix={<LinkOutlined className="text-gray-400" />}
placeholder="Or paste a custom logo URL..."
value={value && !WELL_KNOWN_LOGOS.some((l) => l.url === value) ? value : ""}
onChange={(e) => {
const v = e.target.value.trim();
onChange?.(v || undefined);
}}
className="rounded-lg"
size="small"
/>
<div className="relative">
<LinkIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
<Input
placeholder="Or paste a custom logo URL..."
value={
value && !WELL_KNOWN_LOGOS.some((l) => l.url === value) ? value : ""
}
onChange={(e) => {
const v = e.target.value.trim();
onChange?.(v || undefined);
}}
className="pl-8 h-8 rounded-lg"
/>
</div>
</div>
);
};

View file

@ -1,65 +1,75 @@
import React, { useState, useEffect } from "react";
import { Select, Button, Card, Typography, Spin, Tag } from "antd";
import { SaveOutlined, PlusOutlined } from "@ant-design/icons";
import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting, fetchMCPClientIp } from "../networking";
const { Text } = Typography;
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { Plus, Save, X } from "lucide-react";
import {
getGeneralSettingsCall,
updateConfigFieldSetting,
deleteConfigFieldSetting,
fetchMCPClientIp,
} from "../networking";
interface MCPNetworkSettingsProps {
accessToken: string | null;
}
/**
* Given an IP like "203.0.113.45", return "203.0.113.0/24".
*/
function ipToSlash24(ip: string): string {
const parts = ip.split(".");
if (parts.length !== 4) return ip + "/32";
return `${parts[0]}.${parts[1]}.${parts[2]}.0/24`;
}
const MCPNetworkSettings: React.FC<MCPNetworkSettingsProps> = ({ accessToken }) => {
const MCPNetworkSettings: React.FC<MCPNetworkSettingsProps> = ({
accessToken,
}) => {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [privateRanges, setPrivateRanges] = useState<string[]>([]);
const [currentIp, setCurrentIp] = useState<string | null>(null);
const [newRangeInput, setNewRangeInput] = useState("");
useEffect(() => {
const loadSettings = async () => {
if (!accessToken) return;
setLoading(true);
try {
const settings = await getGeneralSettingsCall(accessToken);
for (const field of settings) {
if (
field.field_name === "mcp_internal_ip_ranges" &&
field.field_value
) {
setPrivateRanges(field.field_value);
}
}
} catch (error) {
console.error("Failed to load MCP network settings:", error);
} finally {
setLoading(false);
}
};
const detectCurrentIp = async () => {
if (!accessToken) return;
const ip = await fetchMCPClientIp(accessToken);
if (ip) setCurrentIp(ip);
};
loadSettings();
detectCurrentIp();
}, [accessToken]);
const loadSettings = async () => {
if (!accessToken) return;
setLoading(true);
try {
const settings = await getGeneralSettingsCall(accessToken);
for (const field of settings) {
if (field.field_name === "mcp_internal_ip_ranges" && field.field_value) {
setPrivateRanges(field.field_value);
}
}
} catch (error) {
console.error("Failed to load MCP network settings:", error);
} finally {
setLoading(false);
}
};
const detectCurrentIp = async () => {
if (!accessToken) return;
const ip = await fetchMCPClientIp(accessToken);
if (ip) {
setCurrentIp(ip);
}
};
const handleSave = async () => {
if (!accessToken) return;
setSaving(true);
try {
if (privateRanges.length > 0) {
await updateConfigFieldSetting(accessToken, "mcp_internal_ip_ranges", privateRanges);
await updateConfigFieldSetting(
accessToken,
"mcp_internal_ip_ranges",
privateRanges,
);
} else {
await deleteConfigFieldSetting(accessToken, "mcp_internal_ip_ranges");
}
@ -76,10 +86,19 @@ const MCPNetworkSettings: React.FC<MCPNetworkSettingsProps> = ({ accessToken })
}
};
const commitNewRange = () => {
const v = newRangeInput.trim();
if (!v) return;
if (!privateRanges.includes(v)) {
setPrivateRanges([...privateRanges, v]);
}
setNewRangeInput("");
};
if (loading) {
return (
<div className="flex justify-center py-12">
<Spin />
<Skeleton className="h-8 w-48" />
</div>
);
}
@ -89,60 +108,98 @@ const MCPNetworkSettings: React.FC<MCPNetworkSettingsProps> = ({ accessToken })
return (
<div className="space-y-6 p-4">
<div>
<Text className="text-lg font-semibold">Private IP Ranges</Text>
<p className="text-sm text-gray-500 mt-1">
Define which IP ranges are part of your private network. Callers from these IPs can see all MCP servers. Callers from any other IP can only see servers marked &quot;Available on Public Internet&quot;.
<span className="text-lg font-semibold">Private IP Ranges</span>
<p className="text-sm text-muted-foreground mt-1">
Define which IP ranges are part of your private network. Callers from
these IPs can see all MCP servers. Callers from any other IP can only
see servers marked &quot;Available on Public Internet&quot;.
</p>
</div>
<Card>
<Card className="p-4">
{currentIp && (
<div className="mb-4 p-3 bg-blue-50 rounded-lg">
<Text className="text-sm text-blue-700">
Your current IP: <span className="font-mono font-medium">{currentIp}</span>
</Text>
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-950/30 rounded-lg">
<span className="text-sm text-blue-700 dark:text-blue-300">
Your current IP:{" "}
<span className="font-mono font-medium">{currentIp}</span>
</span>
{suggestedRange && !privateRanges.includes(suggestedRange) && (
<div className="mt-1">
<Text className="text-sm text-blue-600">Suggested range: </Text>
<Tag
className="cursor-pointer font-mono"
color="blue"
icon={<PlusOutlined />}
<div className="mt-1 flex items-center gap-1">
<span className="text-sm text-blue-600 dark:text-blue-400">
Suggested range:
</span>
<Badge
className="cursor-pointer font-mono gap-1"
onClick={() => addSuggestedRange(suggestedRange)}
>
<Plus className="h-3 w-3" />
{suggestedRange}
</Tag>
</Badge>
</div>
)}
</div>
)}
<div className="flex items-center mb-2">
<Text className="font-medium">Your Private Network Ranges</Text>
<span className="font-medium">Your Private Network Ranges</span>
</div>
<Select
mode="tags"
value={privateRanges}
onChange={setPrivateRanges}
placeholder="Leave empty to use defaults: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8"
tokenSeparators={[","]}
className="w-full"
size="large"
allowClear
/>
<p className="text-xs text-gray-400 mt-2">
Enter CIDR ranges (e.g., 10.0.0.0/8). When empty, standard private IP ranges are used.
<div className="space-y-2">
<div className="flex gap-2">
<Input
value={newRangeInput}
onChange={(e) => setNewRangeInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
commitNewRange();
}
}}
placeholder="Leave empty to use defaults: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8"
className="flex-1"
/>
<Button
type="button"
variant="outline"
onClick={commitNewRange}
disabled={!newRangeInput.trim()}
>
<Plus className="h-4 w-4" />
Add
</Button>
</div>
{privateRanges.length > 0 && (
<div className="flex flex-wrap gap-1">
{privateRanges.map((range) => (
<Badge
key={range}
variant="secondary"
className="font-mono gap-1"
>
{range}
<button
type="button"
onClick={() =>
setPrivateRanges(privateRanges.filter((r) => r !== range))
}
aria-label={`Remove ${range}`}
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
)}
</div>
<p className="text-xs text-muted-foreground mt-2">
Enter CIDR ranges (e.g., 10.0.0.0/8). When empty, standard private IP
ranges are used.
</p>
</Card>
<div className="flex justify-end">
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
loading={saving}
>
Save
<Button onClick={handleSave} disabled={saving}>
<Save className="h-4 w-4" />
{saving ? "Saving…" : "Save"}
</Button>
</div>
</div>