@@ -22,7 +22,6 @@ import {
2222 Popover ,
2323 PopoverAnchor ,
2424 PopoverContent ,
25- RefreshCw ,
2625 toast ,
2726} from '@sim/emcn'
2827import { Download , Workflow } from '@sim/emcn/icons'
@@ -73,6 +72,7 @@ import {
7372 SnapshotBoundary ,
7473 SnapshotModalFallback ,
7574} from '@/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/snapshot-boundary'
75+ import { getLogsRefreshAction } from '@/app/workspace/[workspaceId]/logs/components/log-refresh-action'
7676import { useLogFilters } from '@/app/workspace/[workspaceId]/logs/hooks/use-log-filters'
7777import { useSearchState } from '@/app/workspace/[workspaceId]/logs/hooks/use-search-state'
7878import {
@@ -94,7 +94,9 @@ import {
9494 useDashboardStats ,
9595 useLogByExecutionId ,
9696 useLogDetail ,
97- useLogsList ,
97+ useLogSnapshotUpdates ,
98+ useLogsSnapshot ,
99+ useNewLogCount ,
98100 useRetryExecution ,
99101} from '@/hooks/queries/logs'
100102import { useWorkflowMap , useWorkflows } from '@/hooks/queries/workflows'
@@ -130,7 +132,6 @@ const ExecutionSnapshot = lazy(() =>
130132)
131133
132134const LOGS_PER_PAGE = 50 as const
133- const REFRESH_SPINNER_DURATION_MS = 1000 as const
134135const LIVE_REFRESH_INTERVAL_MS = 10_000 as const
135136const ACTIVE_RUN_DETAIL_REFRESH_MS = 3_000 as const
136137
@@ -228,8 +229,11 @@ function getTriggerIcon(
228229 return TriggerIcon
229230}
230231
231- function SpinningRefreshCw ( props : React . SVGProps < SVGSVGElement > ) {
232- return < RefreshCw { ...props } animate />
232+ function activeRunRefetchInterval ( query : { state : { data ?: WorkflowLogDetail } } ) {
233+ const status = query . state . data ?. status
234+ return status === 'running' || status === 'pending' || status === 'redacting'
235+ ? ACTIVE_RUN_DETAIL_REFRESH_MS
236+ : false
233237}
234238
235239/**
@@ -291,17 +295,14 @@ export default function Logs() {
291295 */
292296 const debouncedSearchQuery = useDebounce ( urlSearchQuery , SEARCH_DEBOUNCE_MS ) . trim ( )
293297
294- const isLive = true
295- const [ isVisuallyRefreshing , setIsVisuallyRefreshing ] = useState ( false )
296298 const [ isExporting , setIsExporting ] = useState ( false )
297- const refreshTimersRef = useRef ( new Set < number > ( ) )
298299 const logsRef = useRef < WorkflowLogSummary [ ] > ( [ ] )
299300 const selectedLogIndexRef = useRef ( - 1 )
300301 const selectedLogIdRef = useRef < string | null > ( null )
301302 const isSidebarOpenRef = useRef ( false )
302303 const shouldScrollIntoViewRef = useRef ( false )
303304 const resourceTableRef = useRef < ResourceTableHandle > ( null )
304- const activeViewRefetchRef = useRef < ( ) => void > ( ( ) => { } )
305+ const activeViewRefetchRef = useRef < ( ) => Promise < unknown > > ( async ( ) => { } )
305306 const activeLogRefetchRef = useRef < ( ) => void > ( ( ) => { } )
306307 const activeLogTabRef = useRef < string > ( 'overview' )
307308 const logsQueryRef = useRef ( { isFetching : false , hasNextPage : false , fetchNextPage : ( ) => { } } )
@@ -328,24 +329,13 @@ export default function Logs() {
328329
329330 const queryClient = useQueryClient ( )
330331
331- const refetchInterval = useCallback (
332- ( query : { state : { data ?: WorkflowLogDetail } } ) => {
333- if ( ! isLive ) return false
334- const status = query . state . data ?. status
335- return status === 'running' || status === 'pending' || status === 'redacting'
336- ? ACTIVE_RUN_DETAIL_REFRESH_MS
337- : false
338- } ,
339- [ isLive ]
340- )
341-
342332 const selectedDetailQuery = useLogDetail ( selectedLogId ?? undefined , workspaceId , {
343333 enabled : isSidebarOpen ,
344- refetchInterval,
334+ refetchInterval : activeRunRefetchInterval ,
345335 } )
346336
347337 const previewDetailQuery = useLogDetail ( previewLogId ?? undefined , workspaceId , {
348- refetchInterval,
338+ refetchInterval : activeRunRefetchInterval ,
349339 } )
350340
351341 const logFilters = useMemo (
@@ -376,10 +366,23 @@ export default function Logs() {
376366 ]
377367 )
378368
379- const logsQuery = useLogsList ( workspaceId , logFilters , {
369+ const logsQuery = useLogsSnapshot ( workspaceId , logFilters , {
380370 enabled : ! isDashboardView || isSidebarOpen ,
381- refetchInterval : isLive ? LIVE_REFRESH_INTERVAL_MS : false ,
382371 } )
372+ const newLogsQuery = useNewLogCount (
373+ workspaceId ,
374+ logFilters ,
375+ logsQuery . isPlaceholderData ? undefined : logsQuery . data ?. pages [ 0 ] ?. snapshotAt ,
376+ { enabled : ! isDashboardView }
377+ )
378+ const newLogCount = newLogsQuery . data ?? 0
379+ const hasChangedPage = logsQuery . data ?. pages . some ( ( page ) => page . snapshotChanged ) === true
380+ const snapshotUpdatesQuery = useLogSnapshotUpdates (
381+ workspaceId ,
382+ logsQuery . isPlaceholderData ? undefined : logsQuery . data ?. pages [ 0 ] ,
383+ { enabled : ! isDashboardView && newLogCount === 0 && ! hasChangedPage }
384+ )
385+ const hasSnapshotUpdates = snapshotUpdatesQuery . data || hasChangedPage
383386
384387 const dashboardFilters = useMemo (
385388 ( ) => ( {
@@ -397,7 +400,7 @@ export default function Logs() {
397400
398401 const dashboardStatsQuery = useDashboardStats ( workspaceId , dashboardFilters , {
399402 enabled : isDashboardView ,
400- refetchInterval : isLive ? LIVE_REFRESH_INTERVAL_MS : false ,
403+ refetchInterval : LIVE_REFRESH_INTERVAL_MS ,
401404 } )
402405
403406 const logs = useMemo ( ( ) => {
@@ -421,14 +424,11 @@ export default function Logs() {
421424 selectedLogIndexRef . current = selectedLogIndex
422425 selectedLogIdRef . current = selectedLogId
423426 isSidebarOpenRef . current = isSidebarOpen
424- activeViewRefetchRef . current = ( ) => {
425- if ( isDashboardView ) {
426- void dashboardStatsQuery . refetch ( )
427- }
428- if ( ! isDashboardView || isSidebarOpen ) {
429- void logsQuery . refetch ( )
430- }
431- }
427+ activeViewRefetchRef . current = ( ) =>
428+ Promise . all ( [
429+ ...( isDashboardView ? [ dashboardStatsQuery . refetch ( { throwOnError : true } ) ] : [ ] ) ,
430+ ...( ! isDashboardView || isSidebarOpen ? [ logsQuery . refetch ( { throwOnError : true } ) ] : [ ] ) ,
431+ ] )
432432 activeLogRefetchRef . current = selectedDetailQuery . refetch
433433 logsQueryRef . current = {
434434 isFetching : logsQuery . isFetching ,
@@ -449,14 +449,6 @@ export default function Logs() {
449449 }
450450 } , [ pendingExecutionId , deepLinkQuery . data , deepLinkQuery . isError ] )
451451
452- useEffect ( ( ) => {
453- const timers = refreshTimersRef . current
454- return ( ) => {
455- timers . forEach ( ( id ) => window . clearTimeout ( id ) )
456- timers . clear ( )
457- }
458- } , [ ] )
459-
460452 /**
461453 * The single write path for user-driven `executionId` changes. Cancels any
462454 * in-flight deep-link resolution first — an explicit interaction supersedes
@@ -664,36 +656,20 @@ export default function Logs() {
664656 const effectiveSidebarOpen =
665657 isSidebarOpen && ( selectedLogIndex !== - 1 || ! ! selectedDetailQuery . data )
666658
667- const triggerVisualRefresh = useCallback ( ( ) => {
668- setIsVisuallyRefreshing ( true )
669- const timerId = window . setTimeout ( ( ) => {
670- setIsVisuallyRefreshing ( false )
671- refreshTimersRef . current . delete ( timerId )
672- } , REFRESH_SPINNER_DURATION_MS )
673- refreshTimersRef . current . add ( timerId )
674- } , [ ] )
675-
676- const handleRefresh = useCallback ( ( ) => {
677- triggerVisualRefresh ( )
678- activeViewRefetchRef . current ( )
659+ const handleRefresh = useCallback ( async ( ) => {
679660 if ( selectedLogIdRef . current && isSidebarOpenRef . current ) {
680661 activeLogRefetchRef . current ( )
681662 }
682- } , [ triggerVisualRefresh ] )
663+ try {
664+ await activeViewRefetchRef . current ( )
665+ } catch ( error ) {
666+ toast . error ( getErrorMessage ( error , 'Failed to refresh logs' ) )
667+ }
668+ } , [ ] )
683669
684- const activeViewIsFetching = isDashboardView
670+ const isVisuallyRefreshing = isDashboardView
685671 ? dashboardStatsQuery . isFetching || ( isSidebarOpen && logsQuery . isFetching )
686672 : logsQuery . isFetching
687- const prevIsFetchingRef = useRef ( activeViewIsFetching )
688- useEffect ( ( ) => {
689- const wasFetching = prevIsFetchingRef . current
690- const isFetching = activeViewIsFetching
691- prevIsFetchingRef . current = isFetching
692-
693- if ( isLive && ! wasFetching && isFetching ) {
694- triggerVisualRefresh ( )
695- }
696- } , [ activeViewIsFetching , isLive , triggerVisualRefresh ] )
697673
698674 const handleExport = useCallback ( async ( ) => {
699675 setIsExporting ( true )
@@ -1169,7 +1145,6 @@ export default function Logs() {
11691145 ]
11701146 )
11711147
1172- const refreshIcon = isVisuallyRefreshing ? SpinningRefreshCw : RefreshCw
11731148 const hasExportableLogs = isDashboardView
11741149 ? ! dashboardStatsQuery . isPlaceholderData && ( dashboardStatsQuery . data ?. totalRuns ?? 0 ) > 0
11751150 : ! logsQuery . isPlaceholderData && logs . length > 0
@@ -1182,12 +1157,12 @@ export default function Logs() {
11821157 onSelect : handleExport ,
11831158 disabled : ! userPermissions . canEdit || isExporting || ! hasExportableLogs ,
11841159 } ,
1185- {
1186- text : 'Refresh' ,
1187- icon : refreshIcon ,
1188- onSelect : handleRefresh ,
1189- disabled : isVisuallyRefreshing ,
1190- } ,
1160+ getLogsRefreshAction ( {
1161+ newLogCount : isDashboardView ? 0 : newLogCount ,
1162+ hasUpdates : ! isDashboardView && hasSnapshotUpdates ,
1163+ isRefreshing : isVisuallyRefreshing ,
1164+ onRefresh : handleRefresh ,
1165+ } ) ,
11911166 {
11921167 text : 'Logs' ,
11931168 onSelect : ( ) => setViewMode ( 'logs' ) ,
@@ -1202,7 +1177,8 @@ export default function Logs() {
12021177 [
12031178 isDashboardView ,
12041179 setViewMode ,
1205- refreshIcon ,
1180+ newLogCount ,
1181+ hasSnapshotUpdates ,
12061182 isVisuallyRefreshing ,
12071183 handleRefresh ,
12081184 handleExport ,
0 commit comments