Allow forced server deletion

This commit is contained in:
npc0-hue
2026-08-04 15:03:49 +08:00
parent 96a1939f32
commit cf9af14eaf
17 changed files with 236 additions and 24 deletions
+4
View File
@@ -253,8 +253,12 @@ describe("first-party console pages", () => {
const deleteHandlerSource = serversPageSource.split("async function handleDeleteServer")[1]?.split("function openRunTargetSelection")[0] ?? "";
expect(deleteHandlerSource).toContain("deleteServerInstance(deleteConfirmation.serverInstanceId, {");
expect(deleteHandlerSource).toContain("password: deletePassword");
expect(deleteHandlerSource).toContain("force: forceDelete");
expect(deleteHandlerSource).toContain("confirmation: forceDelete ? deleteForceConfirmation : undefined");
expect(serversPageSource).toContain("serverDeleteConfirmation(card.instance)");
expect(serversPageSource).toContain("serverDeleteDisabledReason(session, card.instance)");
expect(serversPageSource).toContain("FORCE DELETE");
expect(serversPageSource).toContain("不会停止远端进程");
expect(serversPageSource).toContain("危险操作");
expect(serversPageSource).toContain("删除服务器");
expect(serversPageSource).toContain("请输入当前登录密码");
+36 -4
View File
@@ -59,6 +59,7 @@ const statusFilters: Array<{ id: ServerStatusFilter; label: string }> = [
const serverListRefreshMs = 5000;
const serverMetricFreshMs = 30000;
const serverForceDeleteConfirmation = "FORCE DELETE";
export function ServersPage({ session, operations, onNavigate }: PageComponentProps) {
const [listState, setListState] = useState<ListState>("loading");
@@ -79,6 +80,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
const [runtimeTaskActions, setRuntimeTaskActions] = useState<RuntimeTaskDialogAction[]>([]);
const [deleteConfirmation, setDeleteConfirmation] = useState<ReturnType<typeof serverDeleteConfirmation> | null>(null);
const [deletePassword, setDeletePassword] = useState("");
const [deleteForceConfirmation, setDeleteForceConfirmation] = useState("");
const [deleteBusy, setDeleteBusy] = useState(false);
const [runTargetSelection, setRunTargetSelection] = useState<RunTargetSelectionState | null>(null);
@@ -210,13 +212,19 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
if (!deleteConfirmation) {
return;
}
const forceDelete = deleteConfirmation.state === "running" || deleteConfirmation.state === "installing";
setDeleteBusy(true);
const operationId = operations.begin({ intent: "删除服务器", targetKind: "server", targetId: deleteConfirmation.serverInstanceId, requester: session.displayName });
try {
await platformApiClient.deleteServerInstance(deleteConfirmation.serverInstanceId, { password: deletePassword });
await platformApiClient.deleteServerInstance(deleteConfirmation.serverInstanceId, {
password: deletePassword,
force: forceDelete,
confirmation: forceDelete ? deleteForceConfirmation : undefined
});
operations.succeed(operationId, `服务器已删除:${deleteConfirmation.serverInstanceId}`);
setDeleteConfirmation(null);
setDeletePassword("");
setDeleteForceConfirmation("");
await refresh();
} catch (error) {
operations.fail(operationId, error instanceof Error ? error.message : "服务器删除失败", operationId);
@@ -611,6 +619,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
onQuickAction={(action) => void handleQuickRuntimeAction(card.instance, action)}
onDelete={() => {
setDeletePassword("");
setDeleteForceConfirmation("");
setDeleteConfirmation(serverDeleteConfirmation(card.instance));
}}
/>
@@ -620,14 +629,15 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
<ConfirmDialog
open={deleteConfirmation !== null}
title="删除服务器"
description={`确认删除 ${deleteConfirmation?.name ?? ""}${deleteConfirmation?.serverInstanceId ?? ""})?运行中或安装中的服务器会被平台拒绝,历史记录会保留。`}
description={deleteDialogDescription(deleteConfirmation)}
confirmLabel="确认删除"
danger
busy={deleteBusy}
confirmDisabled={deletePassword.trim() === ""}
confirmDisabled={deletePassword.trim() === "" || (deleteRequiresForceConfirmation(deleteConfirmation) && deleteForceConfirmation.trim() !== serverForceDeleteConfirmation)}
onCancel={() => {
setDeleteConfirmation(null);
setDeletePassword("");
setDeleteForceConfirmation("");
}}
onConfirm={() => void handleDeleteServer()}
>
@@ -645,6 +655,15 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
onChange={(event) => setDeletePassword(event.target.value)}
/>
</label>
{deleteRequiresForceConfirmation(deleteConfirmation) && (
<label>
/ {serverForceDeleteConfirmation}
<input
value={deleteForceConfirmation}
onChange={(event) => setDeleteForceConfirmation(event.target.value)}
/>
</label>
)}
</ConfirmDialog>
<RuntimeTaskProgressDialog task={runtimeTask.task} onClose={runtimeTask.closeTask} actions={runtimeTaskActions} />
</section>
@@ -677,11 +696,24 @@ function serverDeleteDisabledReason(session: PageComponentProps["session"], inst
return "仅创建人或平台管理员可删除";
}
if (!canDeleteServer(instance.state)) {
return "运行中、安装中或已删除的服务器不能直接删除";
return "已删除的服务器不能再次删除";
}
return "";
}
function deleteRequiresForceConfirmation(confirmation: ReturnType<typeof serverDeleteConfirmation> | null): boolean {
return confirmation?.state === "running" || confirmation?.state === "installing";
}
function deleteDialogDescription(confirmation: ReturnType<typeof serverDeleteConfirmation> | null): string {
const name = confirmation?.name ?? "";
const id = confirmation?.serverInstanceId ?? "";
if (deleteRequiresForceConfirmation(confirmation)) {
return `确认强制删除 ${name}${id})?平台只会把实例标记为已删除并保留历史记录,不会停止可能仍在远端运行的进程。`;
}
return `确认删除 ${name}${id})?历史记录会保留。`;
}
interface ServerCardProps {
card: ServerCardView;
metricsPending: boolean;