mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-05-06 20:02:41 +08:00
修复: 前端审查发现的 3 项问题
1. masterVersion null 态(NodesPage + Wizard + Step2) 原 'latest' 默认值会生成 releases/download/latest/... 404 URL。 改为 null(拉取中)/ 空串(失败)两态: - 拉取成功:Select 显示 Master 版本 - 拉取失败:Input 要求用户手动输入版本号 - handleGenerate 前校验 agentVersion 非空 2. 批量创建 N+1 串行请求 → Promise.all 并发 + 进度条 原 for 循环逐个 await createInstallToken,50 节点 N 次串行延迟。 改为 Promise.all 并发,用 batchProgress state 驱动 Arco Progress 显示 "已生成 X / N 个令牌",同时 mountedRef 保护 unmount 后不更新 state。 3. 批次内重复节点名前端预提示 spec §6.2 要求"前端去重",此前依赖后端报错。 handleGenerate 前扫描 parseBatchNames 检测批次内重复并 Message.warning。
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Modal, Steps, Button, Space, Message, Spin } from '@arco-design/web-react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Modal, Steps, Button, Space, Message, Spin, Progress } from '@arco-design/web-react'
|
||||
import { Step1NodeName, type Mode } from './wizard/Step1NodeName'
|
||||
import { Step2DeployOptions, type DeployOptions } from './wizard/Step2DeployOptions'
|
||||
import { Step3CommandPreview } from './wizard/Step3CommandPreview'
|
||||
@@ -13,7 +13,8 @@ interface Props {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
masterVersion: string
|
||||
// null = 正在拉取;空字符串 = 拉取失败(Step2 将展示手动输入框而非 Select)
|
||||
masterVersion: string | null
|
||||
// 当从节点列表直接点"生成安装命令"时传入,跳过 Step1
|
||||
fixedNode?: { id: number; name: string }
|
||||
}
|
||||
@@ -24,14 +25,33 @@ export function AgentInstallWizard({ visible, onClose, onSuccess, masterVersion,
|
||||
const [singleName, setSingleName] = useState('')
|
||||
const [batchText, setBatchText] = useState('')
|
||||
|
||||
// 批量进度(已生成 / 总数)
|
||||
const [batchProgress, setBatchProgress] = useState<{ done: number; total: number } | null>(null)
|
||||
|
||||
const [deploy, setDeploy] = useState<DeployOptions>({
|
||||
mode: 'systemd',
|
||||
arch: 'auto',
|
||||
agentVersion: masterVersion,
|
||||
agentVersion: masterVersion || '',
|
||||
downloadSrc: 'github',
|
||||
ttlSeconds: 900,
|
||||
})
|
||||
|
||||
// 当父组件异步拿到 masterVersion 后,同步到 deploy.agentVersion(仅初始为空时)
|
||||
useEffect(() => {
|
||||
if (masterVersion && !deploy.agentVersion) {
|
||||
setDeploy((prev) => ({ ...prev, agentVersion: masterVersion }))
|
||||
}
|
||||
}, [masterVersion]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// unmount 保护:用户关 Modal / 切页时,已发出的请求完成后不再更新 state
|
||||
const mountedRef = useRef(true)
|
||||
useEffect(() => {
|
||||
mountedRef.current = true
|
||||
return () => {
|
||||
mountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [singleToken, setSingleToken] = useState<InstallTokenResult | null>(null)
|
||||
const [singleNodeInfo, setSingleNodeInfo] = useState<{ id: number; name: string } | null>(null)
|
||||
const [batchRows, setBatchRows] = useState<BatchCommandRow[]>([])
|
||||
@@ -45,6 +65,7 @@ export function AgentInstallWizard({ visible, onClose, onSuccess, masterVersion,
|
||||
setSingleToken(null)
|
||||
setSingleNodeInfo(null)
|
||||
setBatchRows([])
|
||||
setBatchProgress(null)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -76,6 +97,24 @@ export function AgentInstallWizard({ visible, onClose, onSuccess, masterVersion,
|
||||
}
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!deploy.agentVersion.trim()) {
|
||||
Message.warning('请填写 Agent 版本号(形如 v1.7.0)')
|
||||
return
|
||||
}
|
||||
// 步骤 1 的批次内去重在前端先提示一次,再由后端最终校验
|
||||
if (mode === 'batch' && !fixedNode) {
|
||||
const names = parseBatchNames()
|
||||
const seen = new Set<string>()
|
||||
const dups: string[] = []
|
||||
for (const n of names) {
|
||||
if (seen.has(n)) dups.push(n)
|
||||
seen.add(n)
|
||||
}
|
||||
if (dups.length > 0) {
|
||||
Message.warning(`批次内有重复节点名:${Array.from(new Set(dups)).join(', ')}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
if (fixedNode) {
|
||||
@@ -103,23 +142,30 @@ export function AgentInstallWizard({ visible, onClose, onSuccess, masterVersion,
|
||||
} 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)
|
||||
setBatchProgress({ done: 0, total: created.length })
|
||||
// 并发生成 install token(Promise.all),每完成一个递增 done 计数
|
||||
let done = 0
|
||||
const tokens = await Promise.all(
|
||||
created.map(async (c) => {
|
||||
const tok = await createInstallToken(c.id, {
|
||||
mode: deploy.mode,
|
||||
arch: deploy.arch,
|
||||
agentVersion: deploy.agentVersion,
|
||||
downloadSrc: deploy.downloadSrc,
|
||||
ttlSeconds: deploy.ttlSeconds,
|
||||
})
|
||||
done += 1
|
||||
if (mountedRef.current) setBatchProgress({ done, total: created.length })
|
||||
return { c, tok }
|
||||
}),
|
||||
)
|
||||
const rows: BatchCommandRow[] = tokens.map(({ c, tok }) => ({
|
||||
nodeId: c.id,
|
||||
nodeName: c.name,
|
||||
command: `curl -fsSL ${tok.url} | sudo sh`,
|
||||
expiresAt: tok.expiresAt,
|
||||
}))
|
||||
if (mountedRef.current) setBatchRows(rows)
|
||||
}
|
||||
setStep(2)
|
||||
onSuccess()
|
||||
@@ -178,6 +224,17 @@ export function AgentInstallWizard({ visible, onClose, onSuccess, masterVersion,
|
||||
{submitting && (
|
||||
<div style={{ textAlign: 'center', padding: 32 }}>
|
||||
<Spin />
|
||||
{batchProgress && (
|
||||
<div style={{ marginTop: 16, maxWidth: 360, marginLeft: 'auto', marginRight: 'auto' }}>
|
||||
<div style={{ fontSize: 13, marginBottom: 6 }}>
|
||||
正在生成安装命令 {batchProgress.done} / {batchProgress.total}
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round((batchProgress.done / batchProgress.total) * 100)}
|
||||
showText
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ export default function NodesPage() {
|
||||
|
||||
const [wizardVisible, setWizardVisible] = useState(false)
|
||||
const [wizardFixedNode, setWizardFixedNode] = useState<{ id: number; name: string } | undefined>()
|
||||
const [masterVersion, setMasterVersion] = useState('latest')
|
||||
// null = 拉取中 / 未知;空字符串 = 拉取失败(UI 将要求用户手动输入版本,避免生成无效 URL)
|
||||
const [masterVersion, setMasterVersion] = useState<string | null>(null)
|
||||
|
||||
const [editVisible, setEditVisible] = useState(false)
|
||||
const [editNode, setEditNode] = useState<NodeSummary | null>(null)
|
||||
@@ -39,10 +40,11 @@ export default function NodesPage() {
|
||||
|
||||
useEffect(() => {
|
||||
fetchNodes()
|
||||
// 取 Master 版本号作为 Wizard agentVersion 默认值
|
||||
// 取 Master 版本号作为 Wizard agentVersion 默认值。
|
||||
// 拉取失败或字段缺失时置为空串,Wizard 会提示用户手动输入。
|
||||
fetchSystemInfo().then((info) => {
|
||||
if (info?.version) setMasterVersion(info.version)
|
||||
}).catch(() => {})
|
||||
setMasterVersion(info?.version || '')
|
||||
}).catch(() => setMasterVersion(''))
|
||||
}, [fetchNodes])
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Form, Radio, Select, Typography } from '@arco-design/web-react'
|
||||
import { Form, Radio, Select, Input, Typography } from '@arco-design/web-react'
|
||||
import type { InstallMode, InstallArch, InstallSource } from '../../../types/nodes'
|
||||
|
||||
const { Text } = Typography
|
||||
@@ -13,13 +13,16 @@ export interface DeployOptions {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
masterVersion: string
|
||||
// null = 拉取中;空串 = 拉取失败(改为手动输入)
|
||||
masterVersion: string | null
|
||||
value: DeployOptions
|
||||
onChange: (v: DeployOptions) => void
|
||||
}
|
||||
|
||||
export function Step2DeployOptions({ masterVersion, value, onChange }: Props) {
|
||||
const update = (patch: Partial<DeployOptions>) => onChange({ ...value, ...patch })
|
||||
const versionKnown = !!masterVersion
|
||||
const versionLoading = masterVersion === null
|
||||
|
||||
return (
|
||||
<Form layout="vertical" size="default">
|
||||
@@ -48,14 +51,32 @@ export function Step2DeployOptions({ masterVersion, value, onChange }: Props) {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Agent 版本">
|
||||
<Select
|
||||
value={value.agentVersion}
|
||||
onChange={(v) => update({ agentVersion: v })}
|
||||
options={[
|
||||
{ label: `${masterVersion}(跟随 Master,推荐)`, value: masterVersion },
|
||||
]}
|
||||
/>
|
||||
<Form.Item
|
||||
label="Agent 版本"
|
||||
extra={
|
||||
!versionKnown && !versionLoading ? (
|
||||
<Text type="warning" style={{ fontSize: 12 }}>
|
||||
未能自动获取 Master 版本,请手动输入(形如 v1.7.0)
|
||||
</Text>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{versionKnown ? (
|
||||
<Select
|
||||
value={value.agentVersion}
|
||||
onChange={(v) => update({ agentVersion: v })}
|
||||
options={[
|
||||
{ label: `${masterVersion}(跟随 Master,推荐)`, value: masterVersion as string },
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
placeholder={versionLoading ? '加载中...' : 'v1.7.0'}
|
||||
value={value.agentVersion}
|
||||
onChange={(v) => update({ agentVersion: v })}
|
||||
disabled={versionLoading}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="安装命令有效期">
|
||||
|
||||
Reference in New Issue
Block a user