feat: 去 mock 化 —— 全模块接入真实数据 (参考 jianmu/opsx-jenkins/aiops)

后端(35+ 新路由):
- AES-256-GCM 字段加密(enc:v1:),密钥自动生成 data/encryption.key
- 新模型:Host/K8sCluster/Credential/BuildNode/DeployTool/WebhookEndpoint/
  JenkinsServer/SystemSetting;Pipeline 扩展触发器/变量/缓存列,
  PipelineRun 扩展 buildNumber/externalUrl
- 资源 CRUD + 真实测试端点:主机/集群/构建节点 TCP 拨号、
  工具真实执行版本检测(保守安装策略)、Webhook HMAC-SHA256 签名发送、
  入站流水线触发 /api/hooks/pipeline/:id
- Jenkins 完整集成(移植 opsx-jenkins 生产客户端):crumb/BasicAuth/
  buildWithParameters/Location 队列/progressiveText 增量控制台,
  服务器 CRUD+测试、Job 树拉取(folder 压平)、导入为流水线、
  jenkins 类型流水线真实执行回传日志
- 流水线列表附带 lastRun、运行重试、summary 统计、系统设置单行表
- 运行结束触发出站 Webhook 通知;GitOps 同步状态真实推进

前端(14 页全部去 mock):
- 流水线列表/发布记录/批量发布/环境/凭证/构建节点/工具/Webhook/
  系统设置/Jenkins 导入/GitOps(资源树+差异) 全部接入真实 API
- 编辑器 4 Tab 持久化(基本信息/触发设置含真实 Webhook URL/变量缓存/发布历史)
- TopBar 真实流水线切换 + 仅保存/保存并运行
- Sidebar 徽标真实统计;删除未使用组件与 mock 数据文件
This commit is contained in:
Logan.Li committed 2026-10-27 03:15:20 +08:00
1 parent 04d1342b27
commit 26341c3e8a
37 files changed
+5702 -4683

No files matched your search

