import { useState, useEffect, useCallback } from 'react'; import { Plus, Pencil, Trash2, RefreshCw, Cpu } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { PageShell } from '@/components/layout/PageShell'; import { ContentPanel } from '@/components/layout/ContentPanel'; import { PageEmpty } from '@/components/layout/PageEmpty'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { buildNodeApi, serverApi, type BuildNode } from '@/api'; import i18n from '@/i18n'; interface ServerItem { id: number; hostname: string; ip: string; } // 构建节点管理(Jenkins Agent 风格 SSH 节点) // Logan.Li export default function BuildNodesManager({ embedded = false }: { embedded?: boolean }) { const confirm = useConfirm(); const [nodes, setNodes] = useState([]); const [master, setMaster] = useState({ name: 'Master', max_executors: 2, running_count: 0 }); const [servers, setServers] = useState([]); const [loading, setLoading] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [editNode, setEditNode] = useState(null); const [testingId, setTestingId] = useState(null); const [masterMax, setMasterMax] = useState('2'); const [savingMaster, setSavingMaster] = useState(false); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [serverId, setServerId] = useState(''); const [labels, setLabels] = useState(''); const [maxExecutors, setMaxExecutors] = useState('2'); const [workspacePath, setWorkspacePath] = useState('/data/aiops-workspace'); const [submitting, setSubmitting] = useState(false); const load = useCallback(async () => { setLoading(true); try { const [nodeRes, srvRes] = await Promise.all([ buildNodeApi.list(), serverApi.getServers({ page_size: 200 }).catch(() => ({ list: [] })), ]); const data = nodeRes as { list?: BuildNode[]; master?: typeof master }; setNodes(Array.isArray(data?.list) ? data.list : []); if (data?.master) { setMaster(data.master); setMasterMax(String(data.master.max_executors || 2)); } const srvList = (srvRes as { list?: ServerItem[] })?.list || []; setServers(srvList); } catch { toast.error(i18n.t('deploy.buildNodes.loadFailed')); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const openCreate = () => { setEditNode(null); setName(''); setDescription(''); setServerId(''); setLabels(''); setMaxExecutors('2'); setWorkspacePath('/data/aiops-workspace'); setDialogOpen(true); }; const openEdit = (node: BuildNode) => { setEditNode(node); setName(node.name); setDescription(node.description || ''); setServerId(String(node.server_id)); setLabels(node.labels || ''); setMaxExecutors(String(node.max_executors || 2)); setWorkspacePath(node.workspace_path || '/data/aiops-workspace'); setDialogOpen(true); }; const handleSubmit = async () => { if (!name.trim() || !serverId) { toast.error(i18n.t('deploy.buildNodes.loadFailed')); return; } setSubmitting(true); try { const payload = { name: name.trim(), description, server_id: Number(serverId), labels, max_executors: Number(maxExecutors) || 2, workspace_path: workspacePath || '/data/aiops-workspace', }; if (editNode) { await buildNodeApi.update(editNode.id, payload); toast.success(i18n.t('deploy.toast.updated')); } else { await buildNodeApi.create(payload); toast.success(i18n.t('kubernetes.event.created')); } setDialogOpen(false); load(); } catch { toast.error(i18n.t('cloud.operationFailed')); } finally { setSubmitting(false); } }; const handleDelete = async (node: BuildNode) => { const ok = await confirm({ title: i18n.t('kubernetes.topology.deleteNode'), description: i18n.t('deploy.buildNodes.deleteConfirm', { name: node.name }) }); if (!ok) return; try { await buildNodeApi.delete(node.id); toast.success(i18n.t('monitor.silenceRules.deleted')); load(); } catch { toast.error(i18n.t('deploy.buildNodes.loadFailed')); } }; const handleTest = async (node: BuildNode) => { setTestingId(node.id); try { const res = await buildNodeApi.test(node.id) as { success?: boolean; message?: string }; if (res?.success) { toast.success(i18n.t('deploy.buildNodes.testSuccess') + (res.message ? `: ${res.message}` : '')); } else { toast.error(res?.message || i18n.t('aiops.llmTestFailed')); } } catch { toast.error(i18n.t('monitor.alertChannels.testFailed')); } finally { setTestingId(null); } }; const handleToggle = async (node: BuildNode) => { try { await buildNodeApi.update(node.id, { enabled: !node.enabled }); load(); } catch { toast.error(i18n.t('kubernetes.deployment.imageUpdateFailed')); } }; const handleSaveMaster = async () => { const n = Number(masterMax); if (!n || n < 1) { toast.error(i18n.t('deploy.buildNodes.loadFailed')); return; } setSavingMaster(true); try { const res = await buildNodeApi.updateMaster({ max_executors: n }); const m = res as { max_executors?: number; running_count?: number }; setMaster((prev) => ({ ...prev, max_executors: m?.max_executors ?? n, running_count: m?.running_count ?? prev.running_count, })); toast.success(i18n.t('deploy.buildNodes.deleted')); } catch { toast.error(i18n.t('kubernetes.deployment.imageUpdateFailed')); } finally { setSavingMaster(false); } }; const panel = (
{i18n.t('pipeline.buildNodes')}
)} >
{i18n.t('deploy.buildNodes.masterBuiltin')} {i18n.t('pipeline.buildNodesManage.defaultSchedule')} {i18n.t('deploy.buildNodes.executorUsage', { running: master.running_count, max: master.max_executors })}
setMasterMax(e.target.value)} className="h-7 w-16 text-xs dg-input" />

{i18n.t('deploy.buildNodes.schedulingHint')}

{nodes.length === 0 ? ( {i18n.t('kubernetes.topology.addNode')} } /> ) : (
{nodes.map((node) => (
{node.name} {node.enabled ? i18n.t('system.userActive') : i18n.t('system.userInactive')} {node.hostname || node.ip || `server#${node.server_id}`}
{i18n.t('deploy.buildNodes.executorLabel', { running: node.running_count || 0, max: node.max_executors })} {node.labels && {i18n.t('deploy.buildNodes.labelsLabel', { labels: node.labels })}} {node.workspace_path}
))}
)} ); const dialog = ( {editNode ? i18n.t('kubernetes.topology.editNode') : i18n.t('kubernetes.topology.addNode')} 基于服务器 SSH 凭据注册构建 Agent,流水线可绑定或自动调度
setName(e.target.value)} className="h-8 text-xs dg-input" placeholder="build-node-01" />
setMaxExecutors(e.target.value)} type="number" min={1} className="h-8 text-xs dg-input" />
setWorkspacePath(e.target.value)} className="h-8 text-xs font-mono dg-input" />
setLabels(e.target.value)} className="h-8 text-xs dg-input" placeholder="linux,docker,high-mem" />
setDescription(e.target.value)} className="h-8 text-xs dg-input" />
); if (embedded) { return ( <> {panel} {dialog} ); } return ( {panel} {dialog} ); }