Portal Community

Why Virtual Scrolling

Without virtual scrolling, a workflow that emits 50,000 log entries would create 50,000 DOM rows simultaneously — this would freeze the browser. With virtual scrolling, only the ~30 rows visible in the panel are rendered; the rest are calculated but not mounted.

VirtualLogList Implementation

// VirtualLogList.tsx — uses @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';

interface VirtualLogListProps {
    logs: LogEntry[];
}

export function VirtualLogList({ logs }: VirtualLogListProps) {
    const containerRef = useRef<HTMLDivElement>(null);

    const virtualizer = useVirtualizer({
        count       : logs.length,
        getScrollElement: () => containerRef.current,
        estimateSize: () => 24,      // Estimated row height in px
        overscan    : 10             // Render 10 extra rows above/below viewport
    });

    return (
        <div ref={containerRef} style={{ height: '100%', overflow: 'auto' }}>
            <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
                {virtualizer.getVirtualItems().map(item => (
                    <div
                        key={logs[item.index].logId}
                        style={{
                            position : 'absolute',
                            top      : `${item.start}px`,
                            height   : `${item.size}px`,
                            width    : '100%'
                        }}
                    >
                        <LogRow entry={logs[item.index]} />
                    </div>
                ))}
            </div>
        </div>
    );
}

Auto-Scroll to Bottom

// Auto-scroll to newest entries during live run
useEffect(() => {
    if (isLive && !userHasScrolledUp) {
        virtualizer.scrollToIndex(logs.length - 1, { behavior: 'smooth' });
    }
}, [logs.length]);
Filtering happens before the virtualizer sees the data. The Logs tab filters the logs array in memory first (by level, nodeId, text search), then passes the filtered result to VirtualLogList. The virtualizer always receives a pre-filtered slice — this keeps rendering logic clean.