团子 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

117 lines
5.1 KiB
TypeScript

import { useState } from "react";
import type { ConfigEntry } from "../types";
interface Props {
entries: [string, ConfigEntry][];
onEdit: (key: string, entry: ConfigEntry) => void;
onDelete: (key: string, scope: string) => void;
addToast: (msg: string, type: "success" | "error") => void;
}
export default function ConfigTable({ entries, onEdit, onDelete, addToast }: Props) {
const [revealed, setRevealed] = useState<Set<string>>(new Set());
const toggle = (key: string) => {
setRevealed((prev) => {
const next = new Set(prev);
next.has(key) ? next.delete(key) : next.add(key);
return next;
});
};
const copy = (value: string) => {
navigator.clipboard.writeText(value);
addToast("Copied to clipboard", "success");
};
const fmtTime = (iso: string) => {
try {
const d = new Date(iso);
return d.toLocaleDateString(undefined, { month: "short", day: "numeric" }) + " " + d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
} catch { return iso; }
};
if (entries.length === 0) {
return (
<div className="text-center py-16 text-[#64748b]">
<p className="text-lg mb-1">No configuration keys found</p>
<p className="text-sm">Add your first key to get started</p>
</div>
);
}
return (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-[#64748b] text-xs uppercase tracking-wider border-b border-[#1e2030]">
<th className="pb-3 pr-4 font-medium">Key</th>
<th className="pb-3 pr-4 font-medium">Value</th>
<th className="pb-3 pr-4 font-medium">Scope</th>
<th className="pb-3 pr-4 font-medium">Flags</th>
<th className="pb-3 pr-4 font-medium">Updated</th>
<th className="pb-3 font-medium text-right">Actions</th>
</tr>
</thead>
<tbody>
{entries.map(([key, entry]) => {
const show = revealed.has(key);
const displayVal = show ? entry.value : "•".repeat(Math.min(entry.value.length || 8, 24));
return (
<tr key={key} className="border-b border-[#1e2030]/50 hover:bg-[#1e2030]/30 transition-colors">
<td className="py-3 pr-4" style={{ fontFamily: "var(--font-mono)" }}>
<span className="text-white font-medium">{key}</span>
</td>
<td className="py-3 pr-4 max-w-[280px]">
<div className="flex items-center gap-2">
<span
className={`truncate ${show ? "text-white" : "text-[#64748b]"}`}
style={{ fontFamily: "var(--font-mono)", fontSize: "0.8rem" }}
>
{displayVal}
</span>
<button onClick={() => toggle(key)} className="text-[#64748b] hover:text-white shrink-0 transition-colors" title={show ? "Hide" : "Reveal"}>
{show ? "👁" : "👁‍🗨"}
</button>
<button onClick={() => copy(entry.value)} className="text-[#64748b] hover:text-[#3b82f6] shrink-0 transition-colors" title="Copy">
📋
</button>
</div>
</td>
<td className="py-3 pr-4">
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
entry.scope === "shared" ? "bg-blue-500/15 text-blue-400 border border-blue-500/30" : "bg-green-500/15 text-green-400 border border-green-500/30"
}`}>
{entry.scope}
</span>
</td>
<td className="py-3 pr-4">
<div className="flex gap-1.5">
{entry.secret && (
<span className="text-xs px-2 py-0.5 rounded-full bg-red-500/15 text-red-400 border border-red-500/30 font-medium">secret</span>
)}
{entry.env === false && (
<span className="text-xs px-2 py-0.5 rounded-full bg-yellow-500/15 text-yellow-400 border border-yellow-500/30 font-medium">no-env</span>
)}
</div>
</td>
<td className="py-3 pr-4 text-[#64748b] text-xs whitespace-nowrap">{fmtTime(entry.updated_at)}</td>
<td className="py-3 text-right">
<div className="flex justify-end gap-1">
<button onClick={() => onEdit(key, entry)} className="px-2 py-1 text-xs text-[#64748b] hover:text-white hover:bg-[#1e2030] rounded transition-colors">
Edit
</button>
<button onClick={() => onDelete(key, entry.scope)} className="px-2 py-1 text-xs text-[#64748b] hover:text-red-400 hover:bg-red-500/10 rounded transition-colors">
Delete
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}