@@ -119,14 +119,20 @@ function LogsContent({
119119 const { isRefetchingFromFilterChange, onFetchComplete } =
120120 useFilterRefetchTracking ( level )
121121
122- const { logs, hasNextPage, isFetchingNextPage, isFetching, fetchNextPage } =
123- useBuildLogs ( {
124- teamIdOrSlug,
125- templateId,
126- buildId,
127- level,
128- buildStatus : buildDetails . status ,
129- } )
122+ const {
123+ logs,
124+ isInitialized,
125+ hasNextPage,
126+ isFetchingNextPage,
127+ isFetching,
128+ fetchNextPage,
129+ } = useBuildLogs ( {
130+ teamIdOrSlug,
131+ templateId,
132+ buildId,
133+ level,
134+ buildStatus : buildDetails . status ,
135+ } )
130136
131137 useEffect ( ( ) => {
132138 if ( ! isFetching && isRefetchingFromFilterChange ) {
@@ -166,6 +172,8 @@ function LogsContent({
166172 hasNextPage = { hasNextPage }
167173 isFetchingNextPage = { isFetchingNextPage }
168174 showRefetchOverlay = { showRefetchOverlay }
175+ isInitialized = { isInitialized }
176+ level = { level }
169177 />
170178 ) }
171179 </ Table >
@@ -202,7 +210,7 @@ function LogsTableHeader() {
202210 className = "text-fg"
203211 style = { { display : 'flex' , width : COLUMN_WIDTHS_PX . timestamp } }
204212 >
205- Timestamp < ArrowDownIcon className = "size-3" />
213+ Timestamp < ArrowDownIcon className = "size-3 rotate-180 " />
206214 </ TableHead >
207215 < TableHead style = { { display : 'flex' , width : COLUMN_WIDTHS_PX . level } } >
208216 Level
@@ -319,6 +327,8 @@ interface VirtualizedLogsBodyProps {
319327 hasNextPage : boolean
320328 isFetchingNextPage : boolean
321329 showRefetchOverlay : boolean
330+ isInitialized : boolean
331+ level : LogLevelFilter | null
322332}
323333
324334function VirtualizedLogsBody ( {
@@ -329,6 +339,8 @@ function VirtualizedLogsBody({
329339 hasNextPage,
330340 isFetchingNextPage,
331341 showRefetchOverlay,
342+ isInitialized,
343+ level,
332344} : VirtualizedLogsBodyProps ) {
333345 const tbodyRef = useRef < HTMLTableSectionElement > ( null )
334346 const maxWidthRef = useRef < number > ( 0 )
@@ -345,49 +357,73 @@ function VirtualizedLogsBody({
345357 onLoadMore,
346358 } )
347359
360+ useAutoScrollToBottom ( {
361+ scrollContainerRef,
362+ logsCount : logs . length ,
363+ isInitialized,
364+ level,
365+ } )
366+
367+ useMaintainScrollOnPrepend ( {
368+ scrollContainerRef,
369+ logsCount : logs . length ,
370+ isFetchingNextPage,
371+ } )
372+
373+ const showStatusRow = hasNextPage || isFetchingNextPage
374+
348375 const virtualizer = useVirtualizer ( {
349- count : logs . length + 1 ,
376+ count : logs . length + ( showStatusRow ? 1 : 0 ) ,
350377 estimateSize : ( ) => ROW_HEIGHT_PX ,
351378 getScrollElement : ( ) => scrollContainerRef . current ,
352379 overscan : VIRTUAL_OVERSCAN ,
353380 } )
354381
355- const currentScrollWidth = tbodyRef . current ?. scrollWidth ?? 0
356- if ( currentScrollWidth > maxWidthRef . current ) {
357- maxWidthRef . current = currentScrollWidth
382+ const containerWidth = scrollContainerRef . current ?. clientWidth ?? 0
383+ const contentWidth = scrollContainerRef . current ?. scrollWidth ?? 0
384+ const SCROLLBAR_BUFFER_PX = 20
385+ const hasHorizontalOverflow =
386+ contentWidth > containerWidth + SCROLLBAR_BUFFER_PX
387+
388+ if ( hasHorizontalOverflow && contentWidth > maxWidthRef . current ) {
389+ maxWidthRef . current = contentWidth
358390 }
359391
360392 return (
361393 < TableBody
362394 ref = { tbodyRef }
363- className = { showRefetchOverlay ? 'opacity-70 transition-opacity' : '' }
395+ className = { cn (
396+ showRefetchOverlay ? 'opacity-70 transition-opacity' : '' ,
397+ '[&_tr:last-child]:border-b-0 [&_tr]:border-b-0'
398+ ) }
364399 style = { {
365400 display : 'grid' ,
366401 height : `${ virtualizer . getTotalSize ( ) } px` ,
367- width : maxWidthRef . current ,
402+ width : hasHorizontalOverflow ? maxWidthRef . current : undefined ,
368403 minWidth : '100%' ,
369404 position : 'relative' ,
370405 } }
371406 >
372407 { virtualizer . getVirtualItems ( ) . map ( ( virtualRow ) => {
373- const isStatusRow = virtualRow . index === logs . length
408+ const isStatusRow = showStatusRow && virtualRow . index === 0
374409
375410 if ( isStatusRow ) {
376411 return (
377412 < StatusRow
378413 key = "status-row"
379414 virtualRow = { virtualRow }
380415 virtualizer = { virtualizer }
381- hasNextPage = { hasNextPage }
382416 isFetchingNextPage = { isFetchingNextPage }
383417 />
384418 )
385419 }
386420
421+ const logIndex = showStatusRow ? virtualRow . index - 1 : virtualRow . index
422+
387423 return (
388424 < LogRow
389425 key = { virtualRow . index }
390- log = { logs [ virtualRow . index ] ! }
426+ log = { logs [ logIndex ] ! }
391427 virtualRow = { virtualRow }
392428 virtualizer = { virtualizer }
393429 startedAt = { startedAt }
@@ -416,11 +452,8 @@ function useScrollLoadMore({
416452 if ( ! scrollContainer ) return
417453
418454 const handleScroll = ( ) => {
419- const { scrollTop, scrollHeight, clientHeight } = scrollContainer
420- const distanceFromBottom = scrollHeight - scrollTop - clientHeight
421-
422455 if (
423- distanceFromBottom < SCROLL_LOAD_THRESHOLD_PX &&
456+ scrollContainer . scrollTop < SCROLL_LOAD_THRESHOLD_PX &&
424457 hasNextPage &&
425458 ! isFetchingNextPage
426459 ) {
@@ -433,6 +466,102 @@ function useScrollLoadMore({
433466 } , [ scrollContainerRef , hasNextPage , isFetchingNextPage , onLoadMore ] )
434467}
435468
469+ interface UseMaintainScrollOnPrependParams {
470+ scrollContainerRef : RefObject < HTMLDivElement | null >
471+ logsCount : number
472+ isFetchingNextPage : boolean
473+ }
474+
475+ function useMaintainScrollOnPrepend ( {
476+ scrollContainerRef,
477+ logsCount,
478+ isFetchingNextPage,
479+ } : UseMaintainScrollOnPrependParams ) {
480+ const prevLogsCountRef = useRef ( logsCount )
481+ const wasFetchingRef = useRef ( false )
482+
483+ useEffect ( ( ) => {
484+ const el = scrollContainerRef . current
485+ if ( ! el ) return
486+
487+ const justFinishedFetching = wasFetchingRef . current && ! isFetchingNextPage
488+ const logsWerePrepended = logsCount > prevLogsCountRef . current
489+
490+ if ( justFinishedFetching && logsWerePrepended ) {
491+ const addedCount = logsCount - prevLogsCountRef . current
492+ el . scrollTop += addedCount * ROW_HEIGHT_PX
493+ }
494+
495+ wasFetchingRef . current = isFetchingNextPage
496+ prevLogsCountRef . current = logsCount
497+ } , [ scrollContainerRef , logsCount , isFetchingNextPage ] )
498+ }
499+
500+ interface UseAutoScrollToBottomParams {
501+ scrollContainerRef : RefObject < HTMLDivElement | null >
502+ logsCount : number
503+ isInitialized : boolean
504+ level : LogLevelFilter | null
505+ }
506+
507+ function useAutoScrollToBottom ( {
508+ scrollContainerRef,
509+ logsCount,
510+ isInitialized,
511+ level,
512+ } : UseAutoScrollToBottomParams ) {
513+ const isAutoScrollEnabledRef = useRef ( true )
514+ const prevLogsCountRef = useRef ( 0 )
515+ const prevLevelRef = useRef ( level )
516+ const hasInitialScrolled = useRef ( false )
517+
518+ useEffect ( ( ) => {
519+ const el = scrollContainerRef . current
520+ if ( ! el ) return
521+
522+ const handleScroll = ( ) => {
523+ const distanceFromBottom =
524+ el . scrollHeight - el . scrollTop - el . clientHeight
525+ isAutoScrollEnabledRef . current = distanceFromBottom < ROW_HEIGHT_PX * 2
526+ }
527+
528+ el . addEventListener ( 'scroll' , handleScroll )
529+ return ( ) => el . removeEventListener ( 'scroll' , handleScroll )
530+ } , [ scrollContainerRef ] )
531+
532+ useEffect ( ( ) => {
533+ if ( isInitialized && ! hasInitialScrolled . current && logsCount > 0 ) {
534+ hasInitialScrolled . current = true
535+ prevLogsCountRef . current = logsCount
536+ requestAnimationFrame ( ( ) => {
537+ const el = scrollContainerRef . current
538+ if ( el ) el . scrollTop = el . scrollHeight
539+ } )
540+ }
541+ } , [ isInitialized , logsCount , scrollContainerRef ] )
542+
543+ useEffect ( ( ) => {
544+ if ( prevLevelRef . current !== level ) {
545+ prevLevelRef . current = level
546+ hasInitialScrolled . current = false
547+ prevLogsCountRef . current = 0
548+ }
549+ } , [ level ] )
550+
551+ useEffect ( ( ) => {
552+ if ( ! hasInitialScrolled . current ) return
553+
554+ const newLogsCount = logsCount - prevLogsCountRef . current
555+
556+ if ( newLogsCount > 0 && isAutoScrollEnabledRef . current ) {
557+ const el = scrollContainerRef . current
558+ if ( el ) el . scrollTop += newLogsCount * ROW_HEIGHT_PX
559+ }
560+
561+ prevLogsCountRef . current = logsCount
562+ } , [ logsCount , scrollContainerRef ] )
563+ }
564+
436565interface LogRowProps {
437566 log : BuildLogDTO
438567 virtualRow : VirtualItem
@@ -492,20 +621,19 @@ function LogRow({ log, virtualRow, virtualizer, startedAt }: LogRowProps) {
492621interface StatusRowProps {
493622 virtualRow : VirtualItem
494623 virtualizer : Virtualizer < HTMLDivElement , Element >
495- hasNextPage : boolean
496624 isFetchingNextPage : boolean
497625}
498626
499627function StatusRow ( {
500628 virtualRow,
501629 virtualizer,
502- hasNextPage,
503630 isFetchingNextPage,
504631} : StatusRowProps ) {
505632 return (
506633 < TableRow
507634 data-index = { virtualRow . index }
508635 ref = { ( node ) => virtualizer . measureElement ( node ) }
636+ className = "animate-pulse"
509637 style = { {
510638 display : 'flex' ,
511639 position : 'absolute' ,
@@ -521,19 +649,17 @@ function StatusRow({
521649 style = { {
522650 display : 'flex' ,
523651 alignItems : 'center' ,
524- justifyContent : 'center ' ,
652+ justifyContent : 'start ' ,
525653 } }
526654 >
527- < span className = "text-fg-tertiary text-sm " >
655+ < span className = "prose-body-highlight text-fg-tertiary uppercase " >
528656 { isFetchingNextPage ? (
529- < span className = "inline-flex items-center gap-1" >
530- Loading
657+ < span className = "inline-flex gap-1" >
658+ Loading more logs
531659 < Loader variant = "dots" />
532660 </ span >
533- ) : hasNextPage ? (
534- 'Scroll to load more'
535661 ) : (
536- 'Nothing more to load'
662+ 'Scroll to load more '
537663 ) }
538664 </ span >
539665 </ TableCell >
0 commit comments