import React, { useMemo, useCallback, useState } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable, SortingState, ColumnDef, } from '@tanstack/react-table'; import { format } from 'date-fns'; import { ChevronUp, ChevronDown, ExternalLink, Shield, AlertTriangle, CheckCircle, XCircle, MoreHorizontal, Eye, Link as LinkIcon, Clock, User, Database, MapPin } from 'lucide-react'; import { AuditLogEntry, AuditSeverity } from '@/types/audit'; interface VirtualizedLogTableProps { logs: AuditLogEntry[]; isLoading: boolean; hasNextPage: boolean; fetchNextPage: () => void; onRowClick: (log: AuditLogEntry) => void; onViewCorrelated?: (requestId: string) => void; onViewSession?: (sessionId: string) => void; selectedLogIds?: Set; onSelectionChange?: (selectedIds: Set) => void; } const SeverityBadge: React.FC<{ severity: AuditSeverity }> = ({ severity }) => { const className = `severity-indicator severity-${severity}`; const icon = { low: CheckCircle, medium: AlertTriangle, high: AlertTriangle, critical: XCircle }[severity]; const Icon = icon; return ( {severity.charAt(0).toUpperCase() + severity.slice(1)} ); }; const StatusBadge: React.FC<{ success: boolean }> = ({ success }) => { return ( {success ? ( ) : ( )} {success ? 'Success' : 'Failed'} ); }; const ActionCell: React.FC<{ log: AuditLogEntry }> = ({ log }) => { const actionTypeMap = { authentication: 'Auth', authorization: 'Authz', policy_evaluation: 'Policy', policy_creation: 'Create Policy', policy_update: 'Update Policy', policy_deletion: 'Delete Policy', user_creation: 'Create User', user_update: 'Update User', user_deletion: 'Delete User', role_assignment: 'Assign Role', role_revocation: 'Revoke Role', data_access: 'Data Access', data_modification: 'Data Modify', data_deletion: 'Data Delete', system_configuration: 'Config', backup_creation: 'Backup', backup_restoration: 'Restore', security_incident: 'Security', compliance_check: 'Compliance', anomaly_detection: 'Anomaly', session_start: 'Login', session_end: 'Logout', mfa_challenge: 'MFA', password_change: 'Password', api_access: 'API' }; return (
{actionTypeMap[log.action.type] || log.action.type}
{log.action.resource}
); }; const UserCell: React.FC<{ log: AuditLogEntry }> = ({ log }) => { return (
{log.user.username}
{log.user.roles.join(', ')}
); }; const ContextCell: React.FC<{ log: AuditLogEntry }> = ({ log }) => { return (
{log.context.ipAddress}
{log.context.location && (
{log.context.location.city}, {log.context.location.country}
)}
); }; const ActionsMenu: React.FC<{ log: AuditLogEntry; onViewDetails: () => void; onViewCorrelated?: (requestId: string) => void; onViewSession?: (sessionId: string) => void; }> = ({ log, onViewDetails, onViewCorrelated, onViewSession }) => { const [isOpen, setIsOpen] = useState(false); return (
{isOpen && (
  • {onViewCorrelated && (
  • )} {onViewSession && (
  • )}
)}
); }; export const VirtualizedLogTable: React.FC = ({ logs, isLoading, hasNextPage, fetchNextPage, onRowClick, onViewCorrelated, onViewSession, selectedLogIds = new Set(), onSelectionChange }) => { const [sorting, setSorting] = useState([]); const columns = useMemo[]>(() => [ { id: 'select', header: ({ table }) => ( ), cell: ({ row }) => ( { if (onSelectionChange) { const newSelection = new Set(selectedLogIds); if (e.target.checked) { newSelection.add(row.original.id); } else { newSelection.delete(row.original.id); } onSelectionChange(newSelection); } }} /> ), size: 50, }, { accessorKey: 'timestamp', header: 'Time', cell: ({ getValue }) => { const timestamp = getValue() as Date; return (
{format(timestamp, 'MMM dd, HH:mm:ss')}
{format(timestamp, 'yyyy')}
); }, size: 120, }, { id: 'user', header: 'User', cell: ({ row }) => , size: 150, }, { id: 'action', header: 'Action', cell: ({ row }) => , size: 180, }, { id: 'status', header: 'Status', cell: ({ row }) => , size: 100, }, { id: 'severity', header: 'Severity', cell: ({ row }) => , size: 120, }, { id: 'context', header: 'Context', cell: ({ row }) => , size: 140, }, { id: 'compliance', header: 'Compliance', cell: ({ row }) => { const compliance = row.original.compliance; const frameworks = []; if (compliance.soc2Relevant) frameworks.push('SOC2'); if (compliance.hipaaRelevant) frameworks.push('HIPAA'); if (compliance.pciRelevant) frameworks.push('PCI'); if (compliance.gdprRelevant) frameworks.push('GDPR'); return (
{frameworks.slice(0, 2).map((framework) => ( {framework} ))} {frameworks.length > 2 && ( +{frameworks.length - 2} )}
); }, size: 120, }, { id: 'actions', header: '', cell: ({ row }) => ( onRowClick(row.original)} onViewCorrelated={onViewCorrelated} onViewSession={onViewSession} /> ), size: 60, }, ], [selectedLogIds, onSelectionChange, onRowClick, onViewCorrelated, onViewSession]); const table = useReactTable({ data: logs, columns, state: { sorting, }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), debugTable: process.env.NODE_ENV === 'development', }); const { rows } = table.getRowModel(); // Create a parent ref for the virtualizer const parentRef = React.useRef(null); const rowVirtualizer = useVirtualizer({ count: hasNextPage ? rows.length + 1 : rows.length, getScrollElement: () => parentRef.current, estimateSize: () => 80, overscan: 10, }); // Load more data when scrolling near the end const virtualItems = rowVirtualizer.getVirtualItems(); const lastItem = virtualItems[virtualItems.length - 1]; React.useEffect(() => { if ( lastItem && lastItem.index >= rows.length - 1 && hasNextPage && !isLoading ) { fetchNextPage(); } }, [lastItem, hasNextPage, fetchNextPage, isLoading, rows.length]); const handleRowClick = useCallback((log: AuditLogEntry, event: React.MouseEvent) => { // Don't trigger row click if clicking on checkbox, dropdown, or buttons const target = event.target as HTMLElement; if (target.closest('input') || target.closest('.dropdown') || target.closest('button')) { return; } onRowClick(log); }, [onRowClick]); if (logs.length === 0 && !isLoading) { return (

No audit logs found

Try adjusting your search filters or date range.

); } return (
{/* Table Header */}
{table.getFlatHeaders().slice(1).map((header, index) => (
{flexRender(header.column.columnDef.header, header.getContext())} {header.column.getCanSort() && (
{header.column.getIsSorted() === 'asc' ? ( ) : header.column.getIsSorted() === 'desc' ? ( ) : (
)}
)}
))}
{/* Virtualized Table Body */}
{rowVirtualizer.getVirtualItems().map((virtualRow) => { const isLoaderRow = virtualRow.index > rows.length - 1; const row = rows[virtualRow.index]; return (
{isLoaderRow ? ( hasNextPage ? (
Loading more logs...
) : null ) : (
handleRowClick(row.original, e)} > {row.getVisibleCells().map((cell, cellIndex) => (
{flexRender(cell.column.columnDef.cell, cell.getContext())}
))}
)}
); })}
{/* Footer with selection info */} {selectedLogIds.size > 0 && (
{selectedLogIds.size} log{selectedLogIds.size === 1 ? '' : 's'} selected
)}
); }; export default VirtualizedLogTable;