mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
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:
parent
0acfcf3a36
commit
372fdedf5b
2 changed files with 271 additions and 178 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}'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 & 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}
|
||||
'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 & Authorize
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue