SPB Git
1.8 KB · 10 lines tsx
1import { sql } from "@/db";2import { PageTitle } from "@/components/admin-ui";3export default async function Audit({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {4  const sp = await searchParams; const page = Math.max(1, Number(sp.page ?? 1) || 1); const q = sp.q || null;5  const rows = await sql`SELECT * FROM audit_log WHERE (${q}::text IS NULL OR action ILIKE '%' || ${q ?? ""} || '%' OR actor_label ILIKE '%' || ${q ?? ""} || '%' OR entity_id ILIKE '%' || ${q ?? ""} || '%') ORDER BY at DESC LIMIT 100 OFFSET ${(page - 1) * 100}`;6  return (<div><PageTitle title="Audit log" /><form className="flex gap-2 mb-4"><input name="q" defaultValue={sp.q ?? ""} placeholder="filter action / actor / entity" className="input !w-72" /><button className="btn btn-sm">Filter</button></form>7    <div className="card overflow-x-auto"><table className="data"><thead><tr><th>When</th><th>Actor</th><th>IP</th><th>Action</th><th>Entity</th><th>Before</th><th>After</th></tr></thead><tbody>{rows.map((a) => <tr key={a.id}><td className="whitespace-nowrap text-xs">{new Date(a.at).toLocaleString("en-CA")}</td><td className="text-xs">{a.actor_label}</td><td className="text-xs">{a.ip ?? ""}</td><td className="font-mono text-xs">{a.action}</td><td className="text-xs">{a.entity_type} {a.entity_id ?? ""}</td><td className="text-xs text-fg-3 max-w-xs truncate">{a.before ? JSON.stringify(a.before) : ""}</td><td className="text-xs text-fg-3 max-w-xs truncate">{a.after ? JSON.stringify(a.after) : ""}</td></tr>)}</tbody></table></div>8    <p className="mt-3 text-sm flex gap-3">{page > 1 && <a href={`?page=${page - 1}&q=${sp.q ?? ""}`} className="text-navy">← Newer</a>}{rows.length === 100 && <a href={`?page=${page + 1}&q=${sp.q ?? ""}`} className="text-navy">Older →</a>}</p></div>);9}10
Files · main
Menu

Theme