+84 -93
View File
@@ -5,7 +5,7 @@ import {
Server, AlertTriangle, FolderGit, Package
} from 'lucide-react';
import { toast } from 'sonner';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
interface Application {
@@ -34,57 +34,6 @@ interface ResourceNode {
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<string, { color: string; bg: string; label: string; icon: typeof CheckCircle2 }> = {
synced: { color: '#1a7f37', bg: '#dafbe1', label: '已同步', icon: CheckCircle2 },
'out-of-sync': { color: '#9a6700', bg: '#fff8c5', label: '待同步', icon: Clock },
@@ -109,38 +58,73 @@ const manifestConfig: Record<string, { label: string; icon: typeof Package }> =
export default function GitOpsDeploy() {
const [selectedAppId, setSelectedAppId] = useState<string | null>(null);
const [activeView, setActiveView] = useState<'list' | 'tree' | 'diff'>('list');
const queryClient = useQueryClient();
const { data: dbApps = [] } = useQuery({ queryKey: ['gitops', 'apps'], queryFn: () => api<any[]>('/api/gitops/apps') });
const { data: argoApps = [] } = useQuery({ queryKey: ['gitops', 'argo'], queryFn: () => api<any[]>('/api/gitops/argo/apps') });
const syncMutation = useMutation({
mutationFn: (appName: string) => api<any>('/api/gitops/argo/sync', { method: 'POST', body: JSON.stringify({ appName }) }),
onSuccess: (data) => toast.success(data.message),
onSuccess: (data) => {
toast.success(data.message);
queryClient.invalidateQueries({ queryKey: ['gitops'] });
},
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;
// 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);
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<any>(`/api/gitops/argo/${selectedName}/tree`),
enabled: activeView === 'tree' && !!selectedName,
});
const { data: diffData, isLoading: diffLoading } = useQuery({
queryKey: ['gitops', 'diff', selectedName],
queryFn: () => api<any>(`/api/gitops/argo/${selectedName}/diff`),
enabled: activeView === 'diff' && !!selectedName,
});
// nodes+edges 扁平结构 → 树
const resourceTree: ResourceNode[] = (() => {
if (!treeData?.nodes) return [];
const byUid = new Map<string, ResourceNode>();
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<string>();
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];
@@ -184,7 +168,7 @@ export default function GitOpsDeploy() {
</button>
))}
</div>
<button onClick={() => toast.success('刷新应用状态')} className="flex items-center gap-1 px-3 py-[5px] text-[12px] font-medium rounded-md border" style={{ borderColor: 'var(--border-color-light)', color: 'var(--text-secondary)' }}>
<button onClick={() => { queryClient.invalidateQueries({ queryKey: ['gitops'] }); toast.success('已刷新'); }} className="flex items-center gap-1 px-3 py-[5px] text-[12px] font-medium rounded-md border" style={{ borderColor: 'var(--border-color-light)', color: 'var(--text-secondary)' }}>
<RefreshCw className="w-3.5 h-3.5" />刷新
</button>
</div>
@@ -325,29 +309,36 @@ export default function GitOpsDeploy() {
) : activeView === 'tree' ? (
<div className="rounded-lg border overflow-hidden" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
<div className="px-4 py-2 border-b flex items-center justify-between" style={{ borderColor: 'var(--border-color)' }}>
<span className="text-[13px] font-semibold" style={{ color: 'var(--text-primary)' }}>资源树</span>
<span className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>{mockResourceTree.length} 个顶级资源</span>
<span className="text-[13px] font-semibold" style={{ color: 'var(--text-primary)' }}>资源树{selectedName ? ` · ${selectedName}` : ''}</span>
<span className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>{resourceTree.length} 个顶级资源</span>
</div>
<div className="py-2">
{mockResourceTree.map(node => renderResourceNode(node))}
{treeLoading && <div className="py-6 text-center text-[12px]" style={{ color: 'var(--text-tertiary)' }}>加载中…</div>}
{!treeLoading && resourceTree.map(node => renderResourceNode(node))}
</div>
</div>
) : (
<div className="rounded-lg border p-4" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
<h3 className="text-[13px] font-semibold mb-3" style={{ color: 'var(--text-primary)' }}>Git vs 集群差异</h3>
<h3 className="text-[13px] font-semibold mb-3" style={{ color: 'var(--text-primary)' }}>Git vs 集群差异{selectedName ? ` · ${selectedName}` : ''}</h3>
{diffLoading && <div className="py-6 text-center text-[12px]" style={{ color: 'var(--text-tertiary)' }}>加载中…</div>}
<div className="space-y-2">
<div className="flex items-center gap-3 p-2 rounded-md" style={{ background: '#dafbe1' }}>
<CheckCircle2 className="w-4 h-4" style={{ color: '#1a7f37' }} />
<span className="text-[12px]" style={{ color: '#1a7f37' }}>Deployment/aiops-backend: Git 和集群一致</span>
</div>
<div className="flex items-center gap-3 p-2 rounded-md" style={{ background: '#fff8c5' }}>
<AlertTriangle className="w-4 h-4" style={{ color: '#9a6700' }} />
<span className="text-[12px]" style={{ color: '#9a6700' }}>Service/aiops-backend-svc: 集群中端口被手动修改 (80 {'->'} 8080)</span>
</div>
<div className="flex items-center gap-3 p-2 rounded-md" style={{ background: '#ffebe9' }}>
<XCircle className="w-4 h-4" style={{ color: '#cf222e' }} />
<span className="text-[12px]" style={{ color: '#cf222e' }}>ConfigMap/aiops-backend-config: Git 中新增,集群中不存在</span>
</div>
{(diffData?.diffs || []).map((d: any, i: number) => (
<div key={i} className="rounded-md border overflow-hidden" style={{ borderColor: 'var(--border-color-light)' }}>
<div className="flex items-center gap-3 px-3 py-2" style={{ background: d.status === 'modified' ? '#fff8c5' : '#f6f8fa' }}>
<AlertTriangle className="w-4 h-4" style={{ color: d.status === 'modified' ? '#9a6700' : '#6e7781' }} />
<span className="text-[12px] font-medium" style={{ color: 'var(--text-primary)' }}>{d.kind}/{d.name}</span>
<span className="text-[10px] px-1.5 py-[1px] rounded" style={{ background: 'var(--panel-bg-secondary)', color: 'var(--text-tertiary)' }}>{d.namespace}</span>
<span className="text-[11px] ml-auto" style={{ color: d.status === 'modified' ? '#9a6700' : 'var(--text-tertiary)' }}>{d.status}</span>
</div>
<pre className="px-4 py-2 text-[11px] font-mono whitespace-pre-wrap" style={{ background: 'var(--panel-bg-secondary)', color: 'var(--text-secondary)' }}>{d.diff}</pre>
</div>
))}
{!diffLoading && (diffData?.diffs || []).length === 0 && (
<div className="flex items-center gap-3 p-3 rounded-md" style={{ background: '#dafbe1' }}>
<CheckCircle2 className="w-4 h-4" style={{ color: '#1a7f37' }} />
<span className="text-[12px]" style={{ color: '#1a7f37' }}>Git 与集群一致,无差异</span>
</div>
)}
</div>
</div>
)}