| import React, { useState } from 'react'; |
| import { X, Plus, RefreshCw } from 'lucide-react'; |
| const TRANSPORTS = ['SSE', 'STREAMABLEHTTP', 'HTTP', 'STDIO']; |
| export function AddServerDrawer({ backendUrl, apiKey, onClose, onRegistered }) { |
| const [name, setName] = useState(''); |
| const [url, setUrl] = useState(''); |
| const [transport, setTransport] = useState('SSE'); |
| const [description, setDescription] = useState(''); |
| const [autoRefresh, setAutoRefresh] = useState(true); |
| const [busy, setBusy] = useState(false); |
| const [error, setError] = useState(null); |
| const [success, setSuccess] = useState(false); |
| const canSubmit = name.trim() && url.trim() && !busy; |
| const handleSubmit = async () => { |
| if (!canSubmit) |
| return; |
| setBusy(true); |
| setError(null); |
| try { |
| const headers = { 'Content-Type': 'application/json' }; |
| if (apiKey) |
| headers['x-api-key'] = apiKey; |
| const res = await fetch(`${backendUrl}/v1/agentic/register/gateway`, { |
| method: 'POST', |
| headers, |
| body: JSON.stringify({ |
| name: name.trim(), |
| url: url.trim(), |
| transport, |
| description: description.trim(), |
| auto_refresh: autoRefresh, |
| }), |
| }); |
| const data = await res.json(); |
| if (!res.ok || !data.ok) { |
| throw new Error(data.detail || `HTTP ${res.status}`); |
| } |
| setSuccess(true); |
| setTimeout(() => { |
| onRegistered(); |
| onClose(); |
| }, 1000); |
| } |
| catch (e) { |
| setError(e?.message || 'Registration failed'); |
| } |
| finally { |
| setBusy(false); |
| } |
| }; |
| return (<div className="fixed inset-0 z-50 flex justify-end" onClick={onClose}> |
| <div className="absolute inset-0 bg-black/60 backdrop-blur-sm"/> |
| |
| <div className="relative w-full max-w-md bg-[#0b0b12] border-l border-white/10 h-full overflow-y-auto animate-slide-in-right" onClick={(e) => e.stopPropagation()}> |
| {/* Header */} |
| <div className="sticky top-0 bg-[#0b0b12]/95 backdrop-blur border-b border-white/10 px-6 py-4 flex items-center justify-between z-10"> |
| <h2 className="text-base font-semibold text-white">Add MCP Server</h2> |
| <button onClick={onClose} className="p-2 text-white/50 hover:text-white hover:bg-white/10 rounded-xl transition-colors"> |
| <X size={18}/> |
| </button> |
| </div> |
| |
| {/* Form */} |
| <div className="px-6 py-6 space-y-5"> |
| {/* Name */} |
| <div> |
| <label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block"> |
| Server Name * |
| </label> |
| <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. my-mcp-server" className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-purple-500/50 transition-colors"/> |
| </div> |
| |
| {/* URL */} |
| <div> |
| <label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block"> |
| Endpoint URL * |
| </label> |
| <input type="url" value={url} onChange={(e) => setUrl(e.target.value)} placeholder="e.g. http://localhost:9101/rpc" className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-purple-500/50 transition-colors"/> |
| </div> |
| |
| {/* Transport */} |
| <div> |
| <label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block"> |
| Transport |
| </label> |
| <div className="flex gap-2"> |
| {TRANSPORTS.map((t) => (<button key={t} onClick={() => setTransport(t)} className={`px-3 py-1.5 text-xs font-medium rounded-lg border transition-colors ${transport === t |
| ? 'bg-purple-500/20 border-purple-500/50 text-purple-300' |
| : 'bg-white/5 border-white/10 text-white/40 hover:text-white/60'}`}> |
| {t} |
| </button>))} |
| </div> |
| </div> |
| |
| {/* Description */} |
| <div> |
| <label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block"> |
| Description |
| </label> |
| <textarea value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Optional description..." rows={3} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-purple-500/50 transition-colors resize-none"/> |
| </div> |
| |
| {/* Auto-refresh toggle */} |
| <label className="flex items-center gap-3 cursor-pointer"> |
| <div onClick={() => setAutoRefresh(!autoRefresh)} className={`w-10 h-5 rounded-full transition-colors relative ${autoRefresh ? 'bg-purple-500' : 'bg-white/10'}`}> |
| <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${autoRefresh ? 'left-5' : 'left-0.5'}`}/> |
| </div> |
| <span className="text-sm text-white/70">Auto-discover tools after registration</span> |
| </label> |
| |
| {/* Error / Success */} |
| {error && (<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-xl px-4 py-3"> |
| {error} |
| </div>)} |
| {success && (<div className="text-sm text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 rounded-xl px-4 py-3"> |
| Server registered successfully! |
| </div>)} |
| |
| {/* Submit */} |
| <button onClick={handleSubmit} disabled={!canSubmit} className="w-full flex items-center justify-center gap-2 bg-purple-500 hover:bg-purple-600 disabled:bg-white/10 disabled:text-white/30 px-4 py-3 rounded-xl text-sm font-semibold transition-all"> |
| {busy ? (<RefreshCw size={16} className="animate-spin"/>) : (<Plus size={16}/>)} |
| {busy ? 'Registering...' : 'Register Gateway'} |
| </button> |
| </div> |
| </div> |
| |
| <style>{` |
| @keyframes slide-in-right { |
| from { transform: translateX(100%); } |
| to { transform: translateX(0); } |
| } |
| .animate-slide-in-right { |
| animation: slide-in-right 0.2s ease-out; |
| } |
| `}</style> |
| </div>); |
| } |
|
|