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