|
import React, { useCallback, useEffect, useState, useMemo } from 'react'; |
|
import { useSettings } from '~/lib/hooks/useSettings'; |
|
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'; |
|
|
|
export default function EventLogsTab() { |
|
const {} = useSettings(); |
|
const showLogs = useStore(logStore.showLogs); |
|
const [logLevel, setLogLevel] = useState<LogEntry['level'] | 'all'>('info'); |
|
const [autoScroll, setAutoScroll] = useState(true); |
|
const [searchQuery, setSearchQuery] = useState(''); |
|
const [, forceUpdate] = useState({}); |
|
|
|
const filteredLogs = useMemo(() => { |
|
const logs = logStore.getLogs(); |
|
return logs.filter((log) => { |
|
const matchesLevel = !logLevel || log.level === logLevel || logLevel === 'all'; |
|
const matchesSearch = |
|
!searchQuery || |
|
log.message?.toLowerCase().includes(searchQuery.toLowerCase()) || |
|
JSON.stringify(log.details)?.toLowerCase()?.includes(searchQuery?.toLowerCase()); |
|
|
|
return matchesLevel && matchesSearch; |
|
}); |
|
}, [logLevel, searchQuery]); |
|
|
|
|
|
useEffect(() => { |
|
logStore.showLogs.set(true); |
|
}, []); |
|
|
|
useEffect(() => { |
|
|
|
logStore.logSystem('Application initialized', { |
|
version: process.env.NEXT_PUBLIC_APP_VERSION, |
|
environment: process.env.NODE_ENV, |
|
}); |
|
|
|
|
|
logStore.logDebug('System configuration loaded', { |
|
runtime: 'Next.js', |
|
features: ['AI Chat', 'Event Logging'], |
|
}); |
|
|
|
|
|
logStore.logWarning('Resource usage threshold approaching', { |
|
memoryUsage: '75%', |
|
cpuLoad: '60%', |
|
}); |
|
|
|
|
|
logStore.logError('API connection failed', new Error('Connection timeout'), { |
|
endpoint: '/api/chat', |
|
retryCount: 3, |
|
lastAttempt: new Date().toISOString(), |
|
}); |
|
}, []); |
|
|
|
useEffect(() => { |
|
const container = document.querySelector('.logs-container'); |
|
|
|
if (container && autoScroll) { |
|
container.scrollTop = container.scrollHeight; |
|
} |
|
}, [filteredLogs, autoScroll]); |
|
|
|
const handleClearLogs = useCallback(() => { |
|
if (confirm('Are you sure you want to clear all logs?')) { |
|
logStore.clearLogs(); |
|
toast.success('Logs cleared successfully'); |
|
forceUpdate({}); |
|
} |
|
}, []); |
|
|
|
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 getLevelColor = (level: LogEntry['level']) => { |
|
switch (level) { |
|
case 'info': |
|
return 'text-blue-500'; |
|
case 'warning': |
|
return 'text-yellow-500'; |
|
case 'error': |
|
return 'text-red-500'; |
|
case 'debug': |
|
return 'text-gray-500'; |
|
default: |
|
return 'text-bolt-elements-textPrimary'; |
|
} |
|
}; |
|
|
|
return ( |
|
<div className="p-4 h-full flex flex-col"> |
|
<div className="flex flex-col space-y-4 mb-4"> |
|
{/* Title and Toggles Row */} |
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"> |
|
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Event Logs</h3> |
|
<div className="flex flex-wrap items-center gap-4"> |
|
<div className="flex items-center space-x-2"> |
|
<span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Show Actions</span> |
|
<Switch checked={showLogs} onCheckedChange={(checked) => logStore.showLogs.set(checked)} /> |
|
</div> |
|
<div className="flex items-center space-x-2"> |
|
<span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Auto-scroll</span> |
|
<Switch checked={autoScroll} onCheckedChange={setAutoScroll} /> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
{/* Controls Row */} |
|
<div className="flex flex-wrap items-center gap-2"> |
|
<select |
|
value={logLevel} |
|
onChange={(e) => setLogLevel(e.target.value as LogEntry['level'])} |
|
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[20%] text-sm min-w-[100px]" |
|
> |
|
<option value="all">All</option> |
|
<option value="info">Info</option> |
|
<option value="warning">Warning</option> |
|
<option value="error">Error</option> |
|
<option value="debug">Debug</option> |
|
</select> |
|
<div className="flex-1 min-w-[200px]"> |
|
<input |
|
type="text" |
|
placeholder="Search logs..." |
|
value={searchQuery} |
|
onChange={(e) => setSearchQuery(e.target.value)} |
|
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" |
|
/> |
|
</div> |
|
{showLogs && ( |
|
<div className="flex items-center gap-2 flex-nowrap"> |
|
<button |
|
onClick={handleExportLogs} |
|
className={classNames( |
|
'bg-bolt-elements-button-primary-background', |
|
'rounded-lg px-4 py-2 transition-colors duration-200', |
|
'hover:bg-bolt-elements-button-primary-backgroundHover', |
|
'text-bolt-elements-button-primary-text', |
|
)} |
|
> |
|
Export Logs |
|
</button> |
|
<button |
|
onClick={handleClearLogs} |
|
className={classNames( |
|
'bg-bolt-elements-button-danger-background', |
|
'rounded-lg px-4 py-2 transition-colors duration-200', |
|
'hover:bg-bolt-elements-button-danger-backgroundHover', |
|
'text-bolt-elements-button-danger-text', |
|
)} |
|
> |
|
Clear Logs |
|
</button> |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
|
|
<div className="bg-bolt-elements-bg-depth-1 rounded-lg p-4 h-[calc(100vh - 250px)] min-h-[400px] overflow-y-auto logs-container overflow-y-auto"> |
|
{filteredLogs.length === 0 ? ( |
|
<div className="text-center text-bolt-elements-textSecondary py-8">No logs found</div> |
|
) : ( |
|
filteredLogs.map((log, index) => ( |
|
<div |
|
key={index} |
|
className="text-sm mb-3 font-mono border-b border-bolt-elements-borderColor pb-2 last:border-0" |
|
> |
|
<div className="flex items-start space-x-2 flex-wrap"> |
|
<span className={`font-bold ${getLevelColor(log.level)} whitespace-nowrap`}> |
|
[{log.level.toUpperCase()}] |
|
</span> |
|
<span className="text-bolt-elements-textSecondary whitespace-nowrap"> |
|
{new Date(log.timestamp).toLocaleString()} |
|
</span> |
|
<span className="text-bolt-elements-textPrimary break-all">{log.message}</span> |
|
</div> |
|
{log.details && ( |
|
<pre className="mt-2 text-xs text-bolt-elements-textSecondary overflow-x-auto whitespace-pre-wrap break-all"> |
|
{JSON.stringify(log.details, null, 2)} |
|
</pre> |
|
)} |
|
</div> |
|
)) |
|
)} |
|
</div> |
|
</div> |
|
); |
|
} |
|
|