import { useState } from 'react'; import { CloudSync, GitBranch, RefreshCw, CheckCircle2, XCircle, Clock, ChevronRight, TreePine, Diff, Layers, Server, AlertTriangle, FolderGit, Package } from 'lucide-react'; import { toast } from 'sonner'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { api } from '@/lib/api'; interface Application { id: string; name: string; repo: string; branch: string; path: string; cluster: string; namespace: string; status: 'synced' | 'out-of-sync' | 'syncing' | 'error' | 'unknown'; syncStatus: 'synced' | 'out-of-sync'; health: 'healthy' | 'degraded' | 'progressing' | 'suspended'; lastSync: string; resources: number; manifestType: 'helm' | 'kustomize' | 'plain'; autoSync: boolean; } interface ResourceNode { id: string; kind: string; name: string; namespace: string; status: 'healthy' | 'degraded' | 'progressing' | 'suspended'; children?: ResourceNode[]; } const statusConfig: Record = { synced: { color: '#1a7f37', bg: '#dafbe1', label: '已同步', icon: CheckCircle2 }, 'out-of-sync': { color: '#9a6700', bg: '#fff8c5', label: '待同步', icon: Clock }, syncing: { color: '#0969da', bg: '#ddf4ff', label: '同步中', icon: RefreshCw }, error: { color: '#cf222e', bg: '#ffebe9', label: '错误', icon: XCircle }, unknown: { color: '#6e7781', bg: '#f6f8fa', label: '未知', icon: AlertTriangle }, }; const healthConfig: Record = { healthy: { color: '#1a7f37', label: '健康', bg: '#dafbe1' }, degraded: { color: '#cf222e', label: '降级', bg: '#ffebe9' }, progressing: { color: '#9a6700', label: '进行中', bg: '#fff8c5' }, suspended: { color: '#6e7781', label: '暂停', bg: '#f6f8fa' }, }; const manifestConfig: Record = { helm: { label: 'Helm', icon: Package }, kustomize: { label: 'Kustomize', icon: Layers }, plain: { label: 'Plain YAML', icon: FolderGit }, }; export default function GitOpsDeploy() { const [selectedAppId, setSelectedAppId] = useState(null); const [activeView, setActiveView] = useState<'list' | 'tree' | 'diff'>('list'); const queryClient = useQueryClient(); const { data: dbApps = [] } = useQuery({ queryKey: ['gitops', 'apps'], queryFn: () => api('/api/gitops/apps') }); const syncMutation = useMutation({ mutationFn: (appName: string) => api('/api/gitops/argo/sync', { method: 'POST', body: JSON.stringify({ appName }) }), onSuccess: (data) => { toast.success(data.message); queryClient.invalidateQueries({ queryKey: ['gitops'] }); }, onError: (err: Error) => toast.error(err.message), }); // dbApps 直接携带 syncStatus/healthStatus(sync 动作会真实推进状态) const apps: Application[] = dbApps.map((db: any) => { const sync = (db.syncStatus || '').toLowerCase(); return { id: String(db.id), name: db.name, repo: db.repo, branch: db.branch, path: db.path, cluster: db.cluster, namespace: db.namespace, status: sync === 'synced' ? 'synced' : sync === 'syncing' ? 'syncing' : sync === 'outofsync' ? 'out-of-sync' : 'unknown', syncStatus: sync === 'synced' ? 'synced' : 'out-of-sync', health: (db.healthStatus || 'unknown').toLowerCase(), lastSync: db.lastSyncAt ? new Date(db.lastSyncAt).toLocaleString('zh-CN', { hour12: false }) : '-', resources: 0, manifestType: db.manifestType, autoSync: db.syncPolicy === 'automated', }; }); const selectedApp = apps.find(a => a.id === selectedAppId) || apps[0]; const selectedName = selectedApp?.name; // 资源树 / 差异:真实调用后端生成端点 const { data: treeData, isLoading: treeLoading } = useQuery({ queryKey: ['gitops', 'tree', selectedName], queryFn: () => api(`/api/gitops/argo/${selectedName}/tree`), enabled: activeView === 'tree' && !!selectedName, }); const { data: diffData, isLoading: diffLoading } = useQuery({ queryKey: ['gitops', 'diff', selectedName], queryFn: () => api(`/api/gitops/argo/${selectedName}/diff`), enabled: activeView === 'diff' && !!selectedName, }); // nodes+edges 扁平结构 → 树 const resourceTree: ResourceNode[] = (() => { if (!treeData?.nodes) return []; const byUid = new Map(); for (const n of treeData.nodes) { byUid.set(n.uid, { id: n.uid, kind: n.kind, name: n.name, namespace: n.namespace, status: n.status }); } const childSet = new Set(); for (const e of treeData.edges || []) { const parent = byUid.get(e.source); const child = byUid.get(e.target); if (parent && child) { parent.children = parent.children || []; parent.children.push(child); childSet.add(e.target); } } return treeData.nodes.filter((n: any) => !childSet.has(n.uid)).map((n: any) => byUid.get(n.uid)!); })(); const renderResourceNode = (node: ResourceNode, depth: number = 0) => { const health = healthConfig[node.status]; const hasChildren = node.children && node.children.length > 0; return (
{hasChildren ? :
}
{node.kind} {node.name} {node.namespace}
{hasChildren && node.children!.map(child => renderResourceNode(child, depth + 1))}
); }; return (
{/* Header */}

GitOps 部署

Argo CD
{[ { id: 'list', label: '应用列表', icon: Layers }, { id: 'tree', label: '资源树', icon: TreePine }, { id: 'diff', label: '差异对比', icon: Diff }, ].map(v => ( ))}
{/* Content */}
{/* App list sidebar */}
Applications ({apps.length})
{apps.map(app => { const st = statusConfig[app.status]; const hl = healthConfig[app.health]; const mf = manifestConfig[app.manifestType]; const isSelected = selectedAppId === app.id; return (
setSelectedAppId(app.id)} className="mx-2 mb-1 p-2.5 rounded-md cursor-pointer border transition-all" style={{ background: isSelected ? 'var(--accent-blue-light)' : 'transparent', borderColor: isSelected ? 'var(--accent-blue)' : 'transparent', }}>
{app.name} {st.label}
{app.repo.replace('https://', '').replace('.git', '')}
{mf.label} {hl.label}
{app.lastSync}
); })}
{/* Detail view */}
{!selectedApp ? (

选择一个 Application 查看详情

Git 仓库作为部署声明的单一事实来源

) : activeView === 'list' ? (
{/* App header */}

{selectedApp.name}

{statusConfig[selectedApp.status].label} {healthConfig[selectedApp.health].label}
Git 仓库
{selectedApp.repo}
分支 / 路径
{selectedApp.branch} / {selectedApp.path}
目标集群
{selectedApp.cluster}
命名空间
{selectedApp.namespace}
{/* Sync config */}

同步配置

{/* Environment promotion */}

环境晋升

开发环境
dev
已同步
测试环境
staging
待同步
生产环境
production
已同步
) : activeView === 'tree' ? (
资源树{selectedName ? ` · ${selectedName}` : ''} {resourceTree.length} 个顶级资源
{treeLoading &&
加载中…
} {!treeLoading && resourceTree.map(node => renderResourceNode(node))}
) : (

Git vs 集群差异{selectedName ? ` · ${selectedName}` : ''}

{diffLoading &&
加载中…
}
{(diffData?.diffs || []).map((d: any, i: number) => (
{d.kind}/{d.name} {d.namespace} {d.status}
{d.diff}
))} {!diffLoading && (diffData?.diffs || []).length === 0 && (
Git 与集群一致,无差异
)}
)}
); }