fix(logs): instant log detail panel with initialData and hover prefetch

This commit is contained in:
waleed
2026-02-14 10:55:47 -08:00
parent 3ef6b05035
commit 72d613e92a
3 changed files with 85 additions and 7 deletions

View File

@@ -24,6 +24,7 @@ interface LogRowProps {
log: WorkflowLog
isSelected: boolean
onClick: (log: WorkflowLog) => void
onHover?: (log: WorkflowLog) => void
onContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
selectedRowRef: React.RefObject<HTMLTableRowElement | null> | null
}
@@ -33,7 +34,14 @@ interface LogRowProps {
* Uses shallow comparison for the log object.
*/
const LogRow = memo(
function LogRow({ log, isSelected, onClick, onContextMenu, selectedRowRef }: LogRowProps) {
function LogRow({
log,
isSelected,
onClick,
onHover,
onContextMenu,
selectedRowRef,
}: LogRowProps) {
const formattedDate = useMemo(() => formatDate(log.createdAt), [log.createdAt])
const isDeletedWorkflow = !log.workflow?.id && !log.workflowId
const workflowName = isDeletedWorkflow
@@ -43,6 +51,8 @@ const LogRow = memo(
const handleClick = useCallback(() => onClick(log), [onClick, log])
const handleMouseEnter = useCallback(() => onHover?.(log), [onHover, log])
const handleContextMenu = useCallback(
(e: React.MouseEvent) => {
if (onContextMenu) {
@@ -61,6 +71,7 @@ const LogRow = memo(
isSelected && 'bg-[var(--surface-3)] dark:bg-[var(--surface-4)]'
)}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onContextMenu={handleContextMenu}
>
<div className='flex flex-1 items-center'>
@@ -142,7 +153,8 @@ const LogRow = memo(
prevProps.log.id === nextProps.log.id &&
prevProps.log.duration === nextProps.log.duration &&
prevProps.log.status === nextProps.log.status &&
prevProps.isSelected === nextProps.isSelected
prevProps.isSelected === nextProps.isSelected &&
prevProps.onHover === nextProps.onHover
)
}
)
@@ -151,6 +163,7 @@ interface RowProps {
logs: WorkflowLog[]
selectedLogId: string | null
onLogClick: (log: WorkflowLog) => void
onLogHover?: (log: WorkflowLog) => void
onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
isFetchingNextPage: boolean
@@ -167,6 +180,7 @@ function Row({
logs,
selectedLogId,
onLogClick,
onLogHover,
onLogContextMenu,
selectedRowRef,
isFetchingNextPage,
@@ -198,6 +212,7 @@ function Row({
log={log}
isSelected={isSelected}
onClick={onLogClick}
onHover={onLogHover}
onContextMenu={onLogContextMenu}
selectedRowRef={isSelected ? selectedRowRef : null}
/>
@@ -209,6 +224,7 @@ export interface LogsListProps {
logs: WorkflowLog[]
selectedLogId: string | null
onLogClick: (log: WorkflowLog) => void
onLogHover?: (log: WorkflowLog) => void
onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
hasNextPage: boolean
@@ -227,6 +243,7 @@ export function LogsList({
logs,
selectedLogId,
onLogClick,
onLogHover,
onLogContextMenu,
selectedRowRef,
hasNextPage,
@@ -272,6 +289,7 @@ export function LogsList({
logs,
selectedLogId,
onLogClick,
onLogHover,
onLogContextMenu,
selectedRowRef,
isFetchingNextPage,
@@ -281,6 +299,7 @@ export function LogsList({
logs,
selectedLogId,
onLogClick,
onLogHover,
onLogContextMenu,
selectedRowRef,
isFetchingNextPage,

View File

@@ -1,6 +1,7 @@
'use client'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { Loader2 } from 'lucide-react'
import { useParams } from 'next/navigation'
import { cn } from '@/lib/core/utils/cn'
@@ -11,7 +12,12 @@ import {
} from '@/lib/logs/filters'
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
import { useFolders } from '@/hooks/queries/folders'
import { useDashboardStats, useLogDetail, useLogsList } from '@/hooks/queries/logs'
import {
prefetchLogDetail,
useDashboardStats,
useLogDetail,
useLogsList,
} from '@/hooks/queries/logs'
import { useDebounce } from '@/hooks/use-debounce'
import { useFilterStore } from '@/stores/logs/filters/store'
import type { WorkflowLog } from '@/stores/logs/filters/types'
@@ -94,8 +100,19 @@ export default function Logs() {
const [previewLogId, setPreviewLogId] = useState<string | null>(null)
const activeLogId = isPreviewOpen ? previewLogId : selectedLogId
const queryClient = useQueryClient()
const detailRefetchInterval = useCallback(
(query: { state: { data?: WorkflowLog } }) => {
if (!isLive) return false
const status = query.state.data?.status
return status === 'running' || status === 'pending' ? 3000 : false
},
[isLive]
)
const activeLogQuery = useLogDetail(activeLogId ?? undefined, {
refetchInterval: isLive ? 3000 : false,
refetchInterval: detailRefetchInterval,
})
const logFilters = useMemo(
@@ -154,6 +171,13 @@ export default function Logs() {
return { ...selectedLogFromList, ...activeLogQuery.data }
}, [selectedLogFromList, activeLogQuery.data, isPreviewOpen])
const handleLogHover = useCallback(
(log: WorkflowLog) => {
prefetchLogDetail(queryClient, log.id)
},
[queryClient]
)
useFolders(workspaceId)
useEffect(() => {
@@ -476,6 +500,7 @@ export default function Logs() {
logs={logs}
selectedLogId={selectedLogId}
onLogClick={handleLogClick}
onLogHover={handleLogHover}
onLogContextMenu={handleLogContextMenu}
selectedRowRef={selectedRowRef}
hasNextPage={logsQuery.hasNextPage ?? false}

View File

@@ -1,4 +1,10 @@
import { keepPreviousData, useInfiniteQuery, useQuery } from '@tanstack/react-query'
import {
keepPreviousData,
type QueryClient,
useInfiniteQuery,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { getEndDateFromTimeRange, getStartDateFromTimeRange } from '@/lib/logs/filters'
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
import type {
@@ -146,17 +152,45 @@ export function useLogsList(
interface UseLogDetailOptions {
enabled?: boolean
refetchInterval?: number | false
refetchInterval?:
| number
| false
| ((query: { state: { data?: WorkflowLog } }) => number | false | undefined)
}
export function useLogDetail(logId: string | undefined, options?: UseLogDetailOptions) {
const queryClient = useQueryClient()
return useQuery({
queryKey: logKeys.detail(logId),
queryFn: () => fetchLogDetail(logId as string),
enabled: Boolean(logId) && (options?.enabled ?? true),
refetchInterval: options?.refetchInterval ?? false,
staleTime: 30 * 1000,
placeholderData: keepPreviousData,
initialData: () => {
if (!logId) return undefined
const listQueries = queryClient.getQueriesData<{
pages: { logs: WorkflowLog[] }[]
}>({
queryKey: logKeys.lists(),
})
for (const [, data] of listQueries) {
const match = data?.pages?.flatMap((p) => p.logs).find((l) => l.id === logId)
if (match) return match
}
return undefined
},
initialDataUpdatedAt: 0,
})
}
/**
* Prefetches log detail data on hover for instant panel rendering on click.
*/
export function prefetchLogDetail(queryClient: QueryClient, logId: string) {
queryClient.prefetchQuery({
queryKey: logKeys.detail(logId),
queryFn: () => fetchLogDetail(logId),
staleTime: 30 * 1000,
})
}