From c72ea7630a5c1df6d58add5aec961c13c3af5f3e Mon Sep 17 00:00:00 2001 From: IHUOMA NKECHI BRIDGET Date: Tue, 28 Jul 2026 19:57:07 +0100 Subject: [PATCH] Build scalable filterable audit log system --- apps/dashboard/app/activity/page.tsx | 50 +++++-- apps/dashboard/lib/activity/mapper.ts | 13 +- apps/dashboard/test/activity-mapper.test.ts | 59 +++++++- apps/dashboard/test/activity-query.test.ts | 24 ++++ docs/audit-log-schema.md | 152 ++++++++++++++++++++ 5 files changed, 273 insertions(+), 25 deletions(-) create mode 100644 docs/audit-log-schema.md diff --git a/apps/dashboard/app/activity/page.tsx b/apps/dashboard/app/activity/page.tsx index c0e6e5f..43981f0 100644 --- a/apps/dashboard/app/activity/page.tsx +++ b/apps/dashboard/app/activity/page.tsx @@ -8,6 +8,7 @@ import { formatRelativeTime } from "@/lib/format-relative-time"; import { useActivityFeed } from "@/lib/hooks/useActivityFeed"; import { convertToCsv, downloadCsv, } from "@/lib/csv-export"; import { + type ActivityEventEntity, type ActivityEventSeverity, type ActivityEventSource, type ActivityEventType, @@ -83,6 +84,15 @@ const SEVERITY_FILTERS: { label: string; value: ActivityEventSeverity | "" }[] = { label: "Critical", value: "critical" }, ]; +const ENTITY_TYPE_FILTERS: { label: string; value: ActivityEventEntity["type"] | "" }[] = [ + { label: "All entity types", value: "" }, + { label: "Pass", value: "pass" }, + { label: "Guild", value: "guild" }, + { label: "Member", value: "member" }, + { label: "Verification", value: "verification" }, + { label: "Webhook", value: "webhook" }, +]; + const SORT_OPTIONS: { label: string; value: ActivitySortOrder }[] = [ { label: "Newest first", value: "newest" }, { label: "Oldest first", value: "oldest" }, @@ -106,6 +116,7 @@ function ActivityPageContent() { const [type, setType] = useState(() => (searchParams.get("type") as ActivityEventType | null) ?? ""); const [source, setSource] = useState(() => (searchParams.get("source") as ActivityEventSource | null) ?? ""); const [severity, setSeverity] = useState(() => (searchParams.get("severity") as ActivityEventSeverity | null) ?? ""); + const [entityType, setEntityType] = useState(() => (searchParams.get("entityType") as ActivityEventEntity["type"] | null) ?? ""); const [actor, setActor] = useState(() => searchParams.get("actor") ?? ""); const [from, setFrom] = useState(() => searchParams.get("from") ?? ""); const [sort, setSort] = useState(() => readSort(searchParams.get("sort"))); @@ -116,6 +127,7 @@ function ActivityPageContent() { type?: ActivityEventType | ""; source?: ActivityEventSource | ""; severity?: ActivityEventSeverity | ""; + entityType?: ActivityEventEntity["type"] | ""; actor?: string; from?: string; sort?: ActivitySortOrder; @@ -134,6 +146,7 @@ function ActivityPageContent() { if (updates.type !== undefined) setOrDelete("type", updates.type); if (updates.source !== undefined) setOrDelete("source", updates.source); if (updates.severity !== undefined) setOrDelete("severity", updates.severity); + if (updates.entityType !== undefined) setOrDelete("entityType", updates.entityType); if (updates.actor !== undefined) setOrDelete("actor", updates.actor); if (updates.from !== undefined) setOrDelete("from", updates.from); if (updates.sort !== undefined) { @@ -161,6 +174,7 @@ function ActivityPageContent() { const nextType = (searchParams.get("type") as ActivityEventType | null) ?? ""; const nextSource = (searchParams.get("source") as ActivityEventSource | null) ?? ""; const nextSeverity = (searchParams.get("severity") as ActivityEventSeverity | null) ?? ""; + const nextEntityType = (searchParams.get("entityType") as ActivityEventEntity["type"] | null) ?? ""; const nextActor = searchParams.get("actor") ?? ""; const nextFrom = searchParams.get("from") ?? ""; const nextSort = readSort(searchParams.get("sort")); @@ -169,11 +183,12 @@ function ActivityPageContent() { if (nextType !== type) setType(nextType); if (nextSource !== source) setSource(nextSource); if (nextSeverity !== severity) setSeverity(nextSeverity); + if (nextEntityType !== entityType) setEntityType(nextEntityType); if (nextActor !== actor) setActor(nextActor); if (nextFrom !== from) setFrom(nextFrom); if (nextSort !== sort) setSort(nextSort); if (nextLimit !== limit) setLimit(nextLimit); - }, [actor, from, limit, searchParams, severity, sort, source, type]); + }, [actor, entityType, from, limit, searchParams, severity, sort, source, type]); const fromIso = useMemo(() => { if (!from) return undefined; @@ -197,6 +212,7 @@ function ActivityPageContent() { type: type || undefined, source: source || undefined, severity: severity || undefined, + entityType: entityType || undefined, actor: actor.trim() || undefined, from: fromIso, sort, @@ -250,17 +266,18 @@ function ActivityPageContent() { downloadCsv(csv, `activity-log-${date}.csv`); }; - const hasActiveFilters = Boolean(type || source || severity || actor.trim() || from || sort !== "newest" || limit !== 10); + const hasActiveFilters = Boolean(type || source || severity || entityType || actor.trim() || from || sort !== "newest" || limit !== 10); const clearFilters = () => { setType(""); setSource(""); setSeverity(""); + setEntityType(""); setActor(""); setFrom(""); setSort("newest"); setLimit(10); - updateActivityQuery({ type: "", source: "", severity: "", actor: "", from: "", sort: "newest", limit: 10 }); + updateActivityQuery({ type: "", source: "", severity: "", entityType: "", actor: "", from: "", sort: "newest", limit: 10 }); }; return ( @@ -282,16 +299,6 @@ function ActivityPageContent() { className="inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 shadow-sm transition-colors hover:border-slate-300 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50" title="Fetch the latest activity events" > - - -