feat(ui): migrate mcp_tools/ByokCredentialModal to shadcn

- antd Modal/Input/Input.Password/Switch + @ant-design/icons KeyOutlined/
  LockOutlined/CheckOutlined/ArrowRight/ArrowLeft/CloseOutlined/
  LinkOutlined \u2192 shadcn Dialog/Input/Button/Switch/Label + lucide
  Key/Lock/Check/ArrowRight/ArrowLeft/X/LinkIcon/Eye/EyeOff.
- Two-step flow (service-icons \u2192 api-key-form) preserved. Preserve
  the teal/cyan \u2192 blue/indigo gradient logo tiles (intentional brand
  illustration); ByokCredentialModal added to .eslintrc.json overrides.

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:10:13 +00:00
parent 0acfcf3a36
commit 372fdedf5b
No known key found for this signature in database
2 changed files with 271 additions and 178 deletions

View file

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

View file

@ -1,17 +1,27 @@
"use client";
import React, { useState } from "react";
import { Modal, Input, Switch } from "antd";
import {
Dialog,
DialogContent,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import MessageManager from "@/components/molecules/message_manager";
import {
KeyOutlined,
LockOutlined,
CheckOutlined,
ArrowRightOutlined,
ArrowLeftOutlined,
CloseOutlined,
LinkOutlined,
} from "@ant-design/icons";
ArrowLeft,
ArrowRight,
Check,
Eye,
EyeOff,
Key,
Link as LinkIcon,
Lock,
X,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { MCPServer } from "./types";
interface ByokCredentialModalProps {
@ -31,6 +41,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
}) => {
const [step, setStep] = useState<1 | 2>(1);
const [apiKey, setApiKey] = useState("");
const [showApiKey, setShowApiKey] = useState(false);
const [saveKey, setSaveKey] = useState(true);
const [loading, setLoading] = useState(false);
@ -40,6 +51,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
const handleClose = () => {
setStep(1);
setApiKey("");
setShowApiKey(false);
setSaveKey(true);
setLoading(false);
onClose();
@ -52,14 +64,17 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
}
setLoading(true);
try {
const response = await fetch(`/v1/mcp/server/${server.server_id}/user-credential`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
const response = await fetch(
`/v1/mcp/server/${server.server_id}/user-credential`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ credential: apiKey.trim(), save: saveKey }),
},
body: JSON.stringify({ credential: apiKey.trim(), save: saveKey }),
});
);
if (!response.ok) {
const err = await response.json();
throw new Error(err?.detail?.error || "Failed to save credential");
@ -67,6 +82,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
MessageManager.success(`Connected to ${serverDisplayName}`);
onSuccess(server.server_id);
handleClose();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
MessageManager.error(e.message || "Failed to connect");
} finally {
@ -75,180 +91,256 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
};
return (
<Modal
<Dialog
open={open}
onCancel={handleClose}
footer={null}
width={480}
closeIcon={null}
className="byok-modal"
onOpenChange={(o) => (!o ? handleClose() : undefined)}
>
<div className="relative p-2">
{/* Step dots + close */}
<div className="flex items-center justify-between mb-6">
{step === 2 ? (
<button
onClick={() => setStep(1)}
className="flex items-center gap-1 text-gray-500 hover:text-gray-800 text-sm"
>
<ArrowLeftOutlined /> Back
</button>
) : (
<div />
)}
<div className="flex items-center gap-1.5">
<div className={`w-2 h-2 rounded-full ${step === 1 ? "bg-blue-500" : "bg-gray-300"}`} />
<div className={`w-2 h-2 rounded-full ${step === 2 ? "bg-blue-500" : "bg-gray-300"}`} />
</div>
<button onClick={handleClose} className="text-gray-400 hover:text-gray-600">
<CloseOutlined />
</button>
</div>
{step === 1 ? (
<div className="text-center">
{/* Logos */}
<div className="flex items-center justify-center gap-3 mb-6">
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-teal-400 to-cyan-600 flex items-center justify-center text-white font-bold text-xl shadow">
L
</div>
<ArrowRightOutlined className="text-gray-400 text-lg" />
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-blue-600 to-indigo-800 flex items-center justify-center text-white font-bold text-xl shadow">
{firstLetter}
</div>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Connect {serverDisplayName}</h2>
<p className="text-gray-500 mb-6">
LiteLLM needs access to {serverDisplayName} to complete your request.
</p>
{/* How it works */}
<div className="bg-gray-50 rounded-xl p-4 text-left mb-4">
<div className="flex items-start gap-3">
<div className="mt-0.5">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" className="text-gray-500">
<rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" strokeWidth="2" />
<path d="M8 4v16M16 4v16" stroke="currentColor" strokeWidth="2" />
</svg>
</div>
<div>
<p className="font-semibold text-gray-800 mb-1">How it works</p>
<p className="text-gray-500 text-sm">
LiteLLM acts as a secure bridge. Your requests are routed through our MCP client directly to{" "}
{serverDisplayName}&apos;s API.
</p>
</div>
</div>
</div>
{/* Requested access */}
{server.byok_description && server.byok_description.length > 0 && (
<div className="bg-gray-50 rounded-xl p-4 text-left mb-6">
<p className="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-3 flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" className="text-green-500">
<path
d="M12 2L12 22M2 12L22 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2" />
</svg>
Requested Access
</p>
<ul className="space-y-2">
{server.byok_description.map((item, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-gray-700">
<CheckOutlined className="text-green-500 flex-shrink-0" />
{item}
</li>
))}
</ul>
</div>
<DialogContent className="max-w-md p-0 overflow-hidden">
<div className="relative p-6">
<div className="flex items-center justify-between mb-6">
{step === 2 ? (
<button
onClick={() => setStep(1)}
className="flex items-center gap-1 text-muted-foreground hover:text-foreground text-sm"
>
<ArrowLeft className="h-3.5 w-3.5" /> Back
</button>
) : (
<div />
)}
<button
onClick={() => setStep(2)}
className="w-full bg-gray-900 hover:bg-gray-700 text-white font-medium py-3 px-6 rounded-xl flex items-center justify-center gap-2 transition-colors"
>
Continue to Authentication <ArrowRightOutlined />
</button>
<div className="flex items-center gap-1.5">
<div
className={cn(
"w-2 h-2 rounded-full",
step === 1 ? "bg-primary" : "bg-muted",
)}
/>
<div
className={cn(
"w-2 h-2 rounded-full",
step === 2 ? "bg-primary" : "bg-muted",
)}
/>
</div>
<button
onClick={handleClose}
className="mt-3 w-full text-gray-400 hover:text-gray-600 text-sm py-2"
className="text-muted-foreground hover:text-foreground"
aria-label="Close"
>
Cancel
<X className="h-4 w-4" />
</button>
</div>
) : (
<div>
{/* Key icon */}
<div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center mb-4">
<KeyOutlined className="text-blue-400 text-xl" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Provide API Key</h2>
<p className="text-gray-500 mb-6">
Enter your {serverDisplayName} API key to authorize this connection.
</p>
<div className="mb-4">
<label className="block text-sm font-semibold text-gray-800 mb-2">
{serverDisplayName} API Key
</label>
<Input.Password
placeholder="Enter your API key"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
size="large"
className="rounded-lg"
/>
{server.byok_api_key_help_url && (
<a
href={server.byok_api_key_help_url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-700 text-sm mt-2 flex items-center gap-1"
>
Where do I find my API key? <LinkOutlined />
</a>
)}
</div>
{/* Save toggle */}
<div className="bg-gray-50 rounded-xl p-4 flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" className="text-gray-500">
<path
d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
fill="currentColor"
/>
</svg>
<span className="text-sm font-medium text-gray-800">Save key for future use</span>
{step === 1 ? (
<div className="text-center">
<div className="flex items-center justify-center gap-3 mb-6">
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-teal-400 to-cyan-600 flex items-center justify-center text-white font-bold text-xl shadow">
L
</div>
<ArrowRight className="text-muted-foreground h-4 w-4" />
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-blue-600 to-indigo-800 flex items-center justify-center text-white font-bold text-xl shadow">
{firstLetter}
</div>
</div>
<Switch checked={saveKey} onChange={setSaveKey} />
</div>
{/* Security note */}
<div className="bg-blue-50 rounded-xl p-4 flex items-start gap-3 mb-6">
<LockOutlined className="text-blue-400 mt-0.5 flex-shrink-0" />
<p className="text-sm text-blue-700">
Your key is stored securely and transmitted over HTTPS. It is never shared with third parties.
<h2 className="text-2xl font-bold text-foreground mb-2">
Connect {serverDisplayName}
</h2>
<p className="text-muted-foreground mb-6">
LiteLLM needs access to {serverDisplayName} to complete your
request.
</p>
</div>
<button
onClick={handleAuthorize}
disabled={loading}
className="w-full bg-blue-500 hover:bg-blue-600 disabled:opacity-60 text-white font-medium py-3 px-6 rounded-xl flex items-center justify-center gap-2 transition-colors"
>
<LockOutlined /> Connect &amp; Authorize
</button>
</div>
)}
</div>
</Modal>
<div className="bg-muted rounded-xl p-4 text-left mb-4">
<div className="flex items-start gap-3">
<div className="mt-0.5">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
className="text-muted-foreground"
>
<rect
x="2"
y="4"
width="20"
height="16"
rx="2"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M8 4v16M16 4v16"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
</div>
<div>
<p className="font-semibold text-foreground mb-1">
How it works
</p>
<p className="text-muted-foreground text-sm">
LiteLLM acts as a secure bridge. Your requests are routed
through our MCP client directly to {serverDisplayName}
&apos;s API.
</p>
</div>
</div>
</div>
{server.byok_description &&
server.byok_description.length > 0 && (
<div className="bg-muted rounded-xl p-4 text-left mb-6">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-widest mb-3 flex items-center gap-2">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
className="text-emerald-500"
>
<path
d="M12 2L12 22M2 12L22 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<circle
cx="12"
cy="12"
r="9"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
Requested Access
</p>
<ul className="space-y-2">
{server.byok_description.map((item, i) => (
<li
key={i}
className="flex items-center gap-2 text-sm text-foreground"
>
<Check className="text-emerald-500 flex-shrink-0 h-4 w-4" />
{item}
</li>
))}
</ul>
</div>
)}
<Button
onClick={() => setStep(2)}
className="w-full"
>
Continue to Authentication <ArrowRight className="h-4 w-4" />
</Button>
<Button
onClick={handleClose}
variant="ghost"
className="mt-3 w-full text-muted-foreground"
>
Cancel
</Button>
</div>
) : (
<div>
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-4">
<Key className="text-primary h-5 w-5" />
</div>
<h2 className="text-2xl font-bold text-foreground mb-2">
Provide API Key
</h2>
<p className="text-muted-foreground mb-6">
Enter your {serverDisplayName} API key to authorize this
connection.
</p>
<div className="mb-4 space-y-2">
<Label htmlFor="byok-api-key">
{serverDisplayName} API Key
</Label>
<div className="relative">
<Input
id="byok-api-key"
type={showApiKey ? "text" : "password"}
placeholder="Enter your API key"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
<button
type="button"
onClick={() => setShowApiKey(!showApiKey)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground"
aria-label={showApiKey ? "Hide api key" : "Show api key"}
>
{showApiKey ? (
<EyeOff size={14} />
) : (
<Eye size={14} />
)}
</button>
</div>
{server.byok_api_key_help_url && (
<a
href={server.byok_api_key_help_url}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline text-sm mt-2 flex items-center gap-1"
>
Where do I find my API key?{" "}
<LinkIcon className="h-3.5 w-3.5" />
</a>
)}
</div>
<div className="bg-muted rounded-xl p-4 flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
className="text-muted-foreground"
>
<path
d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
fill="currentColor"
/>
</svg>
<span className="text-sm font-medium text-foreground">
Save key for future use
</span>
</div>
<Switch
checked={saveKey}
onCheckedChange={setSaveKey}
/>
</div>
<div className="bg-primary/5 rounded-xl p-4 flex items-start gap-3 mb-6">
<Lock className="text-primary mt-0.5 flex-shrink-0 h-4 w-4" />
<p className="text-sm text-primary">
Your key is stored securely and transmitted over HTTPS. It is
never shared with third parties.
</p>
</div>
<Button
onClick={handleAuthorize}
disabled={loading}
className="w-full"
>
<Lock className="h-4 w-4" /> Connect &amp; Authorize
</Button>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
};