mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-15 00:44:56 -05:00
fix(logs): instant log detail panel with initialData and hover prefetch
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user