import { useState } from "react"; import { useTranslation } from "react-i18next"; import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Trash2 } from "lucide-react"; import { api, type AuditQuery, type AuditRow } from "../lib/api"; import { notifyActionError } from "../lib/actionErrors"; import { sinceOf } from "../lib/activityUrlState"; import { auditActionLabel, auditText } from "../lib/auditText"; import { formatDateTimeSec } from "../lib/format"; import { notify } from "../lib/notifications"; import { qk } from "../lib/queryKeys"; import { useHistoryList } from "../lib/useHistoryList"; import type { Shared } from "./ActivityRuns"; import { useConfirm } from "./confirmContext"; import { StateMessage } from "./ui/QueryState"; import HistoryList from "./HistoryList"; import HistoryRow from "./HistoryRow"; import { RelativeTime } from "./ui/RelativeTime"; import { Button } from "./ui/Button"; import FilterSelect from "./ui/FilterSelect"; // Activity → Admin actions: the append-only who/what/when trail, newest first, filtered on the // server. A row opens to its before/after and to the lines its request logged. Clear stays here, // leaving one row that says who cleared it. const GRID = "grid-cols-[minmax(0,1fr)_auto] sm:grid-cols-[minmax(0,12rem)_minmax(0,12rem)_8rem_minmax(10rem,1fr)]"; export default function ActivityAudit({ shared, action, onAction, onRequest, }: { shared: Shared; action: string | null; onAction: (action: string | null) => void; /** Open the server log at this request's lines. */ onRequest: (requestId: string) => void; }) { const { t, i18n } = useTranslation(); const lang = i18n.language; const qc = useQueryClient(); const confirm = useConfirm(); const [open, setOpen] = useState(null); const query: AuditQuery = { user: shared.user ?? undefined, q: shared.q.trim() || undefined, action: action ?? undefined, }; const key = { ...query, window: shared.window }; const state = useHistoryList(qk.adminAudit(key), (before) => api.adminAudit({ ...query, since: sinceOf(shared.window) }, before), ); const facets = useQuery({ queryKey: qk.adminAuditFacets(key), queryFn: () => api.adminAuditFacets({ ...query, since: sinceOf(shared.window) }), placeholderData: keepPreviousData, }); const actions = [ ...(facets.data?.actions ?? []), ...(action && !facets.data?.actions.some((f) => f.value === action) ? [{ value: action, count: 0 }] : []), ]; const clear = useMutation({ mutationFn: () => api.clearAudit(), onSuccess: (r) => { void qc.invalidateQueries({ queryKey: qk.adminAudit() }); void qc.invalidateQueries({ queryKey: qk.adminAuditFacets() }); notify({ level: "success", message: t("audit.cleared", { count: r.cleared }) }); }, onError: (e) => notifyActionError(e, t("audit.clearFailed")), }); const onClear = async () => { if ( await confirm({ title: t("audit.clearTitle"), message: t("audit.clearConfirm"), confirmLabel: t("audit.clear"), danger: true, }) ) clear.mutate(); }; const actor = (row: AuditRow) => row.actor_id === null ? null : (state.accounts[String(row.actor_id)] ?? `#${row.actor_id}`); return (
({ value: f.value, label: auditActionLabel(t, f.value), count: f.count, })), ]} selected={[action ?? ""]} onChange={([value]) => onAction(value || null)} testId="audit-action" />
{/* The action menu above is built from these counts: without them it offers only "Any". */} {facets.isError && ( void facets.refetch()}> {t("activity.filtersFailed")} )} {state.rows.map((row, index) => { const { head, sub } = auditText(t, row, lang); const who = actor(row); return ( setOpen(open === row.id ? null : row.id)} title={formatDateTimeSec(row.created_at, lang)} cells={ <> {auditActionLabel(t, row.action)} {who ?? t("audit.system")} {[head, sub].filter(Boolean).join(" · ")} } > ); })}
); } function AuditDetail({ row, who, onRequest, }: { row: AuditRow; who: string | null; onRequest: (requestId: string) => void; }) { const { t, i18n } = useTranslation(); const lang = i18n.language; const { head, sub } = auditText(t, row, lang); return (
{t("audit.cols.actor")}
{who ?? t("audit.system")}
{t("audit.cols.when")}
{formatDateTimeSec(row.created_at, lang)}
{(head || sub) && ( <>
{t("audit.cols.details")}
{[head, sub].filter(Boolean).join(" · ")}
)} {row.target_id && ( <>
{t("activity.audit.target")}
{row.target_id}
)}
{(row.before || row.after) && (
          {JSON.stringify({ before: row.before, after: row.after }, null, 2)}
        
)} {/* A request's lines are kept days, its row months: a link to none would open an empty list. */} {row.request_id && (row.log_lines > 0 ? (
) : (

{t("activity.audit.noRequestLines")}

))}
); }