mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: enhance error handling and messages
This commit is contained in:
@@ -1,19 +1,99 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server, ApiResponse } from './types'
|
||||
import ServerCard from './components/ServerCard'
|
||||
import AddServerForm from './components/AddServerForm'
|
||||
import EditServerForm from './components/EditServerForm'
|
||||
|
||||
// 配置选项
|
||||
const CONFIG = {
|
||||
// 初始化启动阶段的配置
|
||||
startup: {
|
||||
maxAttempts: 60, // 初始化阶段最大尝试次数
|
||||
pollingInterval: 3000 // 初始阶段轮询间隔 (3秒)
|
||||
},
|
||||
// 正常运行阶段的配置
|
||||
normal: {
|
||||
pollingInterval: 10000 // 正常运行时的轮询间隔 (10秒)
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { t } = useTranslation()
|
||||
const [servers, setServers] = useState<Server[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [refreshKey, setRefreshKey] = useState(0)
|
||||
const [editingServer, setEditingServer] = useState<Server | null>(null)
|
||||
const [isInitialLoading, setIsInitialLoading] = useState(true)
|
||||
const [fetchAttempts, setFetchAttempts] = useState(0)
|
||||
|
||||
// 轮询定时器引用
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
// 保存当前尝试次数,避免依赖循环
|
||||
const attemptsRef = useRef<number>(0)
|
||||
|
||||
// 清理定时器
|
||||
const clearTimer = () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
// 开始正常轮询
|
||||
const startNormalPolling = useCallback(() => {
|
||||
// 确保没有其他定时器在运行
|
||||
clearTimer()
|
||||
|
||||
const fetchServers = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/servers')
|
||||
const data = await response.json()
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setServers(data.data)
|
||||
} else if (data && Array.isArray(data)) {
|
||||
setServers(data)
|
||||
} else {
|
||||
console.error('Invalid server data format:', data)
|
||||
setServers([])
|
||||
}
|
||||
|
||||
// 重置错误状态
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
console.error('Error fetching servers during normal polling:', err)
|
||||
|
||||
// 使用友好的错误消息
|
||||
if (!navigator.onLine) {
|
||||
setError(t('errors.network'))
|
||||
} else if (err instanceof TypeError && (
|
||||
err.message.includes('NetworkError') ||
|
||||
err.message.includes('Failed to fetch')
|
||||
)) {
|
||||
setError(t('errors.serverConnection'))
|
||||
} else {
|
||||
setError(t('errors.serverFetch'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 立即执行一次
|
||||
fetchServers()
|
||||
|
||||
// 设置定期轮询
|
||||
intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval)
|
||||
}, [t])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchServers = async () => {
|
||||
// 重置尝试计数
|
||||
if (refreshKey > 0) {
|
||||
attemptsRef.current = 0;
|
||||
setFetchAttempts(0);
|
||||
}
|
||||
|
||||
// 初始化加载阶段的请求函数
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/servers')
|
||||
const data = await response.json()
|
||||
@@ -21,25 +101,92 @@ function App() {
|
||||
// 处理API响应中的包装对象,提取data字段
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setServers(data.data)
|
||||
setIsInitialLoading(false)
|
||||
// 初始化成功,开始正常轮询
|
||||
startNormalPolling()
|
||||
return true
|
||||
} else if (data && Array.isArray(data)) {
|
||||
// 兼容性处理,如果API直接返回数组
|
||||
setServers(data)
|
||||
setIsInitialLoading(false)
|
||||
// 初始化成功,开始正常轮询
|
||||
startNormalPolling()
|
||||
return true
|
||||
} else {
|
||||
// 如果数据格式不符合预期,设置为空数组
|
||||
console.error('Invalid server data format:', data)
|
||||
setServers([])
|
||||
setIsInitialLoading(false)
|
||||
// 初始化成功但数据为空,开始正常轮询
|
||||
startNormalPolling()
|
||||
return true
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
// 增加尝试次数计数,使用 ref 避免触发 effect 重新运行
|
||||
attemptsRef.current += 1;
|
||||
console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err)
|
||||
|
||||
// 更新状态用于显示
|
||||
setFetchAttempts(attemptsRef.current)
|
||||
|
||||
// 设置适当的错误消息
|
||||
if (!navigator.onLine) {
|
||||
setError(t('errors.network'))
|
||||
} else {
|
||||
setError(t('errors.initialStartup'))
|
||||
}
|
||||
|
||||
// 如果已超过最大尝试次数,放弃初始化并切换到正常轮询
|
||||
if (attemptsRef.current >= CONFIG.startup.maxAttempts) {
|
||||
console.log('Maximum startup attempts reached, switching to normal polling')
|
||||
setIsInitialLoading(false)
|
||||
// 清除初始化的轮询
|
||||
clearTimer()
|
||||
// 切换到正常轮询模式
|
||||
startNormalPolling()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fetchServers()
|
||||
// 组件挂载时,根据当前状态设置适当的轮询
|
||||
if (isInitialLoading) {
|
||||
// 确保没有其他定时器在运行
|
||||
clearTimer()
|
||||
|
||||
// 立即执行一次初始请求
|
||||
fetchInitialData()
|
||||
|
||||
// 设置初始阶段的轮询间隔
|
||||
intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval)
|
||||
console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`)
|
||||
} else {
|
||||
// 已经初始化完成,开始正常轮询
|
||||
startNormalPolling()
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
clearTimer()
|
||||
}
|
||||
}, [refreshKey, t, isInitialLoading, startNormalPolling])
|
||||
|
||||
// Poll for updates every 5 seconds
|
||||
const interval = setInterval(fetchServers, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [refreshKey])
|
||||
// 手动触发刷新
|
||||
const triggerRefresh = () => {
|
||||
// 清除当前的定时器
|
||||
clearTimer()
|
||||
|
||||
// 如果在初始化阶段,重置初始化状态
|
||||
if (isInitialLoading) {
|
||||
setIsInitialLoading(true)
|
||||
attemptsRef.current = 0
|
||||
setFetchAttempts(0)
|
||||
}
|
||||
|
||||
// refreshKey 的改变会触发 useEffect 再次运行
|
||||
setRefreshKey(prevKey => prevKey + 1)
|
||||
}
|
||||
|
||||
const handleServerAdd = () => {
|
||||
setRefreshKey(prevKey => prevKey + 1)
|
||||
@@ -101,28 +248,29 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-red-50 p-8">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h2 className="text-red-600 text-xl font-semibold">{t('app.error')}</h2>
|
||||
<p className="text-gray-600 mt-2">{error}</p>
|
||||
<button
|
||||
onClick={() => setError(null)}
|
||||
className="mt-4 bg-red-100 text-red-800 py-1 px-3 rounded hover:bg-red-200"
|
||||
>
|
||||
{t('app.closeButton')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-8">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{error && (
|
||||
<div className="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-red-600 text-lg font-medium">{t('app.error')}</h3>
|
||||
<p className="text-gray-600 mt-1">{error}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setError(null)}
|
||||
className="ml-4 text-gray-500 hover:text-gray-700"
|
||||
aria-label={t('app.closeButton')}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 011.414 0L10 8.586l4.293-4.293a1 1 111.414 1.414L11.414 10l4.293 4.293a1 1 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 01-1.414-1.414L8.586 10 4.293 5.707a1 1 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('app.title')}</h1>
|
||||
<AddServerForm onAdd={handleServerAdd} />
|
||||
|
||||
@@ -9,13 +9,16 @@ interface AddServerFormProps {
|
||||
const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const toggleModal = () => {
|
||||
setModalVisible(!modalVisible)
|
||||
setError(null) // Clear any previous errors when toggling modal
|
||||
}
|
||||
|
||||
const handleSubmit = async (payload: any) => {
|
||||
try {
|
||||
setError(null)
|
||||
const response = await fetch('/api/servers', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -25,14 +28,35 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
alert(result.message || 'Failed to add server')
|
||||
// Use specific error message from the response if available
|
||||
if (result && result.message) {
|
||||
setError(result.message)
|
||||
} else if (response.status === 400) {
|
||||
setError(t('server.invalidData'))
|
||||
} else if (response.status === 409) {
|
||||
setError(t('server.alreadyExists', { serverName: payload.name }))
|
||||
} else {
|
||||
setError(t('server.addError'))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setModalVisible(false)
|
||||
onAdd()
|
||||
} catch (err) {
|
||||
alert(`Error: ${err instanceof Error ? err.message : String(err)}`)
|
||||
console.error('Error adding server:', err)
|
||||
|
||||
// Use friendly error messages based on error type
|
||||
if (!navigator.onLine) {
|
||||
setError(t('errors.network'))
|
||||
} else if (err instanceof TypeError && (
|
||||
err.message.includes('NetworkError') ||
|
||||
err.message.includes('Failed to fetch')
|
||||
)) {
|
||||
setError(t('errors.serverConnection'))
|
||||
} else {
|
||||
setError(t('errors.serverAdd'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +71,12 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
||||
|
||||
{modalVisible && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||
<ServerForm onSubmit={handleSubmit} onCancel={toggleModal} modalTitle={t('server.addServer')} />
|
||||
<ServerForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={toggleModal}
|
||||
modalTitle={t('server.addServer')}
|
||||
formError={error}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server } from '@/types'
|
||||
import ServerForm from './ServerForm'
|
||||
@@ -10,9 +11,11 @@ interface EditServerFormProps {
|
||||
|
||||
const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleSubmit = async (payload: any) => {
|
||||
try {
|
||||
setError(null)
|
||||
const response = await fetch(`/api/servers/${server.name}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -22,13 +25,34 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
alert(result.message || t('server.updateError', 'Failed to update server'))
|
||||
// Use specific error message from the response if available
|
||||
if (result && result.message) {
|
||||
setError(result.message)
|
||||
} else if (response.status === 404) {
|
||||
setError(t('server.notFound', { serverName: server.name }))
|
||||
} else if (response.status === 400) {
|
||||
setError(t('server.invalidData'))
|
||||
} else {
|
||||
setError(t('server.updateError', { serverName: server.name }))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
onEdit()
|
||||
} catch (err) {
|
||||
alert(`${t('errors.general')}: ${err instanceof Error ? err.message : String(err)}`)
|
||||
console.error('Error updating server:', err)
|
||||
|
||||
// Use friendly error messages based on error type
|
||||
if (!navigator.onLine) {
|
||||
setError(t('errors.network'))
|
||||
} else if (err instanceof TypeError && (
|
||||
err.message.includes('NetworkError') ||
|
||||
err.message.includes('Failed to fetch')
|
||||
)) {
|
||||
setError(t('errors.serverConnection'))
|
||||
} else {
|
||||
setError(t('errors.serverUpdate', { serverName: server.name }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +63,7 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
||||
onCancel={onCancel}
|
||||
initialData={server}
|
||||
modalTitle={t('server.editTitle', {serverName: server.name})}
|
||||
formError={error}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -7,9 +7,10 @@ interface ServerFormProps {
|
||||
onCancel: () => void
|
||||
initialData?: Server | null
|
||||
modalTitle: string
|
||||
formError?: string | null
|
||||
}
|
||||
|
||||
const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: ServerFormProps) => {
|
||||
const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formError = null }: ServerFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [serverType, setServerType] = useState<'sse' | 'stdio'>(
|
||||
initialData && initialData.config && initialData.config.url ? 'sse' : 'stdio',
|
||||
@@ -104,7 +105,11 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div className="bg-red-50 text-red-700 p-3 rounded mb-4">{error}</div>}
|
||||
{(error || formError) && (
|
||||
<div className="bg-red-50 text-red-700 p-3 rounded mb-4">
|
||||
{formError || error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"save": "Save Changes",
|
||||
"cancel": "Cancel",
|
||||
"invalidConfig": "Could not find configuration data for {{serverName}}",
|
||||
"addError": "Failed to add server",
|
||||
"editError": "Failed to edit server {{serverName}}",
|
||||
"deleteError": "Failed to delete server {{serverName}}",
|
||||
"updateError": "Failed to update server",
|
||||
"editTitle": "Edit Server: {{serverName}}",
|
||||
@@ -36,6 +38,12 @@
|
||||
"connecting": "Connecting"
|
||||
},
|
||||
"errors": {
|
||||
"general": "Something went wrong"
|
||||
"general": "Something went wrong",
|
||||
"network": "Network connection error. Please check your internet connection",
|
||||
"serverConnection": "Unable to connect to the server. Please check if the server is running",
|
||||
"serverAdd": "Failed to add server. Please check the server status",
|
||||
"serverUpdate": "Failed to edit server {{serverName}}. Please check the server status",
|
||||
"serverFetch": "Failed to retrieve server data. Please try again later",
|
||||
"initialStartup": "The server might be starting up. Please wait a moment as this process can take some time on first launch..."
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@
|
||||
"apiKey": "API 密钥",
|
||||
"save": "保存更改",
|
||||
"cancel": "取消",
|
||||
"addError": "添加服务器失败",
|
||||
"editError": "编辑服务器 {{serverName}} 失败",
|
||||
"invalidConfig": "无法找到 {{serverName}} 的配置数据",
|
||||
"deleteError": "删除服务器 {{serverName}} 失败",
|
||||
"updateError": "更新服务器失败",
|
||||
@@ -36,6 +38,12 @@
|
||||
"connecting": "连接中"
|
||||
},
|
||||
"errors": {
|
||||
"general": "发生错误"
|
||||
"general": "发生错误",
|
||||
"network": "网络连接错误,请检查您的互联网连接",
|
||||
"serverConnection": "无法连接到服务器,请检查服务器是否正在运行",
|
||||
"serverAdd": "添加服务器失败,请检查服务器状态",
|
||||
"serverUpdate": "编辑服务器 {{serverName}} 失败,请检查服务器状态",
|
||||
"serverFetch": "获取服务器数据失败,请稍后重试",
|
||||
"initialStartup": "服务器可能正在启动中。首次启动可能需要一些时间,请耐心等候..."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user