- 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
117 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|