This commit is contained in:
lda committed 2026-04-12 02:41:36 +07:00
1 parent 6e91d0b599
commit 34e40e8c0d
13 files changed
+399 -132

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
import type { Agent } from "@/api";
type Props = {
agents: Agent[];
selectedAgentId: string;
onSelectAgent: (agentId: string) => void;
};
export function AgentsPage({ agents, selectedAgentId, onSelectAgent }: Props) {
return (
<section className="card">
<h2>Agents</h2>
<div className="list">
{agents.map((agent) => (
<button
key={agent.agent_id}
className={`row ${selectedAgentId === agent.agent_id ? "selected" : ""}`}
onClick={() => onSelectAgent(agent.agent_id)}
>
<span>{agent.agent_id}</span>
<span>{agent.connected ? "connected" : "offline"}</span>
</button>
))}
{!agents.length && <div className="empty">No agents enrolled</div>}
</div>
</section>
);
}
+34
View File
@@ -0,0 +1,34 @@
import type { Alert, AlertTransition } from "@/api";
type Props = {
alerts: Alert[];
transitions: AlertTransition[];
onRefresh: () => Promise<void>;
};
export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
return (
<section className="two-col">
<div className="card">
<div className="row-head">
<h2>Active Alerts</h2>
<button onClick={() => void onRefresh()}>Refresh</button>
</div>
<div className="list">
{alerts.map((alert) => (
<div className="row plain" key={alert.alert_id}>
<span>{alert.kind}{alert.agent_id ? ` (${alert.agent_id})` : ""}</span>
<span>{alert.severity}</span>
</div>
))}
{!alerts.length && <div className="empty">No active alerts</div>}
</div>
</div>
<div className="card">
<h2>Transition History</h2>
<pre className="output small">{JSON.stringify(transitions, null, 2)}</pre>
</div>
</section>
);
}
+18
View File
@@ -0,0 +1,18 @@
import type { AuditEvent } from "@/api";
type Props = {
events: AuditEvent[];
onRefresh: () => Promise<void>;
};
export function AuditPage({ events, onRefresh }: Props) {
return (
<section className="card span-full">
<div className="row-head">
<h2>Recent Audit Events</h2>
<button onClick={() => void onRefresh()}>Refresh</button>
</div>
<pre className="output">{JSON.stringify(events, null, 2)}</pre>
</section>
);
}
+68
View File
@@ -0,0 +1,68 @@
import { useState } from "react";
import { runCommand, type Agent, type CommandKind } from "@/api";
type Props = {
agents: Agent[];
selectedAgentId: string;
onSelectAgent: (agentId: string) => void;
onAfterCommand?: () => Promise<void>;
};
export function CommandsPage({ agents, selectedAgentId, onSelectAgent, onAfterCommand }: Props) {
const [kind, setKind] = useState<CommandKind>("devs");
const [query, setQuery] = useState("");
const [output, setOutput] = useState("Select agent and run a command");
const [running, setRunning] = useState(false);
async function submit(e: React.FormEvent) {
e.preventDefault();
if (!selectedAgentId) return;
setRunning(true);
setOutput("Running...");
try {
const out = await runCommand(selectedAgentId, kind, query.trim());
setOutput(JSON.stringify(out, null, 2));
if (onAfterCommand) await onAfterCommand();
} catch (err) {
setOutput(String(err));
} finally {
setRunning(false);
}
}
return (
<section className="card">
<h2>Command Runner</h2>
<form className="form" onSubmit={submit}>
<label>
Agent
<select value={selectedAgentId} onChange={(e) => onSelectAgent(e.target.value)} required>
<option value="" disabled>Select agent</option>
{agents.map((agent) => (
<option key={agent.agent_id} value={agent.agent_id}>
{agent.agent_id} ({agent.connected ? "connected" : "offline"})
</option>
))}
</select>
</label>
<label>
Command
<select value={kind} onChange={(e) => setKind(e.target.value as CommandKind)}>
<option value="devs">devs</option>
<option value="status">status</option>
<option value="leases">leases</option>
<option value="inventory">inventory</option>
<option value="wake">wake</option>
</select>
</label>
<label>
Query
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="optional" />
</label>
<button type="submit" disabled={running || !selectedAgentId}>{running ? "Running..." : "Run command"}</button>
</form>
<pre className="output">{output}</pre>
</section>
);
}
+30
View File
@@ -0,0 +1,30 @@
import type { Agent, Alert, AlertTransition } from "@/api";
type Props = {
agents: Agent[];
alerts: Alert[];
transitions: AlertTransition[];
loading: boolean;
onRefresh: () => void;
};
export function DashboardPage({ agents, alerts, transitions, loading, onRefresh }: Props) {
const connected = agents.filter((a) => a.connected).length;
return (
<section className="card-grid">
<div className="card stat"><h3>Agents</h3><strong>{agents.length}</strong></div>
<div className="card stat"><h3>Connected</h3><strong>{connected}</strong></div>
<div className="card stat"><h3>Active Alerts</h3><strong>{alerts.length}</strong></div>
<div className="card stat"><h3>Transitions</h3><strong>{transitions.length}</strong></div>
<div className="card span-full">
<div className="row-head">
<h2>Overview</h2>
<button onClick={onRefresh} disabled={loading}>{loading ? "Refreshing..." : "Refresh"}</button>
</div>
<p className="muted">
Use tabs to run commands, inspect audits, and monitor alerts in real time.
</p>
</div>
</section>
);
}
+10
View File
@@ -0,0 +1,10 @@
export function TokensPage() {
return (
<section className="card">
<h2>Tokens</h2>
<p className="muted">
Token operations UI is the next increment. Backend endpoints are already available for issue/list/revoke.
</p>
</section>
);
}