import { memo, useCallback, useEffect, useRef, useState } from 'react' import { Server, Plus } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { clusterApi, deploymentApi, collectorApi } from '@/api' import { PageContainer } from '@/components/PageContainer' import { LoadingSpinner } from '@/components/LoadingSpinner' import { EmptyState } from '@/components/EmptyState' import { ErrorState } from '@/components/ErrorState' import { PaginationBar } from '@/components/PaginationBar' import { ConfirmDialog } from '@/components/ConfirmDialog' import { toast } from 'sonner' import { useListQuery } from '@/hooks/useListQuery' import type { Cluster } from '@/types' import i18n from '@/i18n' import zhClusters from '@/i18n/locales/zh/clusters' import enClusters from '@/i18n/locales/en/clusters' import { ClusterCard } from './components/ClusterCard' import { ClusterFormDialog } from './components/ClusterFormDialog' import { ClusterDetailDialog } from './components/ClusterDetailDialog' // 注册本域命名空间(i18n/index.ts 只负责 common,域词典就近注册;页面为懒加载,渲染前必然已注册) i18n.addResourceBundle('zh', 'clusters', zhClusters) i18n.addResourceBundle('en', 'clusters', enClusters) /** * 集群管理页 — 只做编排:数据走 useListQuery(取消+版本号), * 卡片/表单/详情/删除确认均为域内子组件。成功操作静默(列表刷新即反馈)。 */ const ClustersPage = memo(function ClustersPage() { const { t } = useTranslation('clusters') const query = useListQuery((params, signal) => clusterApi.list(params, { signal }) ) const [formOpen, setFormOpen] = useState(false) const [detailCluster, setDetailCluster] = useState(null) const [deleteTarget, setDeleteTarget] = useState(null) const [editingTarget, setEditingTarget] = useState(null) const [deleting, setDeleting] = useState(false) const [validating, setValidating] = useState(null) // 验证后延时刷新列表的定时器(卸载清理) const timeoutsRef = useRef[]>([]) useEffect(() => { const timers = timeoutsRef.current return () => { timers.forEach((id) => clearTimeout(id)) } }, []) // 删除保护:引用检查(部署/采集器仍指向该集群时警告) const [refCount, setRefCount] = useState<{ dep: number; col: number }>({ dep: 0, col: 0 }) useEffect(() => { if (!deleteTarget) return Promise.all([ deploymentApi.list({ page: 1, pageSize: 100 }), collectorApi.list({ page: 1, pageSize: 100 }), ]).then(([d, c]) => { setRefCount({ dep: d.list.filter((x) => x.clusterID === deleteTarget.id).length, col: c.list.filter((x) => x.clusterID === deleteTarget.id).length, }) }).catch(() => setRefCount({ dep: 0, col: 0 })) }, [deleteTarget]) const handleDelete = useCallback(async () => { const id = deleteTarget?.id if (!id) return setDeleting(true) try { await clusterApi.remove(id) setDeleteTarget(null) query.refresh() } catch (err) { toast.error(err instanceof Error ? err.message : t('toast.deleteFailed')) } finally { setDeleting(false) } }, [deleteTarget, query, t]) const handleValidate = useCallback( async (id: number) => { setValidating(id) try { await clusterApi.validate(id) timeoutsRef.current.push(setTimeout(() => query.refresh(), 3000)) } catch (err) { toast.error(err instanceof Error ? err.message : t('toast.validateFailed')) } finally { timeoutsRef.current.push(setTimeout(() => setValidating(null), 3000)) } }, [query, t] ) return ( setFormOpen(true)}> {t('actions.add')} } > {query.loading ? ( ) : query.error ? ( ) : query.list.length === 0 ? ( ) : ( <>
{query.list.map((cluster) => ( ))}
)} { if (!o) { setFormOpen(false) setEditingTarget(null) } }} onCreated={() => (editingTarget ? query.refresh() : query.reload())} editing={editingTarget} /> setDetailCluster(null)} /> { if (!open) setDeleteTarget(null) }} name={deleteTarget?.name} loading={deleting} description={ refCount.dep + refCount.col > 0 ? t('delete.warning', { name: deleteTarget?.name ?? '', dep: refCount.dep, col: refCount.col, }) : undefined } onConfirm={handleDelete} />
) }) export default ClustersPage