Unverified Commit 3a2166ca authored by Lemon-miaow's avatar Lemon-miaow
Browse files

fix(panel): 启停按钮统一为带忙碌态和失败原因的组件,有玩家在线或人数未知时停服先确认

parent bcf24541
Loading
Loading
Loading
Loading
+107 −0
Changes for panel/src/components/PowerButton.tsx: 107 added lines, 0 removed lines.
Original line number Diff line number Diff line
import { useState } from "react";
import { Loader2, Play, Square } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { api, humanizeError } from "@/lib/api";
import { cn } from "@/lib/utils";

interface Props {
  name: string;
  /** A pod is up or on its way (Starting/Running/Stopping): offer Stop. */
  live: boolean;
  playersOnline?: number;
  /** The operator cannot read the player count, so players may be online. */
  playerCountUnknown?: boolean;
  /** Called after the wake or stop was accepted, so the parent refetches. */
  onChanged: () => void;
  size?: "sm" | "default";
  className?: string;
}

// PowerButton starts or stops one server. It is busy while the call runs (no
// double send), shows why a call was refused (quota, cooldown, a phase that
// moved on), and asks before a stop that would disconnect players: the count
// is in the question, and an unreadable count asks too.
export function PowerButton({
  name,
  live,
  playersOnline,
  playerCountUnknown,
  onChanged,
  size = "sm",
  className,
}: Props) {
  const { t } = useTranslation("servers");
  const [busy, setBusy] = useState(false);
  const [confirming, setConfirming] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function run(kind: "wake" | "stop") {
    if (busy) return;
    setBusy(true);
    setError(null);
    try {
      await (kind === "wake" ? api.wake(name) : api.stop(name));
      setConfirming(false);
      onChanged();
    } catch (e) {
      setError(humanizeError(e));
    } finally {
      setBusy(false);
    }
  }

  const players = playersOnline ?? 0;
  const askFirst = players > 0 || playerCountUnknown === true;
  const spinner = <Loader2 className="animate-spin" />;

  let control;
  if (!live) {
    control = (
      <Button size={size} onClick={() => void run("wake")} disabled={busy}>
        {busy ? spinner : <Play />}
        {busy ? t("waking") : t("wake")}
      </Button>
    );
  } else if (confirming) {
    control = (
      <div role="alert" className="flex flex-wrap items-center justify-end gap-2">
        <span className="text-xs font-medium text-foreground">
          {players > 0
            ? t("stop_confirm_players", { count: players })
            : t("stop_confirm_unknown")}
        </span>
        <Button size={size} variant="ghost" onClick={() => setConfirming(false)} disabled={busy}>
          {t("common:cancel")}
        </Button>
        <Button size={size} variant="destructive" onClick={() => void run("stop")} disabled={busy}>
          {busy ? spinner : <Square />}
          {busy ? t("stopping") : t("stop")}
        </Button>
      </div>
    );
  } else {
    control = (
      <Button
        size={size}
        variant="destructive"
        onClick={() => (askFirst ? setConfirming(true) : void run("stop"))}
        disabled={busy}
      >
        {busy ? spinner : <Square />}
        {busy ? t("stopping") : t("stop")}
      </Button>
    );
  }

  return (
    <div className={cn("flex flex-col items-end gap-1", className)}>
      {control}
      {error && (
        <p role="alert" className="max-w-xs text-right text-xs text-destructive">
          {error}
        </p>
      )}
    </div>
  );
}
+13 −23
Changes for panel/src/components/ServerCard.tsx: 13 added lines, 23 removed lines.
Original line number Diff line number Diff line
import { useState } from "react";
import { Link } from "react-router-dom";
import { Play, Square, Hand, Users, ExternalLink, Terminal } from "lucide-react";
import { Hand, Users, ExternalLink, Terminal } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { PhaseBadge } from "@/components/PhaseBadge";
import { PowerButton } from "@/components/PowerButton";
import { api, humanizeError } from "@/lib/api";
import { hostFor, type RuntimeConfig } from "@/lib/config";
import type { ServerInfo } from "@/lib/types";
@@ -18,11 +19,11 @@ interface Props {

export function ServerCard({ server, cfg, onChanged }: Props) {
  const { t } = useTranslation("servers");
  const [busy, setBusy] = useState<null | "wake" | "stop" | "claim">(null);
  const [busy, setBusy] = useState<null | "claim">(null);
  const [error, setError] = useState<string | null>(null);
  const host = hostFor(server.subdomain, cfg);

  async function act(kind: "wake" | "stop" | "claim", fn: () => Promise<unknown>) {
  async function act(kind: "claim", fn: () => Promise<unknown>) {
    setBusy(kind);
    setError(null);
    try {
@@ -93,26 +94,15 @@ export function ServerCard({ server, cfg, onChanged }: Props) {
            <Hand /> {busy === "claim" ? t("claiming") : t("claim")}
          </Button>
        )}
        {server.owned &&
          (running || transitioning ? (
            <Button
              size="sm"
              variant="destructive"
              disabled={busy !== null}
              onClick={() => act("stop", () => api.stop(server.name))}
            >
              <Square /> {busy === "stop" ? t("stopping") : t("stop")}
            </Button>
          ) : (
            <Button
              size="sm"
              variant="default"
              disabled={busy !== null}
              onClick={() => act("wake", () => api.wake(server.name))}
            >
              <Play /> {busy === "wake" ? t("waking") : t("wake")}
            </Button>
          ))}
        {server.owned && (
          <PowerButton
            name={server.name}
            live={running || transitioning}
            playersOnline={server.playersOnline}
            playerCountUnknown={server.playerCountUnknown}
            onChanged={onChanged}
          />
        )}
      </div>
    </Card>
  );
+7 −7
Changes for panel/src/components/States.tsx: 7 added lines, 7 removed lines.
Original line number Diff line number Diff line
import { Loader2, AlertTriangle, Inbox, ShieldX, Construction, Moon, SearchX } from "lucide-react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { PowerButton } from "@/components/PowerButton";
import { humanizeError } from "@/lib/api";

export function Loading({ label }: { label?: string }) {
@@ -136,11 +136,13 @@ export function NotYours({ title, body, linkTo = "/servers", linkLabel }: NotYou
export interface NotRunningProps {
  title: string;
  body: string;
  wakeLabel: string;
  onWake: () => void;
  /** The server to wake from here. */
  serverName: string;
  /** Called once the wake was accepted, so the page refetches its status. */
  onWoken: () => void;
}

export function NotRunning({ title, body, wakeLabel, onWake }: NotRunningProps) {
export function NotRunning({ title, body, serverName, onWoken }: NotRunningProps) {
  return (
    <div className="flex flex-col items-center justify-center gap-4 rounded-lg border border-dashed border-border bg-muted/20 py-16 text-center">
      <Moon className="h-8 w-8 text-muted-foreground/70" />
@@ -148,9 +150,7 @@ export function NotRunning({ title, body, wakeLabel, onWake }: NotRunningProps)
        <p className="font-medium">{title}</p>
        <p className="mx-auto mt-1 max-w-sm text-sm text-muted-foreground">{body}</p>
      </div>
      <Button size="sm" onClick={onWake}>
        {wakeLabel}
      </Button>
      <PowerButton name={serverName} live={false} onChanged={onWoken} className="items-center" />
    </div>
  );
}
+4 −1
Changes for panel/src/i18n/resources/en-US/servers.json: 4 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -178,5 +178,8 @@
  "idle_stop_never": "Never",
  "idle_stop_seconds": "{{count}} s",
  "idle_stop_minutes": "{{count}} min",
  "idle_stop_hours": "{{count}} h"
  "idle_stop_hours": "{{count}} h",
  "stop_confirm_players_one": "1 player is online and will be disconnected. Stop anyway?",
  "stop_confirm_players_other": "{{count}} players are online and will be disconnected. Stop anyway?",
  "stop_confirm_unknown": "The player count can't be read, so players may be online. Stop anyway?"
}
+3 −1
Changes for panel/src/i18n/resources/zh-CN/servers.json: 3 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -178,5 +178,7 @@
  "idle_stop_never": "从不",
  "idle_stop_seconds": "{{count}} 秒",
  "idle_stop_minutes": "{{count}} 分钟",
  "idle_stop_hours": "{{count}} 小时"
  "idle_stop_hours": "{{count}} 小时",
  "stop_confirm_players": "{{count}} 名玩家在线,停服会断开他们。确定停止?",
  "stop_confirm_unknown": "读不到在线人数,可能有玩家在线。确定停止?"
}
Loading