feat(rcon): add admin server console
CI / validate (push) Successful in 6m5s
Release / release (push) Successful in 9m56s

This commit is contained in:
dmg
2026-08-07 21:44:00 -04:00
parent e43db34402
commit f9ccfd821d
26 changed files with 2800 additions and 4 deletions
@@ -0,0 +1,23 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
vi.mock("@/app/admin/(console)/rcon/actions", () => ({
executeRconCommand: vi.fn(),
}));
import { RconConsole } from "./rcon-console";
describe("RconConsole", () => {
it("renders labelled keyboard-operable controls without history", () => {
const markup = renderToStaticMarkup(<RconConsole servers={[{ id: "one", name: "Season 4" }]} />);
expect(markup).toContain('for="rcon-console-server"');
expect(markup).toContain('for="rcon-command"');
expect(markup).toContain("Season 4");
expect(markup).toContain("Run command");
expect(markup).not.toContain("Latest response");
});
it("explains when no enabled connection is available", () => {
expect(renderToStaticMarkup(<RconConsole servers={[]} />)).toContain("Enable an RCON connection");
});
});
+39
View File
@@ -0,0 +1,39 @@
"use client";
import { useActionState } from "react";
import { executeRconCommand, type RconCommandState } from "@/app/admin/(console)/rcon/actions";
const initialState: RconCommandState = { status: "idle", message: "", serverId: "" };
type ServerOption = { id: string; name: string };
export function RconConsole({ servers }: { servers: ServerOption[] }) {
const [state, action, pending] = useActionState(executeRconCommand, initialState);
const responseServer = servers.find((server) => server.id === state.serverId);
if (!servers.length) {
return <p className="mt-5 text-sm text-muted">Enable an RCON connection before opening the console.</p>;
}
return (
<form action={action} className="mt-6 space-y-4">
<label className="block font-mono text-[10px] font-bold uppercase tracking-wider" htmlFor="rcon-console-server">
Server
<select className="mt-2 block w-full border border-line bg-canvas px-4 py-3 font-sans text-sm font-normal normal-case" defaultValue={state.serverId || servers[0]?.id} id="rcon-console-server" name="serverId" required>
{servers.map((server) => <option key={server.id} value={server.id}>{server.name}</option>)}
</select>
</label>
<label className="block font-mono text-[10px] font-bold uppercase tracking-wider" htmlFor="rcon-command">
Command
<input autoComplete="off" className="mt-2 block w-full border border-line bg-canvas px-4 py-3 font-mono text-sm outline-none focus:border-accent" id="rcon-command" maxLength={1024} name="command" placeholder="list" required spellCheck={false} />
</label>
<button className="border border-ink bg-ink px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas disabled:cursor-wait disabled:opacity-60" disabled={pending} type="submit">{pending ? "Running…" : "Run command"}</button>
{state.status !== "idle" && (
<div aria-live="polite" className={`border-l-2 bg-canvas p-4 ${state.status === "error" ? "border-accent" : "border-signal"}`} role={state.status === "error" ? "alert" : "status"}>
<p className="font-mono text-[9px] font-bold uppercase tracking-wider text-muted">Latest response{responseServer ? `${responseServer.name}` : ""}</p>
<pre className="mt-2 max-h-80 overflow-auto whitespace-pre-wrap break-words font-mono text-xs leading-5">{state.message}</pre>
</div>
)}
</form>
);
}