feat(rcon): add admin server console
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user