JSX element rendering the event logger panel
export function EventLogger(): React.ReactElement {
const { eventLogEntries, maxEventLogEntries } = useDebugState();
const { clearEventLog, recordEvent } = useDebugActions();
const [activeTypes, setActiveTypes] = useState<string[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [visibleCount, setVisibleCount] = useState(DEFAULT_VISIBLE_COUNT);
const searchInputId = useId();
// Listen for custom events from PerformanceMonitor to set filter
useEffect(() => {
const handleSetFilter = (event: CustomEvent) => {
const detail = event.detail as { types?: string[] } | undefined;
if (detail?.types) {
// Set type filter when custom event is triggered
setActiveTypes(detail.types);
setSearchTerm(""); // Clear search to focus on type filter
setVisibleCount(DEFAULT_VISIBLE_COUNT);
}
};
globalThis.addEventListener(
"debug:events:set-filter" as unknown as string,
handleSetFilter as EventListener,
);
return () => {
globalThis.removeEventListener(
"debug:events:set-filter" as unknown as string,
handleSetFilter as EventListener,
);
};
}, []);
const availableTypes = useMemo(() => {
const unique = new Set<string>();
for (const entry of eventLogEntries) {
unique.add(entry.type);
}
return Array.from(unique).sort((a, b) => a.localeCompare(b));
}, [eventLogEntries]);
const filteredEvents = useMemo(() => {
const query = searchTerm.trim().toLowerCase();
return eventLogEntries.filter(
(entry) =>
eventMatchesType(entry, activeTypes) &&
eventMatchesSearch(entry, query),
);
}, [activeTypes, eventLogEntries, searchTerm]);
const sortedEvents = useMemo(() => {
return [...filteredEvents].sort((a, b) => {
const aTime = Date.parse(a.timestamp);
const bTime = Date.parse(b.timestamp);
const aValid = !Number.isNaN(aTime);
const bValid = !Number.isNaN(bTime);
// Sort by parsed timestamp (newest first), fallback to string compare if parse fails
if (aValid && bValid) {
return bTime - aTime;
}
// Prioritize valid dates over invalid ones
if (aValid) return -1;
if (bValid) return 1;
return b.timestamp.localeCompare(a.timestamp);
});
}, [filteredEvents]);
const visibleEvents = useMemo(() => {
return sortedEvents.slice(0, visibleCount);
}, [sortedEvents, visibleCount]);
const handleClear = () => {
if (!eventLogEntries.length) {
toast.info("Event log already empty");
return;
}
clearEventLog();
setVisibleCount(DEFAULT_VISIBLE_COUNT);
toast.success("Event log cleared");
recordEvent({
type: "debug.event-logger",
message: "Event log cleared",
level: "warn",
});
};
const handleExport = async () => {
if (!filteredEvents.length) {
toast.info("Nothing to export for current filters");
return;
}
try {
const payload = {
exportedAt: new Date().toISOString(),
totalEntries: filteredEvents.length,
filters: {
types: activeTypes.length ? activeTypes : undefined,
search: searchTerm || undefined,
},
events: filteredEvents,
};
await exportToJson(payload, "kenmei-event-log");
toast.success("Exported filtered events");
recordEvent({
type: "debug.event-logger",
message: "Exported event log snapshot",
level: "info",
metadata: { totalEntries: filteredEvents.length },
});
} catch (error) {
console.error("Failed to export event log", error);
toast.error("Unable to export events");
recordEvent(
{
type: "debug.event-logger",
message: "Event log export failed",
level: "error",
metadata: {
error: error instanceof Error ? error.message : String(error),
},
},
{ force: true },
);
}
};
const resetFilters = () => {
setActiveTypes([]);
setSearchTerm("");
setVisibleCount(DEFAULT_VISIBLE_COUNT);
};
const loadMore = () =>
setVisibleCount((value) => value + DEFAULT_VISIBLE_COUNT);
const resetVisibleWindow = () => setVisibleCount(DEFAULT_VISIBLE_COUNT);
return (
<div className="flex h-full flex-col gap-4">
<EventLoggerHeader
filteredCount={filteredEvents.length}
totalCount={eventLogEntries.length}
maxEntries={maxEventLogEntries}
availableTypesCount={availableTypes.length}
activeTypesCount={activeTypes.length}
/>
<FilterControls
availableTypes={availableTypes}
activeTypes={activeTypes}
onTypesChange={setActiveTypes}
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
searchInputId={searchInputId}
onResetFilters={resetFilters}
onExport={handleExport}
onClear={handleClear}
/>
<Separator />
<PaginationControls
visibleCount={visibleEvents.length}
filteredCount={filteredEvents.length}
totalVisible={visibleCount}
totalSorted={sortedEvents.length}
defaultCount={DEFAULT_VISIBLE_COUNT}
onLoadMore={loadMore}
onResetWindow={resetVisibleWindow}
/>
<EventList events={visibleEvents} />
</div>
);
}
Event log viewer with filtering, search, pagination, and export functionality. Displays application and user action events with severity levels and metadata.