feat(rcon): refine server console
This commit is contained in:
@@ -60,7 +60,7 @@ export default async function RconPage({
|
||||
<header className="border-b border-line pb-8">
|
||||
<p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">Server operations</p>
|
||||
<h1 className="mt-4 font-display text-5xl font-black uppercase sm:text-7xl">RCON</h1>
|
||||
<p className="mt-5 max-w-2xl text-sm leading-6 text-muted">Run commands through the portal backend. RCON endpoints remain internal and credentials are never sent to the browser.</p>
|
||||
<p className="mt-5 max-w-2xl text-sm leading-6 text-muted">Run commands through the portal backend to internal or external server addresses. Credentials are never sent to the browser.</p>
|
||||
</header>
|
||||
|
||||
{saved && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 font-mono text-xs font-bold uppercase tracking-wider" role="status">{savedMessages[saved] ?? "RCON settings saved."}</p>}
|
||||
@@ -139,7 +139,7 @@ function ConnectionFields({
|
||||
return (
|
||||
<>
|
||||
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-name`}>Name<input className={fieldClass} defaultValue={defaults?.name} id={`${prefix}-rcon-name`} maxLength={100} name="name" required /></label>
|
||||
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-host`}>Internal hostname<input autoCapitalize="none" autoCorrect="off" className={fieldClass} defaultValue={defaults?.host} id={`${prefix}-rcon-host`} maxLength={253} name="host" placeholder="season4.somc.svc.cluster.local" required spellCheck={false} /></label>
|
||||
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-host`}>Server address<input autoCapitalize="none" autoCorrect="off" className={fieldClass} defaultValue={defaults?.host} id={`${prefix}-rcon-host`} maxLength={253} name="host" placeholder="minecraft.example.com" required spellCheck={false} /></label>
|
||||
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-port`}>Port<input className={fieldClass} defaultValue={defaults?.port ?? 25575} id={`${prefix}-rcon-port`} max={65535} min={1} name="port" required type="number" /></label>
|
||||
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-password`}>{defaults ? "Replacement password" : "Password"}<input autoComplete="new-password" className={fieldClass} id={`${prefix}-rcon-password`} maxLength={512} name="password" required={!defaults} type="password" />{defaults && <span className="mt-2 block font-sans text-[10px] font-normal normal-case text-muted">Leave blank to preserve the current password.</span>}</label>
|
||||
</>
|
||||
|
||||
@@ -8,12 +8,15 @@ vi.mock("@/app/admin/(console)/rcon/actions", () => ({
|
||||
import { RconConsole } from "./rcon-console";
|
||||
|
||||
describe("RconConsole", () => {
|
||||
it("renders labelled keyboard-operable controls without history", () => {
|
||||
it("renders a labelled keyboard-operable terminal without persisted history", () => {
|
||||
const markup = renderToStaticMarkup(<RconConsole servers={[{ id: "one", name: "Season 4" }]} />);
|
||||
expect(markup).toContain('aria-label="RCON terminal"');
|
||||
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).toContain("server://");
|
||||
expect(markup).toContain("Awaiting command");
|
||||
expect(markup).toContain("Enter ↵");
|
||||
expect(markup).not.toContain("Latest response");
|
||||
});
|
||||
|
||||
|
||||
@@ -16,24 +16,41 @@ export function RconConsole({ servers }: { servers: ServerOption[] }) {
|
||||
}
|
||||
|
||||
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>
|
||||
<form action={action} aria-label="RCON terminal" className="mt-6 overflow-hidden border-2 border-ink bg-panel shadow-[6px_6px_0_var(--color-shadow)]">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-line bg-canvas px-4 py-3">
|
||||
<div className="flex items-center gap-2 font-mono text-[10px] font-bold uppercase tracking-wider text-muted">
|
||||
<span aria-hidden="true" className="size-2 rounded-full bg-signal shadow-[0_0_0_1px_var(--color-ink)]" />
|
||||
<span>server://</span>
|
||||
</div>
|
||||
)}
|
||||
<label className="flex items-center gap-2 font-mono text-[9px] font-bold uppercase tracking-wider" htmlFor="rcon-console-server">
|
||||
Target
|
||||
<select className="border border-line bg-panel px-3 py-2 font-mono text-xs font-bold normal-case outline-none focus:border-accent" 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>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" aria-relevant="additions text" className="min-h-52 max-h-80 overflow-auto p-5 font-mono text-xs leading-5" role={state.status === "error" ? "alert" : "status"}>
|
||||
{pending ? (
|
||||
<p className="text-muted"><span aria-hidden="true" className="mr-2 text-accent">›</span>Executing command…</p>
|
||||
) : state.status === "idle" ? (
|
||||
<p className="text-muted"><span aria-hidden="true" className="mr-2 text-accent">#</span>Awaiting command</p>
|
||||
) : (
|
||||
<>
|
||||
<p className={`text-[9px] font-bold uppercase tracking-wider ${state.status === "error" ? "text-accent" : "text-muted"}`}>
|
||||
{state.status === "error" ? "Error" : "Response"}{responseServer ? ` — ${responseServer.name}` : ""}
|
||||
</p>
|
||||
<pre className="mt-3 whitespace-pre-wrap break-words font-mono text-xs leading-5">{state.message}</pre>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 border-t-2 border-ink bg-canvas p-3">
|
||||
<span aria-hidden="true" className="font-mono text-lg font-black text-accent">$</span>
|
||||
<label className="sr-only" htmlFor="rcon-command">Command</label>
|
||||
<input autoComplete="off" autoFocus className="min-w-0 flex-1 bg-transparent px-1 py-2 font-mono text-sm outline-none placeholder:text-muted focus-visible:outline-none" id="rcon-command" maxLength={1024} name="command" placeholder="list" required spellCheck={false} />
|
||||
<button className="border border-ink bg-ink px-4 py-2 font-mono text-[9px] font-bold uppercase tracking-wider text-canvas disabled:cursor-wait disabled:opacity-60" disabled={pending} type="submit">{pending ? "Running…" : "Enter ↵"}</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user