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 } 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 mockApps: Application[] = [ { id: 'app-1', name: 'aiops-backend', repo: 'https://gitee.com/attacker/aiops-gitops.git', branch: 'main', path: 'apps/aiops-backend/overlays/production', cluster: 'production-k8s', namespace: 'aiops', status: 'synced', syncStatus: 'synced', health: 'healthy', lastSync: '2分钟前', resources: 12, manifestType: 'kustomize', autoSync: true, }, { id: 'app-2', name: 'aiops-web', repo: 'https://gitee.com/attacker/aiops-gitops.git', branch: 'main', path: 'apps/aiops-web/overlays/production', cluster: 'production-k8s', namespace: 'aiops', status: 'syncing', syncStatus: 'out-of-sync', health: 'progressing', lastSync: '正在同步', resources: 8, manifestType: 'helm', autoSync: true, }, { id: 'app-3', name: 'gateway-api', repo: 'https://gitee.com/attacker/aiops-gitops.git', branch: 'release/v2.1', path: 'apps/gateway/overlays/staging', cluster: 'staging-k8s', namespace: 'gateway', status: 'out-of-sync', syncStatus: 'out-of-sync', health: 'degraded', lastSync: '1小时前', resources: 6, manifestType: 'kustomize', autoSync: false, }, { id: 'app-4', name: 'data-processor', repo: 'https://gitee.com/attacker/aiops-gitops.git', branch: 'main', path: 'apps/data/overlays/production', cluster: 'production-k8s', namespace: 'data', status: 'synced', syncStatus: 'synced', health: 'healthy', lastSync: '30分钟前', resources: 5, manifestType: 'plain', autoSync: true, }, ]; const mockResourceTree: ResourceNode[] = [ { id: 'r1', kind: 'Deployment', name: 'aiops-backend', namespace: 'aiops', status: 'healthy', children: [ { id: 'r1-1', kind: 'ReplicaSet', name: 'aiops-backend-7d9f4', namespace: 'aiops', status: 'healthy', children: [ { id: 'r1-1-1', kind: 'Pod', name: 'aiops-backend-7d9f4-abc1', namespace: 'aiops', status: 'healthy' }, { id: 'r1-1-2', kind: 'Pod', name: 'aiops-backend-7d9f4-abc2', namespace: 'aiops', status: 'healthy' }, { id: 'r1-1-3', kind: 'Pod', name: 'aiops-backend-7d9f4-abc3', namespace: 'aiops', status: 'healthy' }, ] }, ] }, { id: 'r2', kind: 'Service', name: 'aiops-backend-svc', namespace: 'aiops', status: 'healthy', }, { id: 'r3', kind: 'Ingress', name: 'aiops-backend-ingress', namespace: 'aiops', status: 'healthy', }, { id: 'r4', kind: 'ConfigMap', name: 'aiops-backend-config', namespace: 'aiops', status: 'healthy', }, ]; 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 { data: dbApps = [] } = useQuery({ queryKey: ['gitops', 'apps'], queryFn: () => api('/api/gitops/apps') }); const { data: argoApps = [] } = useQuery({ queryKey: ['gitops', 'argo'], queryFn: () => api('/api/gitops/argo/apps') }); const syncMutation = useMutation({ mutationFn: (appName: string) => api('/api/gitops/argo/sync', { method: 'POST', body: JSON.stringify({ appName }) }), onSuccess: (data) => toast.success(data.message), onError: (err: Error) => toast.error(err.message), }); const apps: Application[] = dbApps.length > 0 ? dbApps.map((db: any) => { const argo = (argoApps as any[]).find((a: any) => a.metadata?.name === db.name); return { id: String(db.id), name: db.name, repo: db.repo, branch: db.branch, path: db.path, cluster: db.cluster, namespace: db.namespace, status: argo?.status?.sync?.status === 'Synced' ? 'synced' : argo?.status?.sync?.status === 'OutOfSync' ? 'out-of-sync' : 'unknown', syncStatus: argo?.status?.sync?.status === 'Synced' ? 'synced' : 'out-of-sync', health: (argo?.status?.health?.status?.toLowerCase() || 'unknown') as any, lastSync: '刚刚', resources: 0, manifestType: db.manifestType, autoSync: db.syncPolicy === 'automated', }; }) : mockApps; const selectedApp = apps.find(a => a.id === selectedAppId); 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' ? (
资源树 {mockResourceTree.length} 个顶级资源
{mockResourceTree.map(node => renderResourceNode(node))}
) : (

Git vs 集群差异

Deployment/aiops-backend: Git 和集群一致
Service/aiops-backend-svc: 集群中端口被手动修改 (80 {'->'} 8080)
ConfigMap/aiops-backend-config: Git 中新增,集群中不存在
)}
); }