diff --git a/frontend/src/components/ServerCard.tsx b/frontend/src/components/ServerCard.tsx index 226cbca..2016f98 100644 --- a/frontend/src/components/ServerCard.tsx +++ b/frontend/src/components/ServerCard.tsx @@ -1,10 +1,11 @@ -import { useState } from 'react' +import { useState, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Server } from '@/types' -import { ChevronDown, ChevronRight } from '@/components/icons/LucideIcons' +import { ChevronDown, ChevronRight, AlertCircle, Copy, Check } from 'lucide-react' import Badge from '@/components/ui/Badge' import ToolCard from '@/components/ui/ToolCard' import DeleteDialog from '@/components/ui/DeleteDialog' +import { useToast } from '@/contexts/ToastContext' interface ServerCardProps { server: Server @@ -15,9 +16,26 @@ interface ServerCardProps { const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => { const { t } = useTranslation() + const { showToast } = useToast() const [isExpanded, setIsExpanded] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [isToggling, setIsToggling] = useState(false) + const [showErrorPopover, setShowErrorPopover] = useState(false) + const [copied, setCopied] = useState(false) + const errorPopoverRef = useRef(null) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (errorPopoverRef.current && !errorPopoverRef.current.contains(event.target as Node)) { + setShowErrorPopover(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) const handleRemove = (e: React.MouseEvent) => { e.stopPropagation() @@ -41,6 +59,44 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => } } + const handleErrorIconClick = (e: React.MouseEvent) => { + e.stopPropagation() + setShowErrorPopover(!showErrorPopover) + } + + const copyToClipboard = (e: React.MouseEvent) => { + e.stopPropagation() + if (!server.error) return + + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(server.error).then(() => { + setCopied(true) + showToast(t('common.copySuccess') || 'Copied to clipboard', 'success') + setTimeout(() => setCopied(false), 2000) + }) + } else { + // Fallback for HTTP or unsupported clipboard API + const textArea = document.createElement('textarea') + textArea.value = server.error + // Avoid scrolling to bottom + textArea.style.position = 'fixed' + textArea.style.left = '-9999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + try { + document.execCommand('copy') + setCopied(true) + showToast(t('common.copySuccess') || 'Copied to clipboard', 'success') + setTimeout(() => setCopied(false), 2000) + } catch (err) { + showToast(t('common.copyFailed') || 'Copy failed', 'error') + console.error('Copy to clipboard failed:', err) + } + document.body.removeChild(textArea) + } + } + const handleConfirmDelete = () => { onRemove(server.name) setShowDeleteDialog(false) @@ -56,6 +112,59 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>

{server.name}

+ + {server.error && ( +
+
+ +
+ + {showErrorPopover && ( +
e.stopPropagation()} + > +
+
+

{t('server.errorDetails')}

+ +
+ +
+
+
{server.error}
+
+
+ )} +
+ )}