This commit is contained in:
lda
2026-04-14 06:03:06 +07:00 Unverified
parent 8948171ae7
commit f034fc5afd
11 changed files with 303 additions and 18 deletions
+11
View File
@@ -11,6 +11,7 @@ import {
fetchAlerts,
fetchAudit,
revokeAgent,
setAgentNickname,
} from "@/api";
import { AppLayout } from "@/layout/AppLayout";
import { AgentsPage } from "@/pages/AgentsPage";
@@ -73,6 +74,15 @@ export function App() {
return result.revoked;
}
async function onSetAgentNickname(
agentId: string,
nickname: string | null,
): Promise<boolean> {
const result = await setAgentNickname(agentId, nickname);
await loadAll();
return result.updated;
}
async function refreshAlertsAndHistory() {
const [nextAlerts, nextHistory] = await Promise.all([
fetchAlerts(),
@@ -149,6 +159,7 @@ export function App() {
selectedAgentId={selectedAgentId}
onSelectAgent={setSelectedAgentId}
onRevokeAgent={onRevokeAgent}
onSetAgentNickname={onSetAgentNickname}
/>
}
/>
+25 -1
View File
@@ -1,4 +1,8 @@
export type Agent = { agent_id: string; connected: boolean };
export type Agent = {
agent_id: string;
connected: boolean;
nickname?: string | null;
};
export type Alert = {
alert_id: string;
@@ -62,6 +66,12 @@ export type RevokeAgentResponse = {
revoked: boolean;
};
export type SetAgentNicknameResponse = {
agent_id: string;
nickname: string | null;
updated: boolean;
};
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(path, {
...init,
@@ -134,6 +144,20 @@ export function revokeAgent(agentId: string): Promise<RevokeAgentResponse> {
);
}
export function setAgentNickname(
agentId: string,
nickname: string | null,
): Promise<SetAgentNicknameResponse> {
const normalized = nickname?.trim() ?? "";
return request<SetAgentNicknameResponse>(
`/api/v1/control/agents/${encodeURIComponent(agentId)}/nickname`,
{
method: "PATCH",
body: JSON.stringify({ nickname: normalized ? normalized : null }),
},
);
}
export function runCommand(
agentId: string,
kind: CommandKind,
+65 -11
View File
@@ -1,6 +1,7 @@
import type { Agent } from "@/api";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { useState } from "react";
type Props = {
@@ -8,6 +9,10 @@ type Props = {
selectedAgentId: string;
onSelectAgent: (agentId: string) => void;
onRevokeAgent: (agentId: string) => Promise<boolean>;
onSetAgentNickname: (
agentId: string,
nickname: string | null,
) => Promise<boolean>;
};
export function AgentsPage({
@@ -15,10 +20,17 @@ export function AgentsPage({
selectedAgentId,
onSelectAgent,
onRevokeAgent,
onSetAgentNickname,
}: Props) {
const [busy, setBusy] = useState(false);
const [status, setStatus] = useState("");
const [error, setError] = useState("");
const [drafts, setDrafts] = useState<Record<string, string>>({});
function displayLabel(agent: Agent): string {
const nickname = agent.nickname?.trim();
return nickname ? nickname : agent.agent_id;
}
async function onRevoke(agentId: string) {
if (!window.confirm(`Revoke agent credentials for ${agentId}?`)) {
@@ -39,6 +51,25 @@ export function AgentsPage({
}
}
async function onSaveNickname(agent: Agent) {
const draft = drafts[agent.agent_id] ?? agent.nickname ?? "";
setBusy(true);
setStatus("");
setError("");
try {
const updated = await onSetAgentNickname(agent.agent_id, draft);
setStatus(
updated
? `Updated nickname for ${agent.agent_id}`
: `${agent.agent_id} not found`,
);
} catch (err) {
setError(String(err));
} finally {
setBusy(false);
}
}
return (
<Card>
<CardHeader>
@@ -55,24 +86,47 @@ export function AgentsPage({
variant={
selectedAgentId === agent.agent_id ? "secondary" : "outline"
}
className="flex h-auto flex-1 items-center justify-between px-3 py-2 text-left"
className="flex h-auto flex-1 items-center justify-between gap-3 px-3 py-2 text-left"
onClick={() => onSelectAgent(agent.agent_id)}
disabled={busy}
>
<span>{agent.agent_id}</span>
<span className="min-w-0 truncate">{displayLabel(agent)}</span>
<span className="text-xs text-muted-foreground">
{agent.connected ? "connected" : "offline"}
</span>
</Button>
<Button
variant="outline"
size="sm"
className="shrink-0 border-destructive/50 text-destructive hover:bg-destructive/10"
onClick={() => void onRevoke(agent.agent_id)}
disabled={busy}
>
Revoke
</Button>
<div className="grid min-w-52 gap-2">
<Input
value={drafts[agent.agent_id] ?? agent.nickname ?? ""}
onChange={(e) =>
setDrafts((prev) => ({
...prev,
[agent.agent_id]: e.target.value,
}))
}
placeholder="nickname (optional)"
disabled={busy}
/>
<div className="flex items-center justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => void onSaveNickname(agent)}
disabled={busy}
>
Save Name
</Button>
<Button
variant="outline"
size="sm"
className="shrink-0 border-destructive/50 text-destructive hover:bg-destructive/10"
onClick={() => void onRevoke(agent.agent_id)}
disabled={busy}
>
Revoke
</Button>
</div>
</div>
</div>
))}
{!agents.length && (
+8 -1
View File
@@ -26,6 +26,11 @@ export function CommandsPage({
onSelectAgent,
onAfterCommand,
}: Props) {
function displayAgentLabel(agent: Agent): string {
const nickname = agent.nickname?.trim();
return nickname ? nickname : agent.agent_id;
}
const [kind, setKind] = useState<CommandKind>("devs");
const [query, setQuery] = useState("");
const [output, setOutput] = useState("Select agent and run a command");
@@ -77,7 +82,9 @@ export function CommandsPage({
className={`size-2 shrink-0 rounded-full ${agent.connected ? "bg-emerald-500" : "bg-zinc-400"}`}
aria-hidden
/>
<span className="min-w-0 truncate">{agent.agent_id}</span>
<span className="min-w-0 truncate">
{displayAgentLabel(agent)}
</span>
<span className="shrink-0 text-xs text-muted-foreground">
{agent.connected ? "connected" : "offline"}
</span>
+9 -2
View File
@@ -44,6 +44,11 @@ export function DevicesPage({
onSelectAgent,
onAfterWake,
}: Props) {
function displayAgentLabel(agent: Agent): string {
const nickname = agent.nickname?.trim();
return nickname ? nickname : agent.agent_id;
}
const selectedAgent = agents.find(
(agent) => agent.agent_id === selectedAgentId,
);
@@ -351,7 +356,9 @@ export function DevicesPage({
) : null}
<span className="min-w-0 truncate">
{selectedAgentId
? selectedAgentId
? selectedAgent
? displayAgentLabel(selectedAgent)
: selectedAgentId
: "Select connected agent"}
</span>
{selectedAgent ? (
@@ -378,7 +385,7 @@ export function DevicesPage({
aria-hidden
/>
<span className="min-w-0 truncate">
{agent.agent_id}
{displayAgentLabel(agent)}
</span>
<span className="shrink-0 text-xs text-muted-foreground">
{agent.connected ? "connected" : "offline"}