// Admin: the access log for everyone, with filters and a CSV download (phase 9). import { useEffect, useState, type FormEvent } from "react"; import { admin9Api, type AccessLogFilters, type AccessLogRow } from "../../api/admin"; import { errorText, fmtDetail, fmtTime } from "../../lib/format"; import { Notice, type Msg } from "./common"; const PAGE = 200; /** A datetime-local value as an ISO time, or undefined. */ function iso(local: string): string | undefined { if (!local) return undefined; const d = new Date(local); return Number.isNaN(d.getTime()) ? undefined : d.toISOString(); } interface Form { since: string; until: string; q: string; kind: string; ok: "" | "true" | "false"; ip: string; } const EMPTY: Form = { since: "", until: "", q: "", kind: "", ok: "", ip: "" }; function toFilters(f: Form): AccessLogFilters { const out: AccessLogFilters = {}; const since = iso(f.since); const until = iso(f.until); if (since) out.since = since; if (until) out.until = until; if (f.q.trim()) out.q = f.q.trim(); if (f.kind.trim()) out.kind = f.kind.trim(); if (f.ok) out.ok = f.ok; if (f.ip.trim()) out.ip = f.ip.trim(); return out; } export function AccessLogTab() { const [form, setForm] = useState
(EMPTY); const [filters, setFilters] = useState({}); const [rows, setRows] = useState([]); const [next, setNext] = useState(null); const [loading, setLoading] = useState(true); const [msg, setMsg] = useState(null); const fetchPage = async (f: AccessLogFilters, before: string | null) => { setLoading(true); setMsg(null); try { const r = await admin9Api.accessLog({ ...f, limit: PAGE, ...(before ? { before } : {}) }); setRows((cur) => (before ? [...cur, ...r.rows] : r.rows)); setNext(r.next_before); } catch (err) { setMsg({ ok: false, text: errorText(err) }); } finally { setLoading(false); } }; // The first page, once. Later pages come from the search and the load-more button. useEffect(() => { void fetchPage({}, null); }, []); const set = (patch: Partial) => setForm((cur) => ({ ...cur, ...patch })); const search = (e: FormEvent) => { e.preventDefault(); const f = toFilters(form); setFilters(f); void fetchPage(f, null); }; const csvHref = admin9Api.accessLogCsvUrl(filters); return (
Download CSV
The CSV has the rows the last search matched, up to 50,000. The download is logged.
{rows.map((r) => ( ))} {!loading && rows.length === 0 ? ( ) : null}
When Who Kind Result Address Detail
{fmtTime(r.at)} {r.display_name ?? ""} {r.account ? {r.account} : null} {r.kind} {r.ok ? OK : Failed} {r.ip ?? "-"} {fmtDetail(r.detail)}
Nothing matches.
{loading ?
Loading
: null} {next && !loading ? (
) : null}
); }