"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
type ColumnDef,
type ColumnFiltersState,
type VisibilityState,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { getEvents, type EventLogEntry } from "@/lib/api";
import { DataTable } from "@/components/data-table/data-table";
import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header";
import { DataTableToolbar } from "@/components/data-table/data-table-toolbar";
import { CodeBlock } from "@/components/code-block";
import { SiteHeader } from "@/components/site-header";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
ResponsiveDialog,
ResponsiveDialogContent,
ResponsiveDialogHeader,
ResponsiveDialogTitle,
} from "@/components/ui/responsive-dialog";
import { ChevronLeft, ChevronRight } from "lucide-react";
function severityBadge(severity: string) {
switch (severity) {
case "error":
return error;
case "warn":
return (
warn
);
default:
return info;
}
}
function timeAgo(dateStr: string): string {
const now = Date.now();
const then = new Date(dateStr).getTime();
const seconds = Math.floor((now - then) / 1000);
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
/** Known detail keys that get their own sections — excluded from the "Other" dump. */
const KNOWN_KEYS = [
"error",
"errorType",
"method",
"line",
"input",
"params",
"response",
"script_source",
"caller_did",
"duration_ms",
"response_size",
"message",
"level",
] as const;
function EventDetailBody({ event }: { event: EventLogEntry }) {
const d = event.detail;
// Collect any keys not in KNOWN_KEYS for the "Other" section
const otherKeys = Object.keys(d).filter(
(k) => !(KNOWN_KEYS as readonly string[]).includes(k),
);
const otherDetail =
otherKeys.length > 0
? Object.fromEntries(otherKeys.map((k) => [k, d[k]]))
: null;
return (
{/* Metadata grid */}
Subject
{event.subject ?? "--"}
Actor
{event.actor_did ?? "System"}
Time
{new Date(event.created_at).toLocaleString()}
{d.method != null && (
Method
{String(d.method)}
)}
{d.caller_did != null && (
Caller DID
{String(d.caller_did)}
)}
{d.duration_ms != null && (
Duration
{String(d.duration_ms)}ms
)}
{d.response_size != null && (
Response Size
{Number(d.response_size).toLocaleString()} bytes
)}
{/* Plugin log message */}
{d.message != null && (
Message
{d.level != null && (
{String(d.level)}
)}
{String(d.message)}
)}
{/* Error section */}
{d.error != null && (
Error
{d.errorType != null && (
{String(d.errorType)}
)}
{d.line != null && (
line {String(d.line)}
)}
{String(d.error)}
)}
{/* Request input/params */}
{(d.input != null || d.params != null) && (
{d.input != null ? "Request Input" : "Request Params"}
)}
{/* Response */}
{d.response != null && (
Response
)}
{/* Script source */}
{d.script_source != null && (
Script Source
)}
{/* Other detail fields */}
{otherDetail && (
Other
)}
);
}
export default function EventsPage() {
const [events, setEvents] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [viewEvent, setViewEvent] = useState(null);
// Pagination
const [cursorStack, setCursorStack] = useState([]);
const [nextCursor, setNextCursor] = useState(null);
// Filters — driven by TanStack column filter state
const [columnFilters, setColumnFilters] = useState([]);
const [columnVisibility, setColumnVisibility] = useState({});
// Debounce subject filter to avoid firing on every keystroke
const debounceRef = useRef>(null);
const [debouncedFilters, setDebouncedFilters] =
useState(columnFilters);
useEffect(() => {
const subjectFilter = columnFilters.find((f) => f.id === "subject");
const prevSubjectFilter = debouncedFilters.find((f) => f.id === "subject");
const subjectChanged = subjectFilter?.value !== prevSubjectFilter?.value;
if (subjectChanged) {
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
setDebouncedFilters(columnFilters);
}, 300);
} else {
setDebouncedFilters(columnFilters);
}
}, [columnFilters]);
const fetchEvents = useCallback(
async (cursor?: string) => {
setLoading(true);
setError(null);
try {
const categoryFilter = debouncedFilters.find(
(f) => f.id === "event_type",
)?.value as string[] | undefined;
const severityFilter = debouncedFilters.find((f) => f.id === "severity")
?.value as string[] | undefined;
const subjectFilter = debouncedFilters.find((f) => f.id === "subject")
?.value as string | undefined;
const data = await getEvents({
category: categoryFilter?.length ? categoryFilter.join(",") : undefined,
severity: severityFilter?.length ? severityFilter.join(",") : undefined,
subject: subjectFilter || undefined,
cursor,
limit: 50,
});
setEvents(data.events);
setNextCursor(data.cursor);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : String(e));
setEvents([]);
setNextCursor(null);
} finally {
setLoading(false);
}
},
[debouncedFilters],
);
// Fetch on mount and when filters change (reset to first page)
useEffect(() => {
setCursorStack([]);
fetchEvents();
}, [fetchEvents]);
// Auto-refresh every 5s when on first page
useEffect(() => {
if (cursorStack.length > 0) return;
const interval = setInterval(() => fetchEvents(), 5000);
return () => clearInterval(interval);
}, [fetchEvents, cursorStack.length]);
function handleNext() {
if (!nextCursor) return;
setCursorStack((prev) => [...prev, nextCursor]);
fetchEvents(nextCursor);
}
function handlePrevious() {
if (cursorStack.length === 0) return;
const stack = [...cursorStack];
stack.pop();
const prevCursor = stack.length > 0 ? stack[stack.length - 1] : undefined;
setCursorStack(stack);
fetchEvents(prevCursor);
}
const columns = useMemo[]>(
() => [
{
id: "subject",
accessorKey: "subject",
header: ({ column }) => (
),
cell: ({ row }) => (
{row.original.subject ?? "--"}
),
enableColumnFilter: true,
enableSorting: false,
meta: {
label: "Subject",
placeholder: "Filter by subject...",
variant: "text",
},
},
{
id: "severity",
accessorKey: "severity",
header: ({ column }) => (
),
cell: ({ row }) => severityBadge(row.original.severity),
enableColumnFilter: true,
enableSorting: false,
enableHiding: false,
meta: {
label: "Severity",
variant: "select",
options: [
{ label: "Info", value: "info" },
{ label: "Warn", value: "warn" },
{ label: "Error", value: "error" },
],
},
},
{
id: "event_type",
accessorKey: "event_type",
header: ({ column }) => (
),
cell: ({ row }) => (
{row.original.event_type}
),
enableColumnFilter: true,
enableSorting: false,
meta: {
label: "Category",
variant: "select",
options: [
{ label: "Lexicon", value: "lexicon" },
{ label: "Record", value: "record" },
{ label: "Script", value: "script" },
{ label: "Admin", value: "admin" },
{ label: "Backfill", value: "backfill" },
{ label: "Plugin", value: "plugin" },
],
},
},
{
id: "actor_did",
accessorKey: "actor_did",
header: ({ column }) => (
),
cell: ({ row }) => (
{row.original.actor_did ?? "System"}
),
enableSorting: false,
},
{
id: "created_at",
accessorKey: "created_at",
header: ({ column }) => (
),
cell: ({ row }) => (
{timeAgo(row.original.created_at)}
),
enableSorting: false,
},
],
[],
);
const table = useReactTable({
data: events,
columns,
state: { columnFilters, columnVisibility },
defaultColumn: {
enableColumnFilter: false,
},
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getRowId: (row) => row.id,
});
return (
<>
{error &&
{error}
}
{events.length} event(s) on this page.
{viewEvent && (
setViewEvent(null)}>
{severityBadge(viewEvent.severity)}
{viewEvent.event_type}
)}
>
);
}