import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
import { toast } from 'react-toastify';
import { Switch } from '~/components/ui/Switch';
import { logStore, type LogEntry } from '~/lib/stores/logs';
import { useStore } from '@nanostores/react';
import { classNames } from '~/utils/classNames';
import { motion } from 'framer-motion';
interface SelectOption {
value: string;
label: string;
icon: string;
color?: string;
}
const logLevelOptions: SelectOption[] = [
{ value: 'all', label: 'All Levels', icon: 'i-ph:funnel' },
{ value: 'info', label: 'Info', icon: 'i-ph:info', color: 'text-blue-500' },
{ value: 'warning', label: 'Warning', icon: 'i-ph:warning', color: 'text-yellow-500' },
{ value: 'error', label: 'Error', icon: 'i-ph:x-circle', color: 'text-red-500' },
{ value: 'debug', label: 'Debug', icon: 'i-ph:bug', color: 'text-gray-500' },
];
const logCategoryOptions: SelectOption[] = [
{ value: 'all', label: 'All Categories', icon: 'i-ph:squares-four' },
{ value: 'system', label: 'System', icon: 'i-ph:desktop' },
{ value: 'provider', label: 'Provider', icon: 'i-ph:plug' },
{ value: 'user', label: 'User', icon: 'i-ph:user' },
{ value: 'error', label: 'Error', icon: 'i-ph:warning-octagon' },
];
const SegmentedGroup = ({
value,
onChange,
options,
className,
}: {
value: string;
onChange: (value: string) => void;
options: SelectOption[];
className?: string;
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const selectedOption = options.find((opt) => opt.value === value);
if (!isExpanded) {
return (
);
}
return (
{options.map((option) => (
))}
);
};
const LogEntryItem = ({
log,
isExpanded: forceExpanded,
use24Hour,
}: {
log: LogEntry;
isExpanded: boolean;
use24Hour: boolean;
}) => {
const [isExpanded, setIsExpanded] = useState(forceExpanded);
const [isCopied, setIsCopied] = useState(false);
useEffect(() => {
setIsExpanded(forceExpanded);
}, [forceExpanded]);
const handleCopy = useCallback(() => {
const logText = `[${log.level.toUpperCase()}] ${log.message}\nTimestamp: ${new Date(
log.timestamp,
).toLocaleString()}\nCategory: ${log.category}\nDetails: ${JSON.stringify(log.details, null, 2)}`;
navigator.clipboard.writeText(logText).then(() => {
setIsCopied(true);
toast.success('Log copied to clipboard');
setTimeout(() => setIsCopied(false), 2000);
});
}, [log]);
const formattedTime = useMemo(() => {
const date = new Date(log.timestamp);
const now = new Date();
const isToday = date.toDateString() === now.toDateString();
const isYesterday = new Date(now.setDate(now.getDate() - 1)).toDateString() === date.toDateString();
const timeStr = date.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
hour12: !use24Hour,
});
if (isToday) {
return {
primary: timeStr,
secondary: 'Today',
};
} else if (isYesterday) {
return {
primary: timeStr,
secondary: 'Yesterday',
};
} else {
const dateStr = date.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
});
return {
primary: dateStr,
secondary: timeStr,
};
}
}, [log.timestamp, use24Hour]);
return (
{log.level}
{log.message}
{log.details && (
)}
{formattedTime.primary}
ยท
{formattedTime.secondary}
{log.category}
{isExpanded && log.details && (
{JSON.stringify(log.details, null, 2)}
)}
);
};
/**
* TODO: Future Enhancements
*
* 1. Advanced Features:
* - Add export to JSON/CSV functionality
* - Implement log retention policies
* - Add custom alert rules and notifications
* - Add pattern detection and highlighting
*
* 2. Visual Improvements:
* - Add dark/light mode specific styling
* - Implement collapsible JSON viewer
* - Add timeline view with zoom capabilities
*
* 3. Performance Optimizations:
* - Implement virtualized scrolling for large logs
* - Add lazy loading for log details
* - Optimize search with indexing
*/
export function EventLogsTab() {
const logs = useStore(logStore.logs);
const [logLevel, setLogLevel] = useState('all');
const [logCategory, setLogCategory] = useState('all');
const [autoScroll, setAutoScroll] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [expandAll, setExpandAll] = useState(false);
const [use24Hour, setUse24Hour] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const logsContainerRef = useRef(null);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
// Add refresh function
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
try {
// Since logStore doesn't have refresh, we'll re-fetch logs
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate refresh
toast.success('Logs refreshed');
} catch (err) {
console.error('Failed to refresh logs:', err);
toast.error('Failed to refresh logs');
} finally {
setIsRefreshing(false);
}
}, []);
const filteredLogs = useMemo(() => {
const allLogs = Object.values(logs);
const filtered = allLogs.filter((log) => {
const matchesLevel = logLevel === 'all' || log.level === logLevel;
const matchesCategory = logCategory === 'all' || log.category === logCategory;
const matchesSearch =
!searchQuery ||
log.message?.toLowerCase().includes(searchQuery.toLowerCase()) ||
JSON.stringify(log.details)?.toLowerCase()?.includes(searchQuery?.toLowerCase());
return matchesLevel && matchesCategory && matchesSearch;
});
return filtered.reverse();
}, [logs, logLevel, logCategory, searchQuery]);
const handleClearLogs = useCallback(() => {
if (confirm('Are you sure you want to clear all logs?')) {
logStore.clearLogs();
toast.success('Logs cleared successfully');
}
}, []);
const handleExportLogs = useCallback(() => {
try {
const logText = logStore
.getLogs()
.map(
(log) =>
`[${log.level.toUpperCase()}] ${log.timestamp} - ${log.message}${
log.details ? '\nDetails: ' + JSON.stringify(log.details, null, 2) : ''
}`,
)
.join('\n\n');
const blob = new Blob([logText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `event-logs-${new Date().toISOString()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Logs exported successfully');
} catch (error) {
toast.error('Failed to export logs');
console.error('Export error:', error);
}
}, []);
const handleScroll = () => {
const container = logsContainerRef.current;
if (!container) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = container;
const isBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
setIsScrolledToBottom(isBottom);
};
useEffect(() => {
const container = logsContainerRef.current;
if (container && (autoScroll || isScrolledToBottom)) {
container.scrollTop = container.scrollHeight;
}
}, [filteredLogs, autoScroll, isScrolledToBottom]);
return (
{/* Header Section */}
{/* Title and Refresh */}
Event Logs
Track system events and debug information
{/* Controls Section */}
Auto-scroll
24h Time
Expand All
{/* Header with Search */}
{/* Filters Row */}
setLogLevel(value as LogEntry['level'] | 'all')}
options={logLevelOptions}
/>
setLogCategory(value as LogEntry['category'] | 'all')}
options={logCategoryOptions}
/>
{/* Logs Display */}
{filteredLogs.map((log) => (
))}
{/* Status Bar */}
{filteredLogs.length} logs displayed
{isScrolledToBottom ? 'Watching for new logs...' : 'Scroll to bottom to watch new logs'}
);
}