diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx index c7ae2bf61..7d917b6cd 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx @@ -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 | 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} >
@@ -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 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 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, diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index aa7311fa5..738332418 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -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(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} diff --git a/apps/sim/hooks/queries/logs.ts b/apps/sim/hooks/queries/logs.ts index 39f0cffe6..3b8e71480 100644 --- a/apps/sim/hooks/queries/logs.ts +++ b/apps/sim/hooks/queries/logs.ts @@ -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, }) }