团子 58494b8fca feat: React webui with flags support, build-ui pipeline
- 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
2026-04-21 04:01:35 +00:00

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>
);
}