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

+94 -249
View File
@@ -1,270 +1,115 @@
import { useState } from 'react';
import { Bell, Info, TestTube } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Separator } from '@/components/ui/separator';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useEffect, useState } from 'react';
import { Info, Save, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api';
export default function BasicInfoPanel() {
const [pipelineName, setPipelineName] = useState('gateway-pre-serverless');
const [description, setDescription] = useState('');
interface Props {
pipelineId: number;
}
// Feishu notification
const [feishuEnabled, setFeishuEnabled] = useState(false);
const [feishuWebhook, setFeishuWebhook] = useState('');
const [feishuTiming, setFeishuTiming] = useState('all');
export default function BasicInfoPanel({ pipelineId }: Props) {
const queryClient = useQueryClient();
const { data: detail } = useQuery({
queryKey: ['pipeline', pipelineId],
queryFn: () => apiClient.getPipeline(pipelineId),
});
const p = detail?.pipeline;
// DingTalk notification
const [dingtalkEnabled, setDingtalkEnabled] = useState(false);
const [dingtalkWebhook, setDingtalkWebhook] = useState('');
const [dingtalkTiming, setDingtalkTiming] = useState('all');
const [form, setForm] = useState({ name: '', description: '', sourceRepo: '', sourceBranch: '', deployType: 'host', env: 'staging', notifyWebhookURL: '' });
const handleTestConnection = (type: 'feishu' | 'dingtalk') => {
toast.success(`${type === 'feishu' ? '飞书' : '钉钉'} Webhook 连接测试成功`, {
description: '通知服务可正常访问',
});
};
useEffect(() => {
if (p) {
setForm({
name: p.name || '', description: p.description || '',
sourceRepo: p.sourceRepo || '', sourceBranch: p.sourceBranch || 'main',
deployType: p.deployType || 'host', env: p.env || 'staging',
notifyWebhookURL: p.notifyWebhookURL || '',
});
}
}, [p?.id, p?.updatedAt]);
const timingOptions = [
{ value: 'all', label: '全部事件' },
{ value: 'success', label: '仅成功' },
{ value: 'failure', label: '仅失败' },
];
const saveMutation = useMutation({
mutationFn: (body: any) => apiClient.updatePipeline(pipelineId, body),
onSuccess: () => {
toast.success('基本信息已保存');
queryClient.invalidateQueries({ queryKey: ['pipeline', pipelineId] });
queryClient.invalidateQueries({ queryKey: ['pipelines'] });
},
onError: (err: Error) => toast.error('保存失败', { description: err.message }),
});
const set = (k: string, v: string) => setForm((f) => ({ ...f, [k]: v }));
const inputStyle = { background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' };
return (
<div className="tab-content-fade space-y-6 w-full px-6 py-6">
{/* Basic Information */}
<div
className="rounded-lg border p-6 shadow-sm"
style={{
background: 'var(--panel-bg)',
borderColor: 'var(--border-color)',
}}
>
<div className="flex items-center gap-2 mb-5">
<Info className="w-5 h-5" style={{ color: 'var(--accent-blue)' }} />
<h3
className="text-base font-semibold"
style={{ color: 'var(--text-primary)' }}
>
基本信息
</h3>
<div className="tab-content-fade w-full px-6 py-6 max-w-3xl">
<div className="rounded-lg border p-6 shadow-sm" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-2">
<Info className="w-5 h-5" style={{ color: 'var(--accent-blue)' }} />
<h3 className="text-[14px] font-semibold" style={{ color: 'var(--text-primary)' }}>基本信息</h3>
{p?.type === 'jenkins' && (
<span className="text-[10px] px-1.5 py-[1px] rounded" style={{ background: '#fbefff', color: '#8250df' }}>Jenkins 导入</span>
)}
</div>
<div className="flex items-center gap-3">
{p && <span className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>更新于 {new Date(p.updatedAt).toLocaleString('zh-CN', { hour12: false })}</span>}
<button
onClick={() => saveMutation.mutate(form)}
disabled={saveMutation.isPending || !form.name}
className="flex items-center gap-1 px-3 py-1 text-[12px] rounded-md text-white disabled:opacity-50"
style={{ background: 'var(--accent-blue)' }}
>
{saveMutation.isPending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}保存
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<div>
<Label className="text-sm font-medium mb-1.5 block" style={{ color: 'var(--text-secondary)' }}>
流水线名称
</Label>
<Input
value={pipelineName}
onChange={(e) => setPipelineName(e.target.value)}
placeholder="输入流水线名称"
className="h-9"
/>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>流水线名称 *</label>
<input value={form.name} onChange={(e) => set('name', e.target.value)} className="w-full px-3 py-2 text-[13px] rounded-md border outline-none" style={inputStyle} />
</div>
<div>
<Label className="text-sm font-medium mb-1.5 block" style={{ color: 'var(--text-secondary)' }}>
描述
</Label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="输入流水线描述(可选)"
rows={1}
className="w-full px-3 py-2 text-sm rounded-md border outline-none focus:ring-1 focus:ring-[var(--accent-blue)] focus:border-transparent transition-all resize-none h-9"
style={{
background: 'var(--canvas-bg)',
borderColor: 'var(--border-color)',
color: 'var(--text-primary)',
}}
/>
<div className="space-y-1.5">
<label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>默认分支</label>
<input value={form.sourceBranch} onChange={(e) => set('sourceBranch', e.target.value)} className="w-full px-3 py-2 text-[13px] rounded-md border outline-none" style={inputStyle} />
</div>
<div>
<Label className="text-xs mb-1 block" style={{ color: 'var(--text-secondary)', opacity: 0.7 }}>
创建人
</Label>
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
admin
</span>
<div className="space-y-1.5 col-span-2">
<label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>描述</label>
<input value={form.description} onChange={(e) => set('description', e.target.value)} className="w-full px-3 py-2 text-[13px] rounded-md border outline-none" style={inputStyle} />
</div>
<div>
<Label className="text-xs mb-1 block" style={{ color: 'var(--text-secondary)', opacity: 0.7 }}>
创建时间
</Label>
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
2024-06-20
</span>
<div className="space-y-1.5 col-span-2">
<label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>代码仓库</label>
<input value={form.sourceRepo} onChange={(e) => set('sourceRepo', e.target.value)} placeholder="https://gitee.com/org/repo.git" className="w-full px-3 py-2 text-[13px] rounded-md border outline-none font-mono" style={inputStyle} />
</div>
<div>
<Label className="text-xs mb-1 block" style={{ color: 'var(--text-secondary)', opacity: 0.7 }}>
最近执行
</Label>
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
未执行
</span>
<div className="space-y-1.5">
<label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>部署方式</label>
<select value={form.deployType} onChange={(e) => set('deployType', e.target.value)} className="w-full px-3 py-2 text-[13px] rounded-md border outline-none" style={inputStyle}>
<option value="host">主机部署</option>
<option value="k8s">K8s 部署</option>
<option value="gitops">GitOps</option>
</select>
</div>
<div className="space-y-1.5">
<label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>默认环境</label>
<select value={form.env} onChange={(e) => set('env', e.target.value)} className="w-full px-3 py-2 text-[13px] rounded-md border outline-none" style={inputStyle}>
<option value="dev">dev</option>
<option value="test">test</option>
<option value="staging">staging</option>
<option value="production">production</option>
</select>
</div>
</div>
</div>
{/* Notification Settings */}
<div
className="rounded-lg border p-6 shadow-sm"
style={{
background: 'var(--panel-bg)',
borderColor: 'var(--border-color)',
}}
>
<div className="flex items-center gap-2 mb-5">
<Bell className="w-5 h-5" style={{ color: 'var(--accent-blue)' }} />
<h3
className="text-base font-semibold"
style={{ color: 'var(--text-primary)' }}
>
通知设置
</h3>
</div>
<div className="space-y-6">
{/* Feishu */}
<div>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
飞书通知
</span>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-3.5 h-3.5 cursor-help" style={{ color: 'var(--text-secondary)', opacity: 0.5 }} />
</TooltipTrigger>
<TooltipContent>
<p>通过飞书群机器人发送流水线执行通知</p>
</TooltipContent>
</Tooltip>
</div>
<Switch checked={feishuEnabled} onCheckedChange={setFeishuEnabled} />
</div>
{feishuEnabled && (
<div className="tab-content-fade pl-0 space-y-3">
<div>
<Label className="text-xs mb-1.5 block" style={{ color: 'var(--text-secondary)' }}>
Webhook 地址
</Label>
<Input
value={feishuWebhook}
onChange={(e) => setFeishuWebhook(e.target.value)}
placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/..."
className="h-9"
/>
</div>
<div>
<Label className="text-xs mb-2 block" style={{ color: 'var(--text-secondary)' }}>
通知时机
</Label>
<RadioGroup
value={feishuTiming}
onValueChange={setFeishuTiming}
className="flex items-center gap-4"
>
{timingOptions.map((opt) => (
<div key={opt.value} className="flex items-center space-x-2">
<RadioGroupItem value={opt.value} id={`feishu-${opt.value}`} />
<Label htmlFor={`feishu-${opt.value}`} className="text-sm cursor-pointer">
{opt.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className="pt-1">
<Button
variant="outline"
size="sm"
onClick={() => handleTestConnection('feishu')}
className="h-8"
>
<TestTube className="w-3.5 h-3.5 mr-1.5" />
测试连接
</Button>
</div>
</div>
)}
</div>
<Separator style={{ backgroundColor: 'var(--border-color)' }} />
{/* DingTalk */}
<div>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
钉钉通知
</span>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-3.5 h-3.5 cursor-help" style={{ color: 'var(--text-secondary)', opacity: 0.5 }} />
</TooltipTrigger>
<TooltipContent>
<p>通过钉钉群机器人发送流水线执行通知</p>
</TooltipContent>
</Tooltip>
</div>
<Switch checked={dingtalkEnabled} onCheckedChange={setDingtalkEnabled} />
</div>
{dingtalkEnabled && (
<div className="tab-content-fade pl-0 space-y-3">
<div>
<Label className="text-xs mb-1.5 block" style={{ color: 'var(--text-secondary)' }}>
Webhook 地址
</Label>
<Input
value={dingtalkWebhook}
onChange={(e) => setDingtalkWebhook(e.target.value)}
placeholder="https://oapi.dingtalk.com/robot/send?access_token=..."
className="h-9"
/>
</div>
<div>
<Label className="text-xs mb-2 block" style={{ color: 'var(--text-secondary)' }}>
通知时机
</Label>
<RadioGroup
value={dingtalkTiming}
onValueChange={setDingtalkTiming}
className="flex items-center gap-4"
>
{timingOptions.map((opt) => (
<div key={opt.value} className="flex items-center space-x-2">
<RadioGroupItem value={opt.value} id={`dingtalk-${opt.value}`} />
<Label htmlFor={`dingtalk-${opt.value}`} className="text-sm cursor-pointer">
{opt.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className="pt-1">
<Button
variant="outline"
size="sm"
onClick={() => handleTestConnection('dingtalk')}
className="h-8"
>
<TestTube className="w-3.5 h-3.5 mr-1.5" />
测试连接
</Button>
</div>
</div>
)}
</div>
<div className="rounded-lg border p-6 shadow-sm mt-4" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
<h3 className="text-[14px] font-semibold mb-1" style={{ color: 'var(--text-primary)' }}>发布通知</h3>
<p className="text-[11px] mb-4" style={{ color: 'var(--text-tertiary)' }}>流水线成功/失败后回调该 Webhook(也在「Webhook 配置」中统一管理)</p>
<div className="space-y-1.5">
<label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>通知 Webhook URL</label>
<input value={form.notifyWebhookURL} onChange={(e) => set('notifyWebhookURL', e.target.value)} placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/xxx" className="w-full px-3 py-2 text-[13px] rounded-md border outline-none font-mono" style={inputStyle} />
</div>
</div>
</div>