From 0ea5f12e07db42e437ed9e071ba8ad318183c34f Mon Sep 17 00:00:00 2001 From: Awuqing <3184394176@qq.com> Date: Sun, 19 Apr 2026 16:47:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20AgentInstallWizard=20?= =?UTF-8?q?=E4=B8=BB=E5=AE=B9=E5=99=A8=E6=95=B4=E5=90=88=E4=B8=89=E6=AD=A5?= =?UTF-8?q?=E5=90=91=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/nodes/AgentInstallWizard.tsx | 244 +++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 web/src/pages/nodes/AgentInstallWizard.tsx diff --git a/web/src/pages/nodes/AgentInstallWizard.tsx b/web/src/pages/nodes/AgentInstallWizard.tsx new file mode 100644 index 0000000..7bab0ca --- /dev/null +++ b/web/src/pages/nodes/AgentInstallWizard.tsx @@ -0,0 +1,244 @@ +import React, { useState } from 'react' +import { Modal, Steps, Button, Space, Message, Spin } from '@arco-design/web-react' +import { Step1NodeName, type Mode } from './wizard/Step1NodeName' +import { Step2DeployOptions, type DeployOptions } from './wizard/Step2DeployOptions' +import { Step3CommandPreview } from './wizard/Step3CommandPreview' +import { BatchCommandTable, type BatchCommandRow } from './BatchCommandTable' +import { batchCreateNodes, createInstallToken } from '../../services/nodes' +import type { InstallTokenResult } from '../../types/nodes' + +const Step = Steps.Step + +interface Props { + visible: boolean + onClose: () => void + onSuccess: () => void + masterVersion: string + // 当从节点列表直接点"生成安装命令"时传入,跳过 Step1 + fixedNode?: { id: number; name: string } +} + +export function AgentInstallWizard({ visible, onClose, onSuccess, masterVersion, fixedNode }: Props) { + const [step, setStep] = useState(fixedNode ? 1 : 0) + const [mode, setMode] = useState('single') + const [singleName, setSingleName] = useState('') + const [batchText, setBatchText] = useState('') + + const [deploy, setDeploy] = useState({ + mode: 'systemd', + arch: 'auto', + agentVersion: masterVersion, + downloadSrc: 'github', + ttlSeconds: 900, + }) + + const [singleToken, setSingleToken] = useState(null) + const [singleNodeInfo, setSingleNodeInfo] = useState<{ id: number; name: string } | null>(null) + const [batchRows, setBatchRows] = useState([]) + const [submitting, setSubmitting] = useState(false) + + const reset = () => { + setStep(fixedNode ? 1 : 0) + setMode('single') + setSingleName('') + setBatchText('') + setSingleToken(null) + setSingleNodeInfo(null) + setBatchRows([]) + } + + const handleClose = () => { + reset() + onClose() + } + + const parseBatchNames = (): string[] => + batchText.split('\n').map((s) => s.trim()).filter(Boolean) + + const handleNextFromStep1 = () => { + if (mode === 'single') { + if (!singleName.trim()) { + Message.warning('请输入节点名称') + return + } + } else { + const names = parseBatchNames() + if (names.length === 0) { + Message.warning('请至少输入一个节点名称') + return + } + if (names.length > 50) { + Message.warning('单次最多创建 50 个节点') + return + } + } + setStep(1) + } + + const handleGenerate = async () => { + setSubmitting(true) + try { + if (fixedNode) { + const tok = await createInstallToken(fixedNode.id, { + mode: deploy.mode, + arch: deploy.arch, + agentVersion: deploy.agentVersion, + downloadSrc: deploy.downloadSrc, + ttlSeconds: deploy.ttlSeconds, + }) + setSingleNodeInfo(fixedNode) + setSingleToken(tok) + } else if (mode === 'single') { + const created = await batchCreateNodes([singleName.trim()]) + const one = created[0] + const tok = await createInstallToken(one.id, { + mode: deploy.mode, + arch: deploy.arch, + agentVersion: deploy.agentVersion, + downloadSrc: deploy.downloadSrc, + ttlSeconds: deploy.ttlSeconds, + }) + setSingleNodeInfo({ id: one.id, name: one.name }) + setSingleToken(tok) + } else { + const names = parseBatchNames() + const created = await batchCreateNodes(names) + const rows: BatchCommandRow[] = [] + for (const c of created) { + const tok = await createInstallToken(c.id, { + mode: deploy.mode, + arch: deploy.arch, + agentVersion: deploy.agentVersion, + downloadSrc: deploy.downloadSrc, + ttlSeconds: deploy.ttlSeconds, + }) + rows.push({ + nodeId: c.id, + nodeName: c.name, + command: `curl -fsSL ${tok.url} | sudo sh`, + expiresAt: tok.expiresAt, + }) + } + setBatchRows(rows) + } + setStep(2) + onSuccess() + } catch (e: any) { + Message.error(e?.message || '操作失败') + } finally { + setSubmitting(false) + } + } + + const regenerateSingle = async () => { + if (!singleNodeInfo) return + setSubmitting(true) + try { + const tok = await createInstallToken(singleNodeInfo.id, { + mode: deploy.mode, + arch: deploy.arch, + agentVersion: deploy.agentVersion, + downloadSrc: deploy.downloadSrc, + ttlSeconds: deploy.ttlSeconds, + }) + setSingleToken(tok) + } catch (e: any) { + Message.error(e?.message || '重新生成失败') + } finally { + setSubmitting(false) + } + } + + const previewParams = { + mode: deploy.mode, + arch: deploy.arch, + agentVersion: deploy.agentVersion, + downloadSrc: deploy.downloadSrc, + } + + // fixedNode 路径下步骤只有 2 步(部署参数 + 安装命令),step 值从 1 开始, + // 需要映射到 Steps current(0-based) + const stepsCurrent = fixedNode ? step - 1 : step + + return ( + + + {!fixedNode && } + + + + + {submitting && ( +
+ +
+ )} + + {!submitting && step === 0 && ( + <> + +
+ + + + +
+ + )} + + {!submitting && step === 1 && ( + <> + +
+ + {!fixedNode && ( + + )} + + + +
+ + )} + + {!submitting && step === 2 && ( + <> + {singleToken && singleNodeInfo && ( + + )} + {batchRows.length > 0 && } +
+ +
+ + )} +
+ ) +}