LogsTab.tsx raw

   1  import { useCallback, useEffect, useRef, useState } from 'react'
   2  import relayAdmin from '@/services/relay-admin.service'
   3  import { Button } from '@/components/ui/button'
   4  import { cn } from '@/lib/utils'
   5  import { toast } from 'sonner'
   6  
   7  interface LogEntry {
   8    timestamp: string
   9    level: string
  10    message: string
  11    file?: string
  12    line?: number
  13  }
  14  
  15  const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']
  16  
  17  function levelColor(level: string): string {
  18    switch (level?.toUpperCase()) {
  19      case 'TRC':
  20      case 'TRACE':
  21        return 'bg-gray-500 text-white'
  22      case 'DBG':
  23      case 'DEBUG':
  24        return 'bg-cyan-600 text-white'
  25      case 'INF':
  26      case 'INFO':
  27        return 'bg-green-600 text-white'
  28      case 'WRN':
  29      case 'WARN':
  30        return 'bg-yellow-500 text-black'
  31      case 'ERR':
  32      case 'ERROR':
  33        return 'bg-red-600 text-white'
  34      case 'FTL':
  35      case 'FATAL':
  36        return 'bg-red-900 text-white'
  37      default:
  38        return 'bg-green-600 text-white'
  39    }
  40  }
  41  
  42  export default function LogsTab() {
  43    const [logs, setLogs] = useState<LogEntry[]>([])
  44    const [isLoading, setIsLoading] = useState(false)
  45    const [hasMore, setHasMore] = useState(true)
  46    const [totalLogs, setTotalLogs] = useState(0)
  47    const [currentLevel, setCurrentLevel] = useState('info')
  48    const [selectedLevel, setSelectedLevel] = useState('info')
  49    const [error, setError] = useState('')
  50    const offsetRef = useRef(0)
  51    const triggerRef = useRef<HTMLDivElement>(null)
  52  
  53    const loadLogs = useCallback(
  54      async (refresh = false) => {
  55        if (isLoading) return
  56        setIsLoading(true)
  57        setError('')
  58  
  59        if (refresh) {
  60          offsetRef.current = 0
  61          setLogs([])
  62        }
  63  
  64        try {
  65          const data = await relayAdmin.getLogs(
  66            String(offsetRef.current),
  67            100
  68          ) as { logs?: LogEntry[]; total?: number; has_more?: boolean }
  69          const newLogs = data.logs || []
  70          if (refresh) {
  71            setLogs(newLogs)
  72          } else {
  73            setLogs((prev) => [...prev, ...newLogs])
  74          }
  75          setTotalLogs(data.total || 0)
  76          setHasMore(data.has_more || false)
  77          offsetRef.current += newLogs.length
  78        } catch (e) {
  79          setError(e instanceof Error ? e.message : 'Failed to load logs')
  80        } finally {
  81          setIsLoading(false)
  82        }
  83      },
  84      [isLoading]
  85    )
  86  
  87    useEffect(() => {
  88      loadLogs(true)
  89      relayAdmin.getLogLevel().then((level) => {
  90        setCurrentLevel(level)
  91        setSelectedLevel(level)
  92      })
  93      // eslint-disable-next-line react-hooks/exhaustive-deps
  94    }, [])
  95  
  96    // IntersectionObserver for infinite scroll
  97    useEffect(() => {
  98      const el = triggerRef.current
  99      if (!el) return
 100      const observer = new IntersectionObserver(
 101        (entries) => {
 102          if (entries[0].isIntersecting && hasMore && !isLoading) {
 103            loadLogs(false)
 104          }
 105        },
 106        { threshold: 0.1 }
 107      )
 108      observer.observe(el)
 109      return () => observer.disconnect()
 110    }, [hasMore, isLoading, loadLogs])
 111  
 112    const handleLevelChange = async (level: string) => {
 113      setSelectedLevel(level)
 114      if (level === currentLevel) return
 115      try {
 116        await relayAdmin.setLogLevel(level)
 117        setCurrentLevel(level)
 118        toast.success(`Log level set to ${level}`)
 119      } catch (e) {
 120        setSelectedLevel(currentLevel)
 121        toast.error(`Failed to set log level: ${e instanceof Error ? e.message : String(e)}`)
 122      }
 123    }
 124  
 125    const handleClear = async () => {
 126      if (!confirm('Clear all logs?')) return
 127      try {
 128        await relayAdmin.clearLogs()
 129        setLogs([])
 130        setTotalLogs(0)
 131        setHasMore(false)
 132        offsetRef.current = 0
 133        toast.success('Logs cleared')
 134      } catch (e) {
 135        toast.error(`Failed: ${e instanceof Error ? e.message : String(e)}`)
 136      }
 137    }
 138  
 139    return (
 140      <div className="p-4 space-y-3 w-full">
 141        <div className="flex flex-wrap items-center justify-between gap-2">
 142          <h3 className="text-lg font-semibold">Logs</h3>
 143          <div className="flex items-center gap-2 flex-wrap">
 144            <label className="text-sm text-muted-foreground">Level:</label>
 145            <select
 146              value={selectedLevel}
 147              onChange={(e) => handleLevelChange(e.target.value)}
 148              className="rounded-md border bg-card px-2 py-1 text-sm"
 149            >
 150              {LOG_LEVELS.map((l) => (
 151                <option key={l} value={l}>
 152                  {l}
 153                </option>
 154              ))}
 155            </select>
 156            <Button variant="outline" size="sm" onClick={handleClear} disabled={isLoading || logs.length === 0}>
 157              Clear
 158            </Button>
 159            <Button size="sm" onClick={() => loadLogs(true)} disabled={isLoading}>
 160              {isLoading ? 'Loading...' : 'Refresh'}
 161            </Button>
 162          </div>
 163        </div>
 164  
 165        {error && <div className="rounded-md bg-destructive/10 text-destructive p-3 text-sm">{error}</div>}
 166  
 167        <div className="text-xs text-muted-foreground">
 168          Showing {logs.length} of {totalLogs} logs (Level: {currentLevel})
 169        </div>
 170  
 171        <div className="space-y-1">
 172          {logs.length === 0 && !isLoading ? (
 173            <div className="text-center py-8 text-muted-foreground">No logs available.</div>
 174          ) : (
 175            logs.map((log, i) => (
 176              <div
 177                key={i}
 178                className="flex items-start gap-2 rounded-md bg-card px-3 py-2 font-mono text-xs"
 179              >
 180                <span className="text-muted-foreground whitespace-nowrap shrink-0">
 181                  {log.timestamp ? new Date(log.timestamp).toLocaleString() : ''}
 182                </span>
 183                <span
 184                  className={cn(
 185                    'rounded px-1.5 py-0.5 text-center font-bold uppercase shrink-0 min-w-[3.5em]',
 186                    levelColor(log.level)
 187                  )}
 188                >
 189                  {log.level}
 190                </span>
 191                {log.file && (
 192                  <span className="text-muted-foreground/50 shrink-0">
 193                    {log.file}:{log.line}
 194                  </span>
 195                )}
 196                <span className="flex-1 break-all">{log.message}</span>
 197              </div>
 198            ))
 199          )}
 200          <div ref={triggerRef} className="text-center py-4 text-xs text-muted-foreground">
 201            {isLoading ? 'Loading more...' : hasMore ? 'Scroll for more' : logs.length > 0 ? 'End of logs' : ''}
 202          </div>
 203        </div>
 204      </div>
 205    )
 206  }
 207