import { useState, useMemo, useCallback, useRef, useEffect } from 'react' import { useQuery } from '@tanstack/react-query' import { Search, RefreshCw } from 'lucide-react' import { networkApi, type LogEntry } from '@/lib/networkApi' import { LoadingText } from '@/components/ui/skeleton' interface LogsTabProps { tenantId: string deviceId: string active: boolean } /** Common RouterOS log topics and their severity colors. */ function getTopicColor(topics: string): { bg: string; text: string } { const t = topics.toLowerCase() if (t.includes('critical') || t.includes('error')) { return { bg: 'bg-error/10', text: 'text-error' } } if (t.includes('warning')) { return { bg: 'bg-warning/10', text: 'text-warning' } } if (t.includes('info')) { return { bg: 'bg-accent/10', text: 'text-accent' } } return { bg: 'bg-elevated', text: 'text-text-muted' } } /** Whether a log entry has error/critical severity. */ function isErrorEntry(topics: string): boolean { const t = topics.toLowerCase() return t.includes('error') || t.includes('critical') } const TOPIC_OPTIONS = [ 'system', 'firewall', 'dhcp', 'wireless', 'interface', 'error', 'warning', 'info', 'critical', 'dns', 'ppp', 'ipsec', 'wireguard', 'ospf', 'bgp', ] const LIMIT_OPTIONS = [50, 100, 200, 500] function TopicBadge({ topics }: { topics: string }) { const colors = getTopicColor(topics) return ( {topics} ) } function TableLoading() { return (
) } export function LogsTab({ tenantId, deviceId, active }: LogsTabProps) { const [searchInput, setSearchInput] = useState('') const [searchQuery, setSearchQuery] = useState('') const [selectedTopic, setSelectedTopic] = useState('') const [limit, setLimit] = useState(100) const [autoRefresh, setAutoRefresh] = useState(false) const debounceRef = useRef | null>(null) // Debounce search input const handleSearchChange = useCallback((value: string) => { setSearchInput(value) if (debounceRef.current) clearTimeout(debounceRef.current) debounceRef.current = setTimeout(() => { setSearchQuery(value) }, 300) }, []) // Clean up debounce on unmount useEffect(() => { return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, []) const { data, isLoading, error } = useQuery({ queryKey: ['device-logs', tenantId, deviceId, limit, selectedTopic, searchQuery], queryFn: () => networkApi.getDeviceLogs(tenantId, deviceId, { limit, topic: selectedTopic || undefined, search: searchQuery || undefined, }), refetchInterval: active && autoRefresh ? 10_000 : false, enabled: active, }) // Extract unique topics from data for reference const uniqueTopics = useMemo(() => { if (!data?.logs) return TOPIC_OPTIONS const fromData = new Set() for (const entry of data.logs) { if (entry.topics) { for (const t of entry.topics.split(',')) { fromData.add(t.trim()) } } } // Merge with common topics, deduplicate const all = new Set([...TOPIC_OPTIONS, ...fromData]) return [...all].sort() }, [data]) return (
{/* Controls bar */}
{/* Search */}
handleSearchChange(e.target.value)} className="w-full pl-8 pr-3 py-1.5 text-xs rounded border border-border bg-elevated/50 text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-1 focus:ring-accent [color-scheme:dark]" />
{/* Topic filter */} {/* Limit selector */} {/* Auto-refresh toggle */}
{/* Log table */}
{isLoading ? ( ) : error ? (
Failed to fetch device logs. The device may be offline or unreachable.
) : !data || data.logs.length === 0 ? (

No log entries found

{searchQuery || selectedTopic ? 'Try adjusting your search or topic filter.' : 'Device returned no logs.'}

) : (
{data.logs.map((entry: LogEntry, i: number) => { const errorRow = isErrorEntry(entry.topics) return ( ) })}
Time Topics Message
{entry.time} {entry.message}
)}
{/* Entry count */} {data && data.count > 0 && (
Showing {data.count} entr{data.count === 1 ? 'y' : 'ies'} {autoRefresh && ' (auto-refreshing every 10s)'}
)}
) }