feat: enhance error handling and messages

This commit is contained in:
samanhappy
2025-04-12 12:00:52 +08:00
parent 5df6dd99b7
commit 188f9ab8e6
8 changed files with 261 additions and 38 deletions

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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>
)

View File

@@ -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">

View File

@@ -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..."
}
}

View File

@@ -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": "服务器可能正在启动中。首次启动可能需要一些时间,请耐心等候..."
}
}