Allow forced server deletion
This commit is contained in:
@@ -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("请输入当前登录密码");
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user