feat(rcon): unify terminal workspace
This commit is contained in:
@@ -1,14 +1,7 @@
|
||||
import { rconServers } from "@minecraft-account-manager/database";
|
||||
import { asc } from "drizzle-orm";
|
||||
import { RconConsole } from "@/components/rcon-console";
|
||||
import { RconConsole, type RconTerminalNotice } from "@/components/rcon-console";
|
||||
import { db } from "@/lib/database";
|
||||
import {
|
||||
createRconServer,
|
||||
deleteRconServer,
|
||||
setRconServerEnabled,
|
||||
testSavedRconServer,
|
||||
updateRconServer,
|
||||
} from "./actions";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -46,13 +39,17 @@ export default async function RconPage({
|
||||
const query = await searchParams;
|
||||
const saved = queryValue(query.saved);
|
||||
const error = queryValue(query.error);
|
||||
const notice: RconTerminalNotice | undefined = error
|
||||
? { status: "error", message: errorMessages[error] ?? "The RCON operation failed." }
|
||||
: saved
|
||||
? { status: "success", message: savedMessages[saved] ?? "RCON settings saved." }
|
||||
: undefined;
|
||||
const servers = await db.select({
|
||||
id: rconServers.id,
|
||||
name: rconServers.name,
|
||||
host: rconServers.host,
|
||||
port: rconServers.port,
|
||||
enabled: rconServers.enabled,
|
||||
updatedAt: rconServers.updatedAt,
|
||||
}).from(rconServers).orderBy(asc(rconServers.name));
|
||||
|
||||
return (
|
||||
@@ -60,88 +57,19 @@ 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 to internal or external server addresses. Credentials are never sent to the browser.</p>
|
||||
<p className="mt-5 max-w-2xl text-sm leading-6 text-muted">Select and manage a connection, then run commands through the portal backend. 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>}
|
||||
{error && <p className="mt-7 border-l-2 border-accent bg-panel px-5 py-4 text-sm text-accent" role="alert">{errorMessages[error] ?? "The RCON operation failed."}</p>}
|
||||
|
||||
<div className="mt-10 grid gap-10 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<section className="border border-line bg-panel p-6 shadow-[6px_6px_0_var(--color-shadow)]">
|
||||
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Command proxy</p>
|
||||
<h2 className="mt-2 font-display text-3xl font-black uppercase">Console</h2>
|
||||
<p className="mt-3 text-xs leading-5 text-muted">Only the latest bounded response is shown. Commands and responses are not saved as console history.</p>
|
||||
<RconConsole servers={servers.filter((server) => server.enabled).map(({ id, name }) => ({ id, name }))} />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Configuration</p>
|
||||
<h2 className="mt-2 font-display text-3xl font-black uppercase">Add connection</h2>
|
||||
<form action={createRconServer} className="mt-5 space-y-4 border border-line bg-panel p-6">
|
||||
<ConnectionFields prefix="new" />
|
||||
<label className="flex items-center gap-3 font-mono text-[10px] font-bold uppercase"><input className="size-4" name="enabled" type="checkbox" value="yes" />Enable immediately</label>
|
||||
<button className="border border-ink bg-ink px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas" type="submit">Add connection</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="mt-12">
|
||||
<div className="flex items-end justify-between border-b border-line pb-4">
|
||||
<div><p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Saved endpoints</p><h2 className="mt-2 font-display text-3xl font-black uppercase">Connections</h2></div>
|
||||
<span className="font-mono text-xs text-muted">{servers.length} configured</span>
|
||||
</div>
|
||||
<div className="divide-y divide-line">
|
||||
{servers.map((server) => (
|
||||
<article className="grid gap-5 py-6 lg:grid-cols-[1fr_auto] lg:items-start" key={server.id}>
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-3"><h3 className="font-mono text-lg font-bold">{server.name}</h3><span className={`px-2 py-1 font-mono text-[9px] font-bold uppercase ${server.enabled ? "bg-signal text-ink" : "border border-line text-muted"}`}>{server.enabled ? "Enabled" : "Disabled"}</span></div>
|
||||
<p className="mt-2 font-mono text-[10px] text-muted">{server.host}:{server.port}</p>
|
||||
<p className="mt-1 font-mono text-[9px] text-muted">Updated {server.updatedAt.toISOString()}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
<form action={testSavedRconServer}><input name="serverId" type="hidden" value={server.id} /><button className="font-mono text-[9px] font-bold uppercase underline underline-offset-4" type="submit">Test</button></form>
|
||||
<form action={setRconServerEnabled}><input name="serverId" type="hidden" value={server.id} /><input name="enabled" type="hidden" value={server.enabled ? "no" : "yes"} /><button className="font-mono text-[9px] font-bold uppercase underline underline-offset-4" type="submit">{server.enabled ? "Disable" : "Enable"}</button></form>
|
||||
<details className="relative">
|
||||
<summary className="cursor-pointer list-none font-mono text-[9px] font-bold uppercase underline underline-offset-4">Edit</summary>
|
||||
<form action={updateRconServer} className="relative z-10 mt-3 w-[min(28rem,80vw)] space-y-4 border border-line bg-panel p-5 shadow-[5px_5px_0_var(--color-shadow)] lg:absolute lg:right-0">
|
||||
<input name="serverId" type="hidden" value={server.id} />
|
||||
<ConnectionFields defaults={server} prefix={server.id} />
|
||||
<label className="flex items-center gap-3 font-mono text-[10px] font-bold uppercase"><input className="size-4" defaultChecked={server.enabled} name="enabled" type="checkbox" value="yes" />Enabled</label>
|
||||
<button className="border border-ink px-4 py-2 font-mono text-[9px] font-bold uppercase" type="submit">Save connection</button>
|
||||
</form>
|
||||
</details>
|
||||
<details className="relative">
|
||||
<summary className="cursor-pointer list-none font-mono text-[9px] font-bold uppercase text-accent underline underline-offset-4">Delete</summary>
|
||||
<form action={deleteRconServer} className="relative z-10 mt-3 w-64 border border-accent bg-panel p-5 shadow-[5px_5px_0_var(--color-accent)] lg:absolute lg:right-0">
|
||||
<input name="serverId" type="hidden" value={server.id} /><input name="confirmation" type="hidden" value={server.id} />
|
||||
<p className="text-xs leading-5">Delete {server.name}? Its encrypted credential will be removed.</p>
|
||||
<button className="mt-4 bg-accent px-4 py-2 font-mono text-[9px] font-bold uppercase text-canvas" type="submit">Confirm deletion</button>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{!servers.length && <p className="py-8 text-sm text-muted">No RCON connections configured.</p>}
|
||||
<section className="mt-10">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Command proxy</p>
|
||||
<h2 className="mt-2 font-display text-3xl font-black uppercase">Terminal</h2>
|
||||
</div>
|
||||
<p className="max-w-xl text-xs leading-5 text-muted">Only the latest bounded response is shown. Commands and responses are not saved as console history.</p>
|
||||
</div>
|
||||
<RconConsole notice={notice} servers={servers} />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectionFields({
|
||||
prefix,
|
||||
defaults,
|
||||
}: {
|
||||
prefix: string;
|
||||
defaults?: { name: string; host: string; port: number };
|
||||
}) {
|
||||
const fieldClass = "mt-2 block w-full border border-line bg-canvas px-4 py-3 font-mono text-sm outline-none focus:border-accent";
|
||||
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`}>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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user