feat: reveal saved server deployment inputs
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user