Files
dashboard/ui/src/pages/Logs.tsx

185 lines
6.1 KiB
TypeScript

import { useState, useEffect, useRef } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Terminal, RefreshCw, Download, Pause, Play } from 'lucide-react'
import { Card, CardHeader } from '../components/Card'
import { fetchSkills, fetchLogs } from '../lib/api'
export function Logs() {
const [selectedSkill, setSelectedSkill] = useState<string>('all')
const [isPaused, setIsPaused] = useState(false)
const [filter, setFilter] = useState('')
const logsEndRef = useRef<HTMLDivElement>(null)
const { data: skills } = useQuery({
queryKey: ['skills'],
queryFn: fetchSkills,
})
const { data: logs, refetch } = useQuery({
queryKey: ['logs', selectedSkill],
queryFn: () => fetchLogs(selectedSkill, 200),
refetchInterval: isPaused ? false : 2000,
})
useEffect(() => {
if (!isPaused) {
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
}, [logs, isPaused])
const filteredLogs = logs?.filter(line =>
filter ? line.toLowerCase().includes(filter.toLowerCase()) : true
)
const downloadLogs = () => {
if (!logs) return
const blob = new Blob([logs.join('\n')], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${selectedSkill}-logs-${new Date().toISOString()}.txt`
a.click()
URL.revokeObjectURL(url)
}
return (
<div className="space-y-6 h-full flex flex-col">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-white">Logs</h1>
<div className="flex items-center gap-2">
<button
onClick={() => setIsPaused(!isPaused)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${
isPaused
? 'bg-green-600 hover:bg-green-700 text-white'
: 'bg-yellow-600 hover:bg-yellow-700 text-white'
}`}
>
{isPaused ? (
<>
<Play className="w-4 h-4" />
Resume
</>
) : (
<>
<Pause className="w-4 h-4" />
Pause
</>
)}
</button>
<button
onClick={() => refetch()}
className="flex items-center gap-2 px-3 py-2 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg transition-colors"
>
<RefreshCw className="w-4 h-4" />
Refresh
</button>
<button
onClick={downloadLogs}
className="flex items-center gap-2 px-3 py-2 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg transition-colors"
>
<Download className="w-4 h-4" />
Download
</button>
</div>
</div>
{/* Filters */}
<Card className="flex-shrink-0">
<div className="flex gap-4">
<div className="flex-1">
<label className="block text-xs text-gray-500 mb-1">Skill</label>
<select
value={selectedSkill}
onChange={(e) => setSelectedSkill(e.target.value)}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-purple-500"
>
<option value="all">All Skills</option>
{skills?.map((skill) => (
<option key={skill.name} value={skill.name}>
{skill.name}
</option>
))}
</select>
</div>
<div className="flex-1">
<label className="block text-xs text-gray-500 mb-1">Filter</label>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter logs..."
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-purple-500"
/>
</div>
</div>
</Card>
{/* Log Output */}
<Card className="flex-1 min-h-0 flex flex-col">
<CardHeader
title={
<div className="flex items-center gap-2">
<Terminal className="w-4 h-4 text-gray-500" />
<span>Output</span>
{!isPaused && (
<span className="flex items-center gap-1 text-xs text-green-400">
<span className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
Live
</span>
)}
</div>
}
/>
<div className="flex-1 bg-gray-950 rounded-lg p-4 font-mono text-xs overflow-auto">
{filteredLogs?.length === 0 ? (
<p className="text-gray-500">No logs available</p>
) : (
<>
{filteredLogs?.map((line, i) => (
<LogLine key={i} line={line} filter={filter} />
))}
<div ref={logsEndRef} />
</>
)}
</div>
</Card>
</div>
)
}
function LogLine({ line, filter }: { line: string; filter: string }) {
// Detect log level
const isError = /error|exception|fail/i.test(line)
const isWarning = /warn/i.test(line)
const isInfo = /info/i.test(line)
const isDebug = /debug/i.test(line)
let colorClass = 'text-gray-400'
if (isError) colorClass = 'text-red-400'
else if (isWarning) colorClass = 'text-yellow-400'
else if (isInfo) colorClass = 'text-blue-400'
else if (isDebug) colorClass = 'text-gray-500'
// Highlight filter matches
if (filter) {
const regex = new RegExp(`(${filter})`, 'gi')
const parts = line.split(regex)
return (
<div className={`${colorClass} hover:bg-gray-900/50`}>
{parts.map((part, i) =>
part.toLowerCase() === filter.toLowerCase() ? (
<span key={i} className="bg-yellow-500/30 text-yellow-200">
{part}
</span>
) : (
part
)
)}
</div>
)
}
return <div className={`${colorClass} hover:bg-gray-900/50`}>{line}</div>
}