113 lines
4.2 KiB
TypeScript
113 lines
4.2 KiB
TypeScript
import { useParams } from "react-router-dom";
|
|
import { Users } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { BackLink } from "@/components/BackLink";
|
|
import { PhaseBadge, shownPhase, startFailure } from "@/components/PhaseBadge";
|
|
import { Loading, ErrorState, NotYours, NotRunning, RefreshError } from "@/components/States";
|
|
import { PageHeader } from "@/components/PageHeader";
|
|
import { OnlineSection } from "@/components/players/OnlineSection";
|
|
import { WhitelistSection } from "@/components/players/WhitelistSection";
|
|
import { BansSection } from "@/components/players/BansSection";
|
|
import { api } from "@/lib/api";
|
|
import { STATUS_POLL_SLOW_MS, STATUS_POLL_FAST_MS, useAsync, usePolling } from "@/lib/hooks";
|
|
import { useTier } from "@/lib/tier";
|
|
import { canManage, ownershipPending } from "@/lib/ownership";
|
|
|
|
/** ServerPlayers is the per-server player-management subpage (/servers/:name/players):
|
|
* whitelist today, online roster and bans as they land. It owns its own gating —
|
|
* ownership (from /me/servers, since GET status never carries `owned`) and server
|
|
* readiness — because as a route it can be reached directly, not just from a link. */
|
|
export function ServerPlayers() {
|
|
const { name = "" } = useParams();
|
|
const { t } = useTranslation("servers");
|
|
const { isAdmin, loading: tierLoading } = useTier();
|
|
const { data, error, loading, reload } = useAsync(() => api.status(name), [name]);
|
|
// The poll is what switches this page over once a wake lands, and back to
|
|
// NotRunning when the server stops (idle stop, the console, a player's timeout).
|
|
usePolling(reload, data && shownPhase(data) === "Running" ? STATUS_POLL_SLOW_MS : STATUS_POLL_FAST_MS);
|
|
const {
|
|
data: mine,
|
|
error: mineError,
|
|
reload: reloadMine,
|
|
} = useAsync(
|
|
() => (isAdmin ? Promise.resolve([]) : api.myServers()),
|
|
[isAdmin, name],
|
|
);
|
|
|
|
const back = (
|
|
<BackLink to={`/servers/${name}`} label={t("players_back_to_console")} />
|
|
);
|
|
|
|
if (loading && !data) {
|
|
return (
|
|
<>
|
|
{back}
|
|
<Loading />
|
|
</>
|
|
);
|
|
}
|
|
if (error && !data) {
|
|
return (
|
|
<>
|
|
{back}
|
|
<ErrorState error={error} onRetry={reload} />
|
|
</>
|
|
);
|
|
}
|
|
if (!data) return back;
|
|
|
|
// Ownership is still resolving (tier fetch, or /me/servers for a non-admin). We
|
|
// have the server's identity, so show its header with a spinner beneath it rather
|
|
// than flashing the whole management surface at someone who may not own it. If the
|
|
// /me/servers read itself failed, `mineError` breaks the pending state (below) so a
|
|
// real owner sees a retry instead of an eternal spinner — never fail closed to
|
|
// NotYours, which would wrongly tell an owner the server isn't theirs on a blip.
|
|
const pending = ownershipPending(tierLoading, isAdmin, mine, mineError);
|
|
const owned = canManage(isAdmin, mine, name);
|
|
const phase = shownPhase(data);
|
|
const failure = startFailure(data);
|
|
|
|
const header = (
|
|
<PageHeader
|
|
icon={Users}
|
|
title={data.displayName || data.name}
|
|
subtitle={t("players_title")}
|
|
actions={<PhaseBadge phase={phase} failure={failure} autoRestarts={data.autoRestarts} />}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{back}
|
|
{header}
|
|
{error && <RefreshError error={error} className="mb-4" />}
|
|
{pending ? (
|
|
<Loading />
|
|
) : mineError ? (
|
|
<ErrorState error={mineError} onRetry={reloadMine} />
|
|
) : !owned ? (
|
|
<NotYours title={t("players_not_yours_title")} body={t("players_not_yours_body")} />
|
|
) : phase !== "Running" ? (
|
|
<NotRunning
|
|
title={t("players_not_running_title")}
|
|
body={t("players_not_running_body")}
|
|
serverName={name}
|
|
phase={data.phase}
|
|
desiredState={data.desiredState}
|
|
failure={failure}
|
|
autoRestarts={data.autoRestarts}
|
|
onWoken={reload}
|
|
/>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{/* Ordered as a who-may-be-here gradient: who is on right now → who may
|
|
join → who may NOT. Each collapses to an index row (shared.tsx). */}
|
|
<OnlineSection name={name} />
|
|
<WhitelistSection name={name} />
|
|
<BansSection name={name} />
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|