diff --git a/panel/src/components/PowerButton.test.tsx b/panel/src/components/PowerButton.test.tsx index 44d9fda..82e7052 100644 --- a/panel/src/components/PowerButton.test.tsx +++ b/panel/src/components/PowerButton.test.tsx @@ -273,3 +273,48 @@ describe("PowerButton on a server given up or being deleted", () => { expect(screen.getByText(t("servers:retiring_badge_delete"))).toBeTruthy(); }); }); + +describe("PowerButton on a page that needs the server down", () => { + const cases: { + phase: Phase; + desiredState?: "Running" | "Stopped"; + failed?: boolean; + shows: string; + disabled: boolean; + }[] = [ + { phase: "Running", desiredState: "Running", shows: "servers:stop", disabled: false }, + // A failed start gets Stop alone: a retry would start what the page waits to see down. + { phase: "Failed", desiredState: "Running", failed: true, shows: "servers:stop", disabled: false }, + // Woken while going down: Stop keeps it down. + { phase: "Stopping", desiredState: "Running", shows: "servers:stop", disabled: false }, + // A phase nobody can place offers Stop, never a wake. + { phase: "Unknown", desiredState: "Stopped", shows: "servers:stop", disabled: false }, + { phase: "Unknown", shows: "servers:stop", disabled: false }, + { phase: "Failed", desiredState: "Stopped", shows: "servers:stop", disabled: false }, + // Already on its way down: nothing to press. + { phase: "Stopping", desiredState: "Stopped", shows: "servers:stopping", disabled: true }, + { phase: "Running", desiredState: "Stopped", shows: "servers:stopping", disabled: true }, + ]; + + for (const c of cases) { + it(`${c.phase} asked ${c.desiredState ?? "(unknown)"}${c.failed ? " (failed)" : ""} offers ${c.shows}`, () => { + render( + , + ); + const buttons = screen.getAllByRole("button"); + expect(buttons).toHaveLength(1); + expect(buttons[0].textContent).toBe(t(c.shows)); + expect(buttons[0]).toHaveProperty("disabled", c.disabled); + }); + } + + it("still asks before disconnecting players", async () => { + stop.mockResolvedValue(undefined); + render(); + + await userEvent.click(screen.getByRole("button", { name: t("servers:stop") })); + + expect(stop).not.toHaveBeenCalled(); + expect(screen.getByText(t("servers:stop_confirm_players", { count: 2 }))).toBeTruthy(); + }); +}); diff --git a/panel/src/components/PowerButton.tsx b/panel/src/components/PowerButton.tsx index eb25ead..b0c0446 100644 --- a/panel/src/components/PowerButton.tsx +++ b/panel/src/components/PowerButton.tsx @@ -29,6 +29,10 @@ interface Props { /** A pending retirement: nothing starts the server until it is cancelled, so * the control gives way to a badge saying why. */ retiring?: RetireState; + /** Offer only ways down: a page that needs the server stopped (the file + * editor) never offers a wake or a retry, and a server it cannot place gets + * a Stop too. */ + stopOnly?: boolean; /** Called after the wake or stop was accepted, so the parent refetches. */ onChanged: () => void; size?: "sm" | "default"; @@ -54,6 +58,8 @@ function isUp(phase: Phase): boolean { // one asked to run with no pod yet offers Stop, which is the way out when it // never comes up. // A server given up or being deleted offers nothing to press, only why. +// The file editor passes stopOnly: it needs the server down, so it offers Stop +// alone, with the same question before disconnecting players. export function PowerButton({ name, phase, @@ -62,6 +68,7 @@ export function PowerButton({ playersOnline, playerCountUnknown, retiring, + stopOnly = false, onChanged, size = "sm", className, @@ -135,6 +142,8 @@ export function PowerButton({ control = ; } else if (submitted !== null) { control = inProgress(submitted); + } else if (failed && stopOnly) { + control = stopButton("outline", () => void run("stop")); } else if (failed) { control = (
@@ -147,10 +156,10 @@ export function PowerButton({ ); } else if (pending === "stop" || (phase === "Stopping" && pending === null)) { control = inProgress("stop"); - } else if (pending === "start" && phase === "Stopping") { + } else if (!stopOnly && pending === "start" && phase === "Stopping") { // Woken while stopping: the operator brings it back up once it is down. control = inProgress("wake"); - } else if (!on) { + } else if (!stopOnly && !on) { control = (
- + ) : listLoading && entries === null ? (