- packages/webui: Vite + React + Tailwind v4, single-file bundle - Dark industrial theme, Space Grotesk + JetBrains Mono - Config table with scope/flag badges, masked values, search/filter - Admin panel for agent management - scripts/build-ui.sh generates worker/src/ui.ts from webui build - Worker serves UI at GET / before auth check
115 lines
4.9 KiB
TypeScript
115 lines
4.9 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useApi } from "../hooks/useApi";
|
|
import type { Agent } from "../types";
|
|
|
|
interface Props {
|
|
addToast: (msg: string, type: "success" | "error") => void;
|
|
}
|
|
|
|
export default function AdminPanel({ addToast }: Props) {
|
|
const api = useApi();
|
|
const [agents, setAgents] = useState<Agent[]>([]);
|
|
const [newAgentId, setNewAgentId] = useState("");
|
|
const [newRole, setNewRole] = useState("agent");
|
|
const [generatedToken, setGeneratedToken] = useState("");
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const loadAgents = async () => {
|
|
try {
|
|
const data = await api.getAgents();
|
|
setAgents(Array.isArray(data) ? data : data.agents || []);
|
|
} catch (e: any) {
|
|
addToast("Failed to load agents: " + e.message, "error");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => { loadAgents(); }, []);
|
|
|
|
const handleCreateToken = async () => {
|
|
if (!newAgentId.trim()) return;
|
|
try {
|
|
const data = await api.createToken(newAgentId.trim(), newRole);
|
|
setGeneratedToken(data.token || JSON.stringify(data));
|
|
addToast("Token created", "success");
|
|
setNewAgentId("");
|
|
loadAgents();
|
|
} catch (e: any) {
|
|
addToast("Failed: " + e.message, "error");
|
|
}
|
|
};
|
|
|
|
const handleRevoke = async (tokenId: string) => {
|
|
try {
|
|
await api.revokeToken(tokenId);
|
|
addToast("Token revoked", "success");
|
|
loadAgents();
|
|
} catch (e: any) {
|
|
addToast("Failed: " + e.message, "error");
|
|
}
|
|
};
|
|
|
|
if (loading) return <div className="text-[#64748b] text-center py-8">Loading agents...</div>;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="bg-[#12141a] border border-[#1e2030] rounded-xl p-6">
|
|
<h3 className="text-lg font-bold text-white mb-4" style={{ fontFamily: "var(--font-heading)" }}>Create Agent Token</h3>
|
|
<div className="flex gap-3 items-end flex-wrap">
|
|
<div>
|
|
<label className="block text-xs text-[#64748b] mb-1">Agent ID</label>
|
|
<input value={newAgentId} onChange={(e) => setNewAgentId(e.target.value)} placeholder="agent-name"
|
|
className="px-3 py-2 bg-[#0a0a0c] border border-[#1e2030] rounded-lg text-white text-sm focus:outline-none focus:border-[#3b82f6]" style={{ fontFamily: "var(--font-mono)" }} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-[#64748b] mb-1">Role</label>
|
|
<select value={newRole} onChange={(e) => setNewRole(e.target.value)}
|
|
className="px-3 py-2 bg-[#0a0a0c] border border-[#1e2030] rounded-lg text-white text-sm focus:outline-none focus:border-[#3b82f6]">
|
|
<option value="agent">Agent</option>
|
|
<option value="admin">Admin</option>
|
|
</select>
|
|
</div>
|
|
<button onClick={handleCreateToken} className="px-4 py-2 bg-[#3b82f6] hover:bg-[#2563eb] text-white text-sm font-semibold rounded-lg transition-colors">
|
|
Generate Token
|
|
</button>
|
|
</div>
|
|
{generatedToken && (
|
|
<div className="mt-4 p-3 bg-[#0a0a0c] border border-green-500/30 rounded-lg">
|
|
<p className="text-xs text-green-400 mb-1">Generated token (copy now, shown once):</p>
|
|
<code className="text-sm text-white break-all" style={{ fontFamily: "var(--font-mono)" }}>{generatedToken}</code>
|
|
<button onClick={() => { navigator.clipboard.writeText(generatedToken); addToast("Copied", "success"); }}
|
|
className="ml-2 text-xs text-[#3b82f6] hover:underline">Copy</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="bg-[#12141a] border border-[#1e2030] rounded-xl p-6">
|
|
<h3 className="text-lg font-bold text-white mb-4" style={{ fontFamily: "var(--font-heading)" }}>Agents</h3>
|
|
{agents.length === 0 ? (
|
|
<p className="text-[#64748b] text-sm">No agents found</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{agents.map((agent) => (
|
|
<div key={agent.id} className="flex items-center justify-between p-3 bg-[#0a0a0c] rounded-lg border border-[#1e2030]">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-white font-medium" style={{ fontFamily: "var(--font-mono)" }}>{agent.id}</span>
|
|
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
|
agent.role === "admin" ? "bg-purple-500/15 text-purple-400 border border-purple-500/30" : "bg-[#1e2030] text-[#64748b]"
|
|
}`}>{agent.role}</span>
|
|
</div>
|
|
{agent.tokens?.map((t) => (
|
|
<button key={t.id} onClick={() => handleRevoke(t.id)}
|
|
className="text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 px-2 py-1 rounded transition-colors">
|
|
Revoke {t.id.slice(0, 8)}...
|
|
</button>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|