Skip to content

Commit 1fa912f

Browse files
authored
improvement(logs): refresh the table on demand (#8235)
* improvement(logs): refresh the table on demand * fix(logs): detect snapshot changes before paging
1 parent d5ef007 commit 1fa912f

8 files changed

Lines changed: 992 additions & 183 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx‎

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {
3939
import { ArrowUpLeft } from '@sim/emcn/icons'
4040
import { createPortal } from 'react-dom'
4141
import { HEADER_ACTION_CLUSTER, TITLE_BAR_LANE_PT } from '@/components/page-header-bar'
42-
import { orderHeaderActions } from '@/components/settings/settings-header'
42+
import { orderHeaderActions, SettingsActionChip } from '@/components/settings/settings-header'
4343
import { InlineRenameInput } from '@/app/workspace/[workspaceId]/components/inline-rename-input'
4444

4545
export interface DropdownOption {
@@ -104,6 +104,7 @@ export interface ResourceAction {
104104
active?: boolean
105105
onSelect: () => void
106106
disabled?: boolean
107+
tooltip?: string
107108
}
108109

109110
/**
@@ -259,16 +260,7 @@ export const ResourceHeader = memo(function ResourceHeader({
259260
<div className={cn(HEADER_ACTION_CLUSTER, 'shrink-0')}>
260261
{aside}
261262
{orderHeaderActions(actions).map(({ action }) => (
262-
<Chip
263-
key={action.id ?? action.text}
264-
variant={action.variant}
265-
active={action.active}
266-
leftIcon={action.icon}
267-
onClick={action.onSelect}
268-
disabled={action.disabled}
269-
>
270-
{action.text}
271-
</Chip>
263+
<SettingsActionChip key={action.id ?? action.text} action={action} />
272264
))}
273265
</div>
274266
)}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { cn } from '@sim/emcn'
2+
import { RefreshCw } from '@sim/emcn/icons'
3+
import type { ResourceAction } from '@/app/workspace/[workspaceId]/components/resource/components/resource-header'
4+
5+
interface RefreshIconProps {
6+
className?: string
7+
}
8+
9+
function SpinningRefreshIcon(props: RefreshIconProps) {
10+
return <RefreshCw {...props} animate />
11+
}
12+
13+
function NewLogsIndicator({ className }: RefreshIconProps) {
14+
return (
15+
<span className={cn(className, 'inline-flex items-center justify-center')} aria-hidden='true'>
16+
<span className='size-1.5 rounded-full bg-[var(--brand-blue)]' />
17+
</span>
18+
)
19+
}
20+
21+
interface LogsRefreshActionOptions {
22+
newLogCount: number
23+
hasUpdates?: boolean
24+
isRefreshing: boolean
25+
onRefresh: () => void
26+
}
27+
28+
export function getLogsRefreshAction({
29+
newLogCount,
30+
hasUpdates = false,
31+
isRefreshing,
32+
onRefresh,
33+
}: LogsRefreshActionOptions): ResourceAction {
34+
const hasNewLogs = newLogCount > 0
35+
return {
36+
id: 'refresh',
37+
text: hasNewLogs
38+
? `${newLogCount} new ${newLogCount === 1 ? 'log' : 'logs'}`
39+
: hasUpdates
40+
? 'Updates available'
41+
: 'Refresh',
42+
tooltip: hasNewLogs
43+
? 'Refresh to see new logs'
44+
: hasUpdates
45+
? 'Refresh to see updated logs'
46+
: 'Refresh',
47+
icon: isRefreshing
48+
? SpinningRefreshIcon
49+
: hasNewLogs || hasUpdates
50+
? NewLogsIndicator
51+
: RefreshCw,
52+
onSelect: onRefresh,
53+
disabled: isRefreshing,
54+
}
55+
}

‎apps/sim/app/workspace/[workspaceId]/logs/logs.tsx‎

Lines changed: 49 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
Popover,
2323
PopoverAnchor,
2424
PopoverContent,
25-
RefreshCw,
2625
toast,
2726
} from '@sim/emcn'
2827
import { 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'
7676
import { useLogFilters } from '@/app/workspace/[workspaceId]/logs/hooks/use-log-filters'
7777
import { useSearchState } from '@/app/workspace/[workspaceId]/logs/hooks/use-search-state'
7878
import {
@@ -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'
100102
import { useWorkflowMap, useWorkflows } from '@/hooks/queries/workflows'
@@ -130,7 +132,6 @@ const ExecutionSnapshot = lazy(() =>
130132
)
131133

132134
const LOGS_PER_PAGE = 50 as const
133-
const REFRESH_SPINNER_DURATION_MS = 1000 as const
134135
const LIVE_REFRESH_INTERVAL_MS = 10_000 as const
135136
const 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

Comments
 (0)