mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
fix: improve client connection handling and tool listing in mcpService (#30)
This commit is contained in:
@@ -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<HTMLDivElement>(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) =>
|
||||
<div className="flex items-center space-x-3">
|
||||
<h2 className={`text-xl font-semibold ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'}`}>{server.name}</h2>
|
||||
<Badge status={server.status} />
|
||||
|
||||
{server.error && (
|
||||
<div className="relative">
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={handleErrorIconClick}
|
||||
aria-label={t('server.viewErrorDetails')}
|
||||
>
|
||||
<AlertCircle className="text-red-500 hover:text-red-600" size={18} />
|
||||
</div>
|
||||
|
||||
{showErrorPopover && (
|
||||
<div
|
||||
ref={errorPopoverRef}
|
||||
className="absolute z-10 mt-2 bg-white border border-gray-200 rounded-md shadow-lg p-0 w-120"
|
||||
style={{
|
||||
left: '-231px',
|
||||
top: '24px',
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
width: '480px',
|
||||
transform: 'translateX(50%)'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-between items-center sticky top-0 bg-white py-2 px-4 border-b border-gray-200 z-20 shadow-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="text-sm font-medium text-red-600">{t('server.errorDetails')}</h4>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
title={t('common.copy')}
|
||||
>
|
||||
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowErrorPopover(false)
|
||||
}}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 pt-2">
|
||||
<pre className="text-sm text-gray-700 break-words whitespace-pre-wrap">{server.error}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
|
||||
@@ -68,7 +68,9 @@
|
||||
"namePlaceholder": "Enter server name",
|
||||
"urlPlaceholder": "Enter server URL",
|
||||
"commandPlaceholder": "Enter command",
|
||||
"argumentsPlaceholder": "Enter arguments"
|
||||
"argumentsPlaceholder": "Enter arguments",
|
||||
"errorDetails": "Error Details",
|
||||
"viewErrorDetails": "View error details"
|
||||
},
|
||||
"status": {
|
||||
"online": "Online",
|
||||
@@ -95,7 +97,9 @@
|
||||
"create": "Create",
|
||||
"submitting": "Submitting...",
|
||||
"delete": "Delete",
|
||||
"copy": "Copy"
|
||||
"copy": "Copy",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"copyFailed": "Copy failed"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
|
||||
@@ -68,7 +68,9 @@
|
||||
"namePlaceholder": "请输入服务器名称",
|
||||
"urlPlaceholder": "请输入服务器URL",
|
||||
"commandPlaceholder": "请输入命令",
|
||||
"argumentsPlaceholder": "请输入参数"
|
||||
"argumentsPlaceholder": "请输入参数",
|
||||
"errorDetails": "错误详情",
|
||||
"viewErrorDetails": "查看错误详情"
|
||||
},
|
||||
"status": {
|
||||
"online": "在线",
|
||||
@@ -95,7 +97,9 @@
|
||||
"create": "创建",
|
||||
"submitting": "提交中...",
|
||||
"delete": "删除",
|
||||
"copy": "复制"
|
||||
"copy": "复制",
|
||||
"copySuccess": "已复制到剪贴板",
|
||||
"copyFailed": "复制失败"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "仪表盘",
|
||||
|
||||
@@ -31,6 +31,7 @@ const ServersPage: React.FC = () => {
|
||||
|
||||
const handleEditComplete = () => {
|
||||
setEditingServer(null);
|
||||
triggerRefresh();
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface ServerConfig {
|
||||
export interface Server {
|
||||
name: string;
|
||||
status: ServerStatus;
|
||||
error?: string;
|
||||
tools?: Tool[];
|
||||
config?: ServerConfig;
|
||||
enabled?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user