@@ -1,7 +1,7 @@
import { CheckCircle2 , CircleDashed , Compass , Download , FolderCog , HeartPulse , Rocket , ScanSearch , ServerCog , SlidersHorizontal } from "lucide-react" ;
import { type ChangeEvent , type FormEvent , useEffect , useMemo , useState } from "react" ;
import type { GamePluginResponse , RunEndpointResponse , ServerDeploymentResponse } from "../api/types" ;
import type { GamePluginResponse , RunEndpointResponse , ServerDeploymentResponse , ServerDeploymentRevealResponse } from "../api/types" ;
import { ManagementDialog } from "./OperationControls" ;
import { endpointLabel , pluginCreateInputDefaults , pluginLabel , runtimeBindingFields , type ServerCreateFormState } from "../contracts/serverManagement" ;
import { cx } from "../utils/classes" ;
@@ -16,14 +16,17 @@ interface ServerDeploymentWorkflowProps {
initialForm : ServerCreateFormState ;
deployment? : ServerDeploymentResponse ;
busy? : boolean ;
onReveal ? : ( ) = > Promise < ServerDeploymentRevealResponse > ;
onClose : ( ) = > void ;
onSubmit : ( form : ServerCreateFormState , saveAsDraft : boolean ) = > Promise < void > ;
}
export function ServerDeploymentWorkflow ( { open , kind , plugins , endpoints , initialForm , deployment , busy = false , onClose , onSubmit } : ServerDeploymentWorkflowProps ) {
export function ServerDeploymentWorkflow ( { open , kind , plugins , endpoints , initialForm , deployment , busy = false , onReveal , onClose , onSubmit } : ServerDeploymentWorkflowProps ) {
const [ step , setStep ] = useState ( 0 ) ;
const [ form , setForm ] = useState < ServerCreateFormState > ( initialForm ) ;
const [ saveAsDraft , setSaveAsDraft ] = useState ( false ) ;
const [ revealBusy , setRevealBusy ] = useState ( false ) ;
const [ revealError , setRevealError ] = useState ( "" ) ;
const selectedPlugin = useMemo ( ( ) = > plugins . find ( ( plugin ) = > plugin . id === form . pluginId ) , [ form . pluginId , plugins ] ) ;
const profileOptions = selectedPlugin ? . runtimeProfiles ? . lifecycleProfiles ? ? [ ] ;
const pluginFields = selectedPlugin ? . createFields ? ? [ ] ;
@@ -47,6 +50,8 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
setStep ( 0 ) ;
setSaveAsDraft ( kind === "create" && ! initialForm . runEndpointId ) ;
setForm ( initialForm ) ;
setRevealBusy ( false ) ;
setRevealError ( "" ) ;
} , [ initialForm , kind , open ] ) ;
function updateForm ( event : ChangeEvent < HTMLInputElement | HTMLSelectElement > ) {
@@ -82,10 +87,31 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
await onSubmit ( { . . . form , deploymentTargetId : saveAsDraft ? "" : form . deploymentTargetId , runEndpointId : saveAsDraft ? "" : form . runEndpointId } , saveAsDraft ) ;
}
async function revealSavedInputs() {
if ( ! onReveal || revealBusy ) return ;
setRevealBusy ( true ) ;
setRevealError ( "" ) ;
try {
const revealed = await onReveal ( ) ;
setForm ( ( current ) = > ( { . . . current , serverRoot : revealed.serverRoot , workingDirectory : revealed.workingDirectory , installCommand : revealed.installCommand , startCommand : revealed.startCommand , stopCommand : revealed.stopCommand , statusCommand : revealed.statusCommand } ) ) ;
} catch ( error ) {
setRevealError ( error instanceof Error ? error . message : "无法显示已保存配置" ) ;
} finally {
setRevealBusy ( false ) ;
}
}
function closeWorkflow() {
if ( busy ) return ;
setForm ( initialForm ) ;
setRevealError ( "" ) ;
onClose ( ) ;
}
const protectedState = ( nextValue : string , configured : boolean ) = > nextValue . trim ( ) ? "将替换" : configured ? "保持已配置" : "未配置" ;
const actionLabel = kind === "create" ? "保存草稿并准备专属 Run" : "保存部署设置" ;
return < ManagementDialog open = { open } title = { kind === "create" ? "创建服务器" : "编辑部署" } description = { kind === "create" ? "按部署顺序完成设置;路径和命令始终受保护,不会在确认页或日志中回显。" : "仅停止中的服务器可以修改部署设置。受保护路径和命令留空会保持原值。" } wide onClose = { ( ) = > { if ( ! busy ) onClose ( ) ; } } >
return < ManagementDialog open = { open } title = { kind === "create" ? "创建服务器" : "编辑部署" } description = { kind === "create" ? "按部署顺序完成设置;路径和命令始终受保护,不会在确认页或日志中回显。" : "仅停止中的服务器可以修改部署设置。受保护路径和命令留空会保持原值;可主动显示已保存配置 。" } wide onClose = { closeWorkflow } >
< form className = "provider-form dialog-form server-deployment-workflow" onSubmit = { ( event ) = > void submit ( event ) } aria-label = { kind === "create" ? "创建服务器部署向导" : "编辑服务器部署向导" } >
< ol className = "deployment-workflow-steps" style = { { gridTemplateColumns : ` repeat( ${ workflowSteps . length } , minmax(0, 1fr)) ` } } aria-label = "部署步骤" > { workflowSteps . map ( ( item , index ) = > { const Icon = item . icon ; return < li key = { item . label } className = { cx ( index === step && "deployment-workflow-step-active" , index < step && "deployment-workflow-step-complete" ) } > < span > { index < step ? < CheckCircle2 size = { 15 } / > : < Icon size = { 15 } / > } < / span > < strong > { index + 1 } . { item . label } < / strong > < / li > ; } ) } < / ol >
{ step === targetStep && < div className = "deployment-workflow-body" >
@@ -98,7 +124,7 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
< ModeOption active = { form . deploymentMode === "existing-server" } title = "接管已有服务器" copy = "预检指定目录并接入已有实例;不会把它当作一次新安装。" onClick = { ( ) = > setForm ( ( current ) = > ( { . . . current , deploymentMode : "existing-server" } ) ) } / >
< ModeOption active = { form . deploymentMode === "custom-command" } title = "自定义启动方式" copy = "用于非标准启动器或脚本;需由节点策略允许。" onClick = { ( ) = > setForm ( ( current ) = > ( { . . . current , deploymentMode : "custom-command" } ) ) } / >
< / div > < / div > }
{ step === configurationStep && < div className = "deployment-workflow-body" > < div className = "form-grid" >
{ step === configurationStep && < div className = "deployment-workflow-body" > { kind === "edit" && onReveal && < div className = "form-guidance" > < strong > 需 要 核 对 原 配 置 ? < / strong > < span > 仅 本 次 编 辑 窗 口 内 显 示 已 保 存 的 目 录 和 命 令 , 关 闭 后 会 清 除 。 < / span > < button type = "button" className = "primary-command" disabled = { busy || revealBusy } onClick = { ( ) = > void revealSavedInputs ( ) } > { revealBusy ? "读取中…" : "显示已保存配置" } < / button > { revealError && < span className = "field-help" > { revealError } < / span > } < / div > } < div className = "form-grid" >
{ kind === "create" && < label > 服 务 器 名 称 < input name = "name" value = { form . name } onChange = { updateForm } placeholder = "Example Survival #3" required / > < / label > }
{ kind === "create" && < label > 运 行 预 设 ( 可 选 ) < select name = "profileKey" value = { form . profileKey } onChange = { updateForm } > < option value = "" > 使 用 插 件 默 认 预 设 < / option > { profileOptions . map ( ( profile ) = > < option key = { profile . key } value = { profile . key } > { profile . key } · { profile . mode } < / option > ) } < / select > < small className = "field-help" > 仅 在 插 件 提 供 多 个 兼 容 启 动 预 设 时 选 择 。 < / small > < / label > }
{ kind === "edit" && < label > 部 署 方 式 < select name = "deploymentMode" value = { form . deploymentMode } onChange = { updateForm } > < option value = "guided-install" > 新 建 并 安 装 < / option > < option value = "existing-server" > 接 管 已 有 服 务 器 < / option > < option value = "custom-command" > 自 定 义 启 动 方 式 < / option > < / select > < small className = "field-help" > 可 在 此 调 整 部 署 方 式 ; 不 会 重 复 要 求 选 择 已 绑 定 的 运 行 节 点 。 < / small > < / label > }
@@ -118,8 +144,8 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
{ form . deploymentMode === "custom-command" && < details className = "provider-advanced-settings" open > < summary > 高 级 启 动 设 置 < / summary > < p className = "field-help" > 只 有 自 定 义 启 动 器 需 要 这 些 设 置 。 执 行 目 录 留 空 时 , 节 点 以 服 务 器 目 录 执 行 。 < / p > < div className = "form-grid" > < label > 启 动 命 令 < input name = "startCommand" value = { form . startCommand } onChange = { updateForm } placeholder = { deployment ? . startCommandConfigured ? "留空保持已配置启动命令" : "必填,例如 ./start-server" } autoComplete = "off" required = { ! deployment ? . startCommandConfigured } / > < / label > < label > 命 令 解 释 器 < select name = "shell" value = { form . shell } onChange = { updateForm } > < option value = "" > 直 接 argv ( 推 荐 ) < / option > < option value = "posix-sh" > POSIX sh < / option > < option value = "powershell" > PowerShell < / option > < option value = "cmd" > Windows cmd < / option > < / select > < / label > < label > 执 行 目 录 ( 可 选 ) < input name = "workingDirectory" value = { form . workingDirectory } onChange = { updateForm } placeholder = { deployment ? . workingDirectoryConfigured ? "留空保持已配置执行目录" : "默认使用服务器目录" } autoComplete = "off" / > < / label > < label > 安 装 命 令 ( 可 选 ) < input name = "installCommand" value = { form . installCommand } onChange = { updateForm } autoComplete = "off" placeholder = "留空保持原值或不使用" / > < / label > < label > 停 止 命 令 ( 可 选 ) < input name = "stopCommand" value = { form . stopCommand } onChange = { updateForm } autoComplete = "off" / > < / label > < label > 状 态 命 令 ( 可 选 ) < input name = "statusCommand" value = { form . statusCommand } onChange = { updateForm } autoComplete = "off" / > < / label > < / div > < / details > }
{ kind === "create" && bindingFields . length > 0 && < details className = "provider-advanced-settings" > < summary > 运 行 连 接 设 置 < / summary > < p className = "field-help" > 用 于 插 件 声 明 的 逻 辑 连 接 , 不 是 服 务 器 目 录 或 游 戏 配 置 。 < / p > < div className = "form-grid" > { bindingFields . map ( ( field ) = > < label key = { field . key } > { field . key } { field . required ? "(必填)" : "" } < input type = { field . sensitive ? "password" : "text" } autoComplete = "off" value = { form . bindings [ field . key ] ? ? "" } onChange = { ( event ) = > updateBinding ( field . key , event . target . value ) } placeholder = { field . sensitive ? "托管凭据引用" : "安全逻辑值" } required = { field . required } / > < / label > ) } < / div > < / details > }
< / div > }
{ step === reviewStep && < div className = "deployment-workflow-body" > < div className = "deployment-review" > < div > < span > 插 件 类 型 < / span > < strong > { pluginLabel ( selectedPlugin , form . pluginId ) } < / strong > < / div > < div > < span > { kind === "create" ? "部署目标" : "目标" } < / span > < strong > { saveAsDraft ? "保存为未指定目标的草稿" : endpointLabel ( endpoints . find ( ( endpoint ) = > endpoint . id === selectedTargetID ) , selectedTargetID ) } < / strong > < / div > < div > < span > 部 署 方 式 < / span > < strong > { form . deploymentMode === "guided-install" ? "新建并安装" : form . deploymentMode === "existing-server" ? "接管已有服务器" : "自定义启动方式" } < / strong > < / div > < div > < span > { form . deploymentMode === "guided-install" ? "安装目录" : form . deploymentMode === "existing-server" ? "已有服务器目录" : "服务器目录" } < / span > < strong > { protectedState ( form . serverRoot , Boolean ( deployment ? . serverRootConfigured ) ) } < / strong > < / div > { form . deploymentMode === "custom-command" && < > < div > < span > 启 动 命 令 < / span > < strong > { protectedState ( form . startCommand , Boolean ( deployment ? . startCommandConfigured ) ) } < / strong > < / div > < div > < span > 执 行 目 录 < / span > < strong > { protectedState ( form . workingDirectory , Boolean ( deployment ? . workingDirectoryConfigured ) ) } < / strong > < / div > < / > } { form . deploymentMode === "guided-install" && < div > < span > 游 戏 配 置 < / span > < strong > { Object . keys ( form . createInputs ) . length ? ` ${ Object . keys ( form . createInputs ) . length } 项已准备 ` : "使用插件默认值" } < / strong > < / div > } { isScum && < div > < span > 完 成 条 件 < / span > < strong > 安 装 / 扫 描 、 映 射 、 验 证 全 部 通 过 < / strong > < / div > } < / div > < div className = "form-guidance" > < strong > { kind === "create" ? "本次保存草稿并保留专属 Run 身份 " : activeServer ? "本次只保存部署设置" : "本次只保存部署设置" } < / strong > < span > { kind === "create" ? "随后生成并启动专属 Run;它完成注册后,才能明确发起部署。" : form . deploymentMode === "existing-server" ? "Run 将先预检现有目录;不会重装或覆盖已有游戏配置。" : "保存后可在详情中明确发起部署;路径和命令不会显示原文 。" } < / span > < / div > < / div > }
< div className = "confirm-actions" > < button type = "button" disabled = { busy } onClick = { ( ) = > step === 0 ? onClose ( ) : setStep ( ( current ) = > current - 1 ) } > { step === 0 ? "取消" : "上一步" } < / button > { step < reviewStep ? < button type = "submit" className = "confirm-primary" disabled = { busy || ! canContinue ( ) } > < CircleDashed size = { 16 } / > < span > 下 一 步 < / span > < / button > : < button type = "submit" className = "confirm-primary" disabled = { busy } > < Rocket size = { 16 } / > < span > { busy ? "保存中…" : actionLabel } < / span > < / button > } < / div >
{ step === reviewStep && < div className = "deployment-workflow-body" > < div className = "deployment-review" > < div > < span > 插 件 类 型 < / span > < strong > { pluginLabel ( selectedPlugin , form . pluginId ) } < / strong > < / div > < div > < span > { kind === "create" ? "部署目标" : "目标" } < / span > < strong > { saveAsDraft ? "保存为未指定目标的草稿" : endpointLabel ( endpoints . find ( ( endpoint ) = > endpoint . id === selectedTargetID ) , selectedTargetID ) } < / strong > < / div > < div > < span > 部 署 方 式 < / span > < strong > { form . deploymentMode === "guided-install" ? "新建并安装" : form . deploymentMode === "existing-server" ? "接管已有服务器" : "自定义启动方式" } < / strong > < / div > < div > < span > { form . deploymentMode === "guided-install" ? "安装目录" : form . deploymentMode === "existing-server" ? "已有服务器目录" : "服务器目录" } < / span > < strong > { protectedState ( form . serverRoot , Boolean ( deployment ? . serverRootConfigured ) ) } < / strong > < / div > { form . deploymentMode === "custom-command" && < > < div > < span > 启 动 命 令 < / span > < strong > { protectedState ( form . startCommand , Boolean ( deployment ? . startCommandConfigured ) ) } < / strong > < / div > < div > < span > 执 行 目 录 < / span > < strong > { protectedState ( form . workingDirectory , Boolean ( deployment ? . workingDirectoryConfigured ) ) } < / strong > < / div > < / > } { form . deploymentMode === "guided-install" && < div > < span > 游 戏 配 置 < / span > < strong > { Object . keys ( form . createInputs ) . length ? ` ${ Object . keys ( form . createInputs ) . length } 项已准备 ` : "使用插件默认值" } < / strong > < / div > } { isScum && < div > < span > 完 成 条 件 < / span > < strong > 安 装 / 扫 描 、 映 射 、 验 证 全 部 通 过 < / strong > < / div > } < / div > < div className = "form-guidance" > < strong > { kind === "create" ? "本次保存草稿并保留专属 Run" : activeServer ? "本次只保存部署设置" : "本次只保存部署设置" } < / strong > < span > { kind === "create" ? "随后生成并启动专属 Run;它完成注册后,才能明确发起部署。" : form . deploymentMode === "existing-server" ? "Run 将先预检现有目录;不会重装或覆盖已有游戏配置。" : "保存后可在详情中明确发起部署;路径和命令仅在本次显式展示后可见 。" } < / span > < / div > < / div > }
< div className = "confirm-actions" > < button type = "button" disabled = { busy } onClick = { ( ) = > step === 0 ? closeWorkflow ( ) : setStep ( ( current ) = > current - 1 ) } > { step === 0 ? "取消" : "上一步" } < / button > { step < reviewStep ? < button type = "submit" className = "confirm-primary" disabled = { busy || ! canContinue ( ) } > < CircleDashed size = { 16 } / > < span > 下 一 步 < / span > < / button > : < button type = "submit" className = "confirm-primary" disabled = { busy } > < Rocket size = { 16 } / > < span > { busy ? "保存中…" : actionLabel } < / span > < / button > } < / div >
< / form >
< / ManagementDialog > ;
}