Skip to content

Commit 93598ff

Browse files
committed
fix: adjust log order and improve scrolling behavior in logs component
1 parent 64f4d2f commit 93598ff

File tree

4 files changed

+174
-51
lines changed

4 files changed

+174
-51
lines changed

src/features/dashboard/build/build-logs-store.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ export const createBuildLogsStore = () =>
188188

189189
set((s) => {
190190
const uniqueNewLogs = deduplicateLogs(s.logs, result.logs)
191-
s.logs = [...s.logs, ...uniqueNewLogs]
191+
s.logs = [...uniqueNewLogs, ...s.logs]
192192
s.hasMoreBackwards = result.nextCursor !== null
193193
s.backwardsCursor = result.nextCursor
194194
s.isLoadingBackwards = false
@@ -243,7 +243,7 @@ export const createBuildLogsStore = () =>
243243
set((s) => {
244244
const uniqueNewLogs = deduplicateLogs(s.logs, result.logs)
245245
if (uniqueNewLogs.length > 0) {
246-
s.logs = [...uniqueNewLogs, ...s.logs]
246+
s.logs = [...s.logs, ...uniqueNewLogs]
247247
}
248248
s.isLoadingForwards = false
249249
})
@@ -264,12 +264,12 @@ export const createBuildLogsStore = () =>
264264

265265
getNewestTimestamp: () => {
266266
const state = get()
267-
return state.logs[0]?.timestampUnix
267+
return state.logs[state.logs.length - 1]?.timestampUnix
268268
},
269269

270270
getOldestTimestamp: () => {
271271
const state = get()
272-
return state.logs[state.logs.length - 1]?.timestampUnix
272+
return state.logs[0]?.timestampUnix
273273
},
274274
}))
275275
)

src/features/dashboard/build/logs.tsx

Lines changed: 157 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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

324334
function 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+
436565
interface LogRowProps {
437566
log: BuildLogDTO
438567
virtualRow: VirtualItem
@@ -492,20 +621,19 @@ function LogRow({ log, virtualRow, virtualizer, startedAt }: LogRowProps) {
492621
interface StatusRowProps {
493622
virtualRow: VirtualItem
494623
virtualizer: Virtualizer<HTMLDivElement, Element>
495-
hasNextPage: boolean
496624
isFetchingNextPage: boolean
497625
}
498626

499627
function 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>

src/features/dashboard/build/use-build-logs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export function useBuildLogs({
8080

8181
return {
8282
logs,
83+
isInitialized,
8384
hasNextPage: hasMoreBackwards,
8485
isFetchingNextPage: isLoadingBackwards,
8586
isFetching: isLoadingBackwards || isLoadingForwards || isPolling,

src/server/api/routers/builds.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,14 @@ export const buildsRouter = createTRPCRouter({
106106

107107
const logsToReturn = buildLogs.logs
108108

109-
const logs: BuildLogDTO[] = logsToReturn
110-
.map((log) => ({
111-
timestampUnix: new Date(log.timestamp).getTime(),
112-
level: log.level,
113-
message: log.message,
114-
}))
115-
.reverse()
109+
const logs: BuildLogDTO[] = logsToReturn.map((log) => ({
110+
timestampUnix: new Date(log.timestamp).getTime(),
111+
level: log.level,
112+
message: log.message,
113+
}))
116114

117115
const hasMore = logs.length === limit
118-
const cursorLog = logs[logs.length - 1]
116+
const cursorLog = logs[0]
119117
const nextCursor = hasMore ? (cursorLog?.timestampUnix ?? null) : null
120118

121119
const result: BuildLogsDTO = {
@@ -153,15 +151,13 @@ export const buildsRouter = createTRPCRouter({
153151
{ cursor, limit, direction, level }
154152
)
155153

156-
const logs: BuildLogDTO[] = buildLogs.logs
157-
.map((log) => ({
158-
timestampUnix: new Date(log.timestamp).getTime(),
159-
level: log.level,
160-
message: log.message,
161-
}))
162-
.reverse()
154+
const logs: BuildLogDTO[] = buildLogs.logs.map((log) => ({
155+
timestampUnix: new Date(log.timestamp).getTime(),
156+
level: log.level,
157+
message: log.message,
158+
}))
163159

164-
const newestLog = logs[0]
160+
const newestLog = logs[logs.length - 1]
165161
const nextCursor = newestLog?.timestampUnix ?? null
166162

167163
const result: BuildLogsDTO = {

0 commit comments

Comments
 (0)