feat: initial commit
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Terminal, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { getRenderLog } from '../api/worker'
|
||||
import type { RenderLogEntry } from '../api/worker'
|
||||
|
||||
const LEVEL_COLORS: Record<string, string> = {
|
||||
info: 'text-gray-300',
|
||||
error: 'text-red-400',
|
||||
success: 'text-green-400',
|
||||
warn: 'text-yellow-400',
|
||||
}
|
||||
|
||||
/**
|
||||
* Live render log panel — polls Redis-backed log entries every 2s.
|
||||
* Shows a compact terminal-style output for a render job.
|
||||
*
|
||||
* Always does an initial fetch to check if entries exist (so failed jobs
|
||||
* still show their log). Polls only when isActive.
|
||||
*/
|
||||
export default function LiveRenderLog({
|
||||
orderLineId,
|
||||
isActive,
|
||||
compact = false,
|
||||
}: {
|
||||
orderLineId: string
|
||||
/** Whether the render is still processing — enables polling */
|
||||
isActive: boolean
|
||||
/** Compact mode (inline, no border) for table rows */
|
||||
compact?: boolean
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(isActive)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Always fetch once to probe for existing entries; poll only when active
|
||||
const { data } = useQuery({
|
||||
queryKey: ['render-log', orderLineId],
|
||||
queryFn: () => getRenderLog(orderLineId),
|
||||
refetchInterval: isActive ? 2000 : false,
|
||||
})
|
||||
|
||||
const entries: RenderLogEntry[] = data?.entries ?? []
|
||||
const hasEntries = entries.length > 0
|
||||
|
||||
// Auto-scroll to bottom when new entries arrive
|
||||
useEffect(() => {
|
||||
if (scrollRef.current && isActive) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
||||
}
|
||||
}, [entries.length, isActive])
|
||||
|
||||
// Auto-expand when active
|
||||
useEffect(() => {
|
||||
if (isActive) setExpanded(true)
|
||||
}, [isActive])
|
||||
|
||||
// Nothing to show at all
|
||||
if (!hasEntries && !isActive) return null
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="mt-1">
|
||||
<button
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="text-[10px] text-gray-400 hover:text-gray-600 flex items-center gap-1"
|
||||
>
|
||||
<Terminal size={10} />
|
||||
Log ({entries.length})
|
||||
{expanded ? <ChevronUp size={10} /> : <ChevronDown size={10} />}
|
||||
</button>
|
||||
{expanded && hasEntries && (
|
||||
<LogPanel entries={entries} isActive={isActive} scrollRef={scrollRef} maxHeight="120px" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<button
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="flex items-center gap-1.5 text-xs text-gray-500 hover:text-gray-700 mb-1"
|
||||
>
|
||||
<Terminal size={12} />
|
||||
<span className="font-medium">Render Log</span>
|
||||
<span className="text-gray-400">({entries.length} entries)</span>
|
||||
{expanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||
</button>
|
||||
{expanded && (
|
||||
<LogPanel entries={entries} isActive={isActive} scrollRef={scrollRef} maxHeight="200px" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LogPanel({
|
||||
entries,
|
||||
isActive,
|
||||
scrollRef,
|
||||
maxHeight,
|
||||
}: {
|
||||
entries: RenderLogEntry[]
|
||||
isActive: boolean
|
||||
scrollRef: React.RefObject<HTMLDivElement | null>
|
||||
maxHeight: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="bg-gray-900 rounded-md p-2 overflow-y-auto font-mono text-[11px] leading-relaxed"
|
||||
style={{ maxHeight }}
|
||||
>
|
||||
{entries.map((entry, i) => (
|
||||
<div key={i} className={`flex gap-2 ${LEVEL_COLORS[entry.level] || LEVEL_COLORS.info}`}>
|
||||
<span className="text-gray-500 shrink-0 select-none">{entry.t}</span>
|
||||
<span>{entry.msg}</span>
|
||||
</div>
|
||||
))}
|
||||
{isActive && entries.length > 0 && (
|
||||
<div className="text-gray-500 animate-pulse">...</div>
|
||||
)}
|
||||
{entries.length === 0 && (
|
||||
<div className="text-gray-600 italic">No log entries yet</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user