Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
import { ArrowUpLeft } from '@sim/emcn/icons'
import { createPortal } from 'react-dom'
import { HEADER_ACTION_CLUSTER, TITLE_BAR_LANE_PT } from '@/components/page-header-bar'
import { orderHeaderActions } from '@/components/settings/settings-header'
import { orderHeaderActions, SettingsActionChip } from '@/components/settings/settings-header'
import { InlineRenameInput } from '@/app/workspace/[workspaceId]/components/inline-rename-input'

export interface DropdownOption {
Expand Down Expand Up @@ -104,6 +104,7 @@ export interface ResourceAction {
active?: boolean
onSelect: () => void
disabled?: boolean
tooltip?: string
}

/**
Expand Down Expand Up @@ -259,16 +260,7 @@ export const ResourceHeader = memo(function ResourceHeader({
<div className={cn(HEADER_ACTION_CLUSTER, 'shrink-0')}>
{aside}
{orderHeaderActions(actions).map(({ action }) => (
<Chip
key={action.id ?? action.text}
variant={action.variant}
active={action.active}
leftIcon={action.icon}
onClick={action.onSelect}
disabled={action.disabled}
>
{action.text}
</Chip>
<SettingsActionChip key={action.id ?? action.text} action={action} />
))}
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { cn } from '@sim/emcn'
import { RefreshCw } from '@sim/emcn/icons'
import type { ResourceAction } from '@/app/workspace/[workspaceId]/components/resource/components/resource-header'

interface RefreshIconProps {
className?: string
}

function SpinningRefreshIcon(props: RefreshIconProps) {
return <RefreshCw {...props} animate />
}

function NewLogsIndicator({ className }: RefreshIconProps) {
return (
<span className={cn(className, 'inline-flex items-center justify-center')} aria-hidden='true'>
<span className='size-1.5 rounded-full bg-[var(--brand-blue)]' />
</span>
)
}

interface LogsRefreshActionOptions {
newLogCount: number
hasUpdates?: boolean
isRefreshing: boolean
onRefresh: () => void
}

export function getLogsRefreshAction({
newLogCount,
hasUpdates = false,
isRefreshing,
onRefresh,
}: LogsRefreshActionOptions): ResourceAction {
const hasNewLogs = newLogCount > 0
return {
id: 'refresh',
text: hasNewLogs
? `${newLogCount} new ${newLogCount === 1 ? 'log' : 'logs'}`
: hasUpdates
? 'Updates available'
: 'Refresh',
tooltip: hasNewLogs
? 'Refresh to see new logs'
: hasUpdates
? 'Refresh to see updated logs'
: 'Refresh',
icon: isRefreshing
? SpinningRefreshIcon
: hasNewLogs || hasUpdates
? NewLogsIndicator
: RefreshCw,
onSelect: onRefresh,
disabled: isRefreshing,
}
}
122 changes: 49 additions & 73 deletions apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
Popover,
PopoverAnchor,
PopoverContent,
RefreshCw,
toast,
} from '@sim/emcn'
import { Download, Workflow } from '@sim/emcn/icons'
Expand Down Expand Up @@ -73,6 +72,7 @@ import {
SnapshotBoundary,
SnapshotModalFallback,
} from '@/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/snapshot-boundary'
import { getLogsRefreshAction } from '@/app/workspace/[workspaceId]/logs/components/log-refresh-action'
import { useLogFilters } from '@/app/workspace/[workspaceId]/logs/hooks/use-log-filters'
import { useSearchState } from '@/app/workspace/[workspaceId]/logs/hooks/use-search-state'
import {
Expand All @@ -94,7 +94,9 @@ import {
useDashboardStats,
useLogByExecutionId,
useLogDetail,
useLogsList,
useLogSnapshotUpdates,
useLogsSnapshot,
useNewLogCount,
useRetryExecution,
} from '@/hooks/queries/logs'
import { useWorkflowMap, useWorkflows } from '@/hooks/queries/workflows'
Expand Down Expand Up @@ -130,7 +132,6 @@ const ExecutionSnapshot = lazy(() =>
)

const LOGS_PER_PAGE = 50 as const
const REFRESH_SPINNER_DURATION_MS = 1000 as const
const LIVE_REFRESH_INTERVAL_MS = 10_000 as const
const ACTIVE_RUN_DETAIL_REFRESH_MS = 3_000 as const

Expand Down Expand Up @@ -228,8 +229,11 @@ function getTriggerIcon(
return TriggerIcon
}

function SpinningRefreshCw(props: React.SVGProps<SVGSVGElement>) {
return <RefreshCw {...props} animate />
function activeRunRefetchInterval(query: { state: { data?: WorkflowLogDetail } }) {
const status = query.state.data?.status
return status === 'running' || status === 'pending' || status === 'redacting'
? ACTIVE_RUN_DETAIL_REFRESH_MS
: false
}

/**
Expand Down Expand Up @@ -291,17 +295,14 @@ export default function Logs() {
*/
const debouncedSearchQuery = useDebounce(urlSearchQuery, SEARCH_DEBOUNCE_MS).trim()

const isLive = true
const [isVisuallyRefreshing, setIsVisuallyRefreshing] = useState(false)
const [isExporting, setIsExporting] = useState(false)
const refreshTimersRef = useRef(new Set<number>())
const logsRef = useRef<WorkflowLogSummary[]>([])
const selectedLogIndexRef = useRef(-1)
const selectedLogIdRef = useRef<string | null>(null)
const isSidebarOpenRef = useRef(false)
const shouldScrollIntoViewRef = useRef(false)
const resourceTableRef = useRef<ResourceTableHandle>(null)
const activeViewRefetchRef = useRef<() => void>(() => {})
const activeViewRefetchRef = useRef<() => Promise<unknown>>(async () => {})
const activeLogRefetchRef = useRef<() => void>(() => {})
const activeLogTabRef = useRef<string>('overview')
const logsQueryRef = useRef({ isFetching: false, hasNextPage: false, fetchNextPage: () => {} })
Expand All @@ -328,24 +329,13 @@ export default function Logs() {

const queryClient = useQueryClient()

const refetchInterval = useCallback(
(query: { state: { data?: WorkflowLogDetail } }) => {
if (!isLive) return false
const status = query.state.data?.status
return status === 'running' || status === 'pending' || status === 'redacting'
? ACTIVE_RUN_DETAIL_REFRESH_MS
: false
},
[isLive]
)

const selectedDetailQuery = useLogDetail(selectedLogId ?? undefined, workspaceId, {
enabled: isSidebarOpen,
refetchInterval,
refetchInterval: activeRunRefetchInterval,
})

const previewDetailQuery = useLogDetail(previewLogId ?? undefined, workspaceId, {
refetchInterval,
refetchInterval: activeRunRefetchInterval,
})

const logFilters = useMemo(
Expand Down Expand Up @@ -376,10 +366,23 @@ export default function Logs() {
]
)

const logsQuery = useLogsList(workspaceId, logFilters, {
const logsQuery = useLogsSnapshot(workspaceId, logFilters, {
enabled: !isDashboardView || isSidebarOpen,
refetchInterval: isLive ? LIVE_REFRESH_INTERVAL_MS : false,
})
const newLogsQuery = useNewLogCount(
workspaceId,
logFilters,
logsQuery.isPlaceholderData ? undefined : logsQuery.data?.pages[0]?.snapshotAt,
{ enabled: !isDashboardView }
)
const newLogCount = newLogsQuery.data ?? 0
const hasChangedPage = logsQuery.data?.pages.some((page) => page.snapshotChanged) === true
const snapshotUpdatesQuery = useLogSnapshotUpdates(
workspaceId,
logsQuery.isPlaceholderData ? undefined : logsQuery.data?.pages[0],
{ enabled: !isDashboardView && newLogCount === 0 && !hasChangedPage }
)
const hasSnapshotUpdates = snapshotUpdatesQuery.data || hasChangedPage

const dashboardFilters = useMemo(
() => ({
Expand All @@ -397,7 +400,7 @@ export default function Logs() {

const dashboardStatsQuery = useDashboardStats(workspaceId, dashboardFilters, {
enabled: isDashboardView,
refetchInterval: isLive ? LIVE_REFRESH_INTERVAL_MS : false,
refetchInterval: LIVE_REFRESH_INTERVAL_MS,
})

const logs = useMemo(() => {
Expand All @@ -421,14 +424,11 @@ export default function Logs() {
selectedLogIndexRef.current = selectedLogIndex
selectedLogIdRef.current = selectedLogId
isSidebarOpenRef.current = isSidebarOpen
activeViewRefetchRef.current = () => {
if (isDashboardView) {
void dashboardStatsQuery.refetch()
}
if (!isDashboardView || isSidebarOpen) {
void logsQuery.refetch()
}
}
activeViewRefetchRef.current = () =>
Promise.all([
...(isDashboardView ? [dashboardStatsQuery.refetch({ throwOnError: true })] : []),
...(!isDashboardView || isSidebarOpen ? [logsQuery.refetch({ throwOnError: true })] : []),
])
activeLogRefetchRef.current = selectedDetailQuery.refetch
logsQueryRef.current = {
isFetching: logsQuery.isFetching,
Expand All @@ -449,14 +449,6 @@ export default function Logs() {
}
}, [pendingExecutionId, deepLinkQuery.data, deepLinkQuery.isError])

useEffect(() => {
const timers = refreshTimersRef.current
return () => {
timers.forEach((id) => window.clearTimeout(id))
timers.clear()
}
}, [])

/**
* The single write path for user-driven `executionId` changes. Cancels any
* in-flight deep-link resolution first — an explicit interaction supersedes
Expand Down Expand Up @@ -664,36 +656,20 @@ export default function Logs() {
const effectiveSidebarOpen =
isSidebarOpen && (selectedLogIndex !== -1 || !!selectedDetailQuery.data)

const triggerVisualRefresh = useCallback(() => {
setIsVisuallyRefreshing(true)
const timerId = window.setTimeout(() => {
setIsVisuallyRefreshing(false)
refreshTimersRef.current.delete(timerId)
}, REFRESH_SPINNER_DURATION_MS)
refreshTimersRef.current.add(timerId)
}, [])

const handleRefresh = useCallback(() => {
triggerVisualRefresh()
activeViewRefetchRef.current()
const handleRefresh = useCallback(async () => {
if (selectedLogIdRef.current && isSidebarOpenRef.current) {
activeLogRefetchRef.current()
}
}, [triggerVisualRefresh])
try {
await activeViewRefetchRef.current()
} catch (error) {
toast.error(getErrorMessage(error, 'Failed to refresh logs'))
}
}, [])

const activeViewIsFetching = isDashboardView
const isVisuallyRefreshing = isDashboardView
? dashboardStatsQuery.isFetching || (isSidebarOpen && logsQuery.isFetching)
: logsQuery.isFetching
Comment thread
waleedlatif1 marked this conversation as resolved.
const prevIsFetchingRef = useRef(activeViewIsFetching)
useEffect(() => {
const wasFetching = prevIsFetchingRef.current
const isFetching = activeViewIsFetching
prevIsFetchingRef.current = isFetching

if (isLive && !wasFetching && isFetching) {
triggerVisualRefresh()
}
}, [activeViewIsFetching, isLive, triggerVisualRefresh])

const handleExport = useCallback(async () => {
setIsExporting(true)
Expand Down Expand Up @@ -1169,7 +1145,6 @@ export default function Logs() {
]
)

const refreshIcon = isVisuallyRefreshing ? SpinningRefreshCw : RefreshCw
const hasExportableLogs = isDashboardView
? !dashboardStatsQuery.isPlaceholderData && (dashboardStatsQuery.data?.totalRuns ?? 0) > 0
: !logsQuery.isPlaceholderData && logs.length > 0
Expand All @@ -1182,12 +1157,12 @@ export default function Logs() {
onSelect: handleExport,
disabled: !userPermissions.canEdit || isExporting || !hasExportableLogs,
},
{
text: 'Refresh',
icon: refreshIcon,
onSelect: handleRefresh,
disabled: isVisuallyRefreshing,
},
getLogsRefreshAction({
newLogCount: isDashboardView ? 0 : newLogCount,
hasUpdates: !isDashboardView && hasSnapshotUpdates,
isRefreshing: isVisuallyRefreshing,
onRefresh: handleRefresh,
}),
{
text: 'Logs',
onSelect: () => setViewMode('logs'),
Expand All @@ -1202,7 +1177,8 @@ export default function Logs() {
[
isDashboardView,
setViewMode,
refreshIcon,
newLogCount,
hasSnapshotUpdates,
isVisuallyRefreshing,
handleRefresh,
handleExport,
Expand Down
Loading
Loading