Skip to content

Commit e9c94fa

Browse files
authored
feat(logs): add copy link and deep link support for log entries (#3855)
* feat(logs): add copy link and deep link support for log entries * fix(logs): fetch next page when deep linked log is beyond initial page * fix(logs): move Link icon to emcn and handle clipboard rejections * fix(logs): track isFetching reactively and drop empty-list early-return - Remove guard that prevented clearing the pending ref when filters return no results - Use directly in the condition and add it to the effect deps so the effect re-triggers after a background refetch * fix(logs): guard deep-link ref clear until query has succeeded Only clear pendingExecutionIdRef when the query status is 'success', preventing premature clearing before the initial fetch completes. On mount, the query is disabled (isInitialized.current starts false), so hasNextPage is false but no data has loaded yet — the ref was being cleared in the same effect pass that set it. * fix(logs): guard fetchNextPage call until query has succeeded Add logsQuery.status === 'success' to the fetchNextPage branch so it mirrors the clear branch. On mount the query is disabled (isFetching is false, status is pending), causing the effect to call fetchNextPage() before the query is initialized — now both branches require success.
1 parent 72eea64 commit e9c94fa

File tree

4 files changed

+71
-28
lines changed

4 files changed

+71
-28
lines changed

apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
DropdownMenuSeparator,
99
DropdownMenuTrigger,
1010
} from '@/components/emcn'
11-
import { Copy, Eye, ListFilter, SquareArrowUpRight, X } from '@/components/emcn/icons'
11+
import { Copy, Eye, Link, ListFilter, SquareArrowUpRight, X } from '@/components/emcn/icons'
1212
import type { WorkflowLog } from '@/stores/logs/filters/types'
1313

1414
interface LogRowContextMenuProps {
@@ -17,6 +17,7 @@ interface LogRowContextMenuProps {
1717
onClose: () => void
1818
log: WorkflowLog | null
1919
onCopyExecutionId: () => void
20+
onCopyLink: () => void
2021
onOpenWorkflow: () => void
2122
onOpenPreview: () => void
2223
onToggleWorkflowFilter: () => void
@@ -35,6 +36,7 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
3536
onClose,
3637
log,
3738
onCopyExecutionId,
39+
onCopyLink,
3840
onOpenWorkflow,
3941
onOpenPreview,
4042
onToggleWorkflowFilter,
@@ -71,6 +73,10 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
7173
<Copy />
7274
Copy Execution ID
7375
</DropdownMenuItem>
76+
<DropdownMenuItem disabled={!hasExecutionId} onSelect={onCopyLink}>
77+
<Link />
78+
Copy Link
79+
</DropdownMenuItem>
7480

7581
<DropdownMenuSeparator />
7682
<DropdownMenuItem disabled={!hasWorkflow} onSelect={onOpenWorkflow}>

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

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -266,16 +266,17 @@ export default function Logs() {
266266
isSidebarOpen: false,
267267
})
268268
const isInitialized = useRef<boolean>(false)
269+
const pendingExecutionIdRef = useRef<string | null>(null)
269270

270271
const [searchQuery, setSearchQuery] = useState('')
271272
const debouncedSearchQuery = useDebounce(searchQuery, 300)
272273

273274
useEffect(() => {
274-
const urlSearch = new URLSearchParams(window.location.search).get('search') || ''
275-
if (urlSearch && urlSearch !== searchQuery) {
276-
setSearchQuery(urlSearch)
277-
}
278-
// eslint-disable-next-line react-hooks/exhaustive-deps
275+
const params = new URLSearchParams(window.location.search)
276+
const urlSearch = params.get('search')
277+
if (urlSearch) setSearchQuery(urlSearch)
278+
const urlExecutionId = params.get('executionId')
279+
if (urlExecutionId) pendingExecutionIdRef.current = urlExecutionId
279280
}, [])
280281

281282
const isLive = true
@@ -298,7 +299,6 @@ export default function Logs() {
298299
const [contextMenuOpen, setContextMenuOpen] = useState(false)
299300
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
300301
const [contextMenuLog, setContextMenuLog] = useState<WorkflowLog | null>(null)
301-
const contextMenuRef = useRef<HTMLDivElement>(null)
302302

303303
const [isPreviewOpen, setIsPreviewOpen] = useState(false)
304304
const [previewLogId, setPreviewLogId] = useState<string | null>(null)
@@ -417,28 +417,30 @@ export default function Logs() {
417417

418418
useFolders(workspaceId)
419419

420+
logsRef.current = sortedLogs
421+
selectedLogIndexRef.current = selectedLogIndex
422+
selectedLogIdRef.current = selectedLogId
423+
logsRefetchRef.current = logsQuery.refetch
424+
activeLogRefetchRef.current = activeLogQuery.refetch
425+
logsQueryRef.current = {
426+
isFetching: logsQuery.isFetching,
427+
hasNextPage: logsQuery.hasNextPage ?? false,
428+
fetchNextPage: logsQuery.fetchNextPage,
429+
}
430+
420431
useEffect(() => {
421-
logsRef.current = sortedLogs
422-
}, [sortedLogs])
423-
useEffect(() => {
424-
selectedLogIndexRef.current = selectedLogIndex
425-
}, [selectedLogIndex])
426-
useEffect(() => {
427-
selectedLogIdRef.current = selectedLogId
428-
}, [selectedLogId])
429-
useEffect(() => {
430-
logsRefetchRef.current = logsQuery.refetch
431-
}, [logsQuery.refetch])
432-
useEffect(() => {
433-
activeLogRefetchRef.current = activeLogQuery.refetch
434-
}, [activeLogQuery.refetch])
435-
useEffect(() => {
436-
logsQueryRef.current = {
437-
isFetching: logsQuery.isFetching,
438-
hasNextPage: logsQuery.hasNextPage ?? false,
439-
fetchNextPage: logsQuery.fetchNextPage,
432+
if (!pendingExecutionIdRef.current) return
433+
const targetExecutionId = pendingExecutionIdRef.current
434+
const found = sortedLogs.find((l) => l.executionId === targetExecutionId)
435+
if (found) {
436+
pendingExecutionIdRef.current = null
437+
dispatch({ type: 'TOGGLE_LOG', logId: found.id })
438+
} else if (!logsQuery.hasNextPage && logsQuery.status === 'success') {
439+
pendingExecutionIdRef.current = null
440+
} else if (!logsQuery.isFetching && logsQuery.status === 'success') {
441+
logsQueryRef.current.fetchNextPage()
440442
}
441-
}, [logsQuery.isFetching, logsQuery.hasNextPage, logsQuery.fetchNextPage])
443+
}, [sortedLogs, logsQuery.hasNextPage, logsQuery.isFetching, logsQuery.status])
442444

443445
useEffect(() => {
444446
const timers = refreshTimersRef.current
@@ -490,10 +492,17 @@ export default function Logs() {
490492

491493
const handleCopyExecutionId = useCallback(() => {
492494
if (contextMenuLog?.executionId) {
493-
navigator.clipboard.writeText(contextMenuLog.executionId)
495+
navigator.clipboard.writeText(contextMenuLog.executionId).catch(() => {})
494496
}
495497
}, [contextMenuLog])
496498

499+
const handleCopyLink = useCallback(() => {
500+
if (contextMenuLog?.executionId) {
501+
const url = `${window.location.origin}/workspace/${workspaceId}/logs?executionId=${contextMenuLog.executionId}`
502+
navigator.clipboard.writeText(url).catch(() => {})
503+
}
504+
}, [contextMenuLog, workspaceId])
505+
497506
const handleOpenWorkflow = useCallback(() => {
498507
const wfId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId
499508
if (wfId) {
@@ -1165,6 +1174,7 @@ export default function Logs() {
11651174
onClose={handleCloseContextMenu}
11661175
log={contextMenuLog}
11671176
onCopyExecutionId={handleCopyExecutionId}
1177+
onCopyLink={handleCopyLink}
11681178
onOpenWorkflow={handleOpenWorkflow}
11691179
onOpenPreview={handleOpenPreview}
11701180
onToggleWorkflowFilter={handleToggleWorkflowFilter}

apps/sim/components/emcn/icons/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export { Key } from './key'
4242
export { KeySquare } from './key-square'
4343
export { Layout } from './layout'
4444
export { Library } from './library'
45+
export { Link } from './link'
4546
export { ListFilter } from './list-filter'
4647
export { Loader } from './loader'
4748
export { Lock } from './lock'
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { SVGProps } from 'react'
2+
3+
/**
4+
* Link icon component
5+
* @param props - SVG properties including className, size, etc.
6+
*/
7+
export function Link(props: SVGProps<SVGSVGElement>) {
8+
return (
9+
<svg
10+
xmlns='http://www.w3.org/2000/svg'
11+
width='24'
12+
height='24'
13+
viewBox='0 0 24 24'
14+
fill='none'
15+
stroke='currentColor'
16+
strokeWidth='2'
17+
strokeLinecap='round'
18+
strokeLinejoin='round'
19+
aria-hidden='true'
20+
{...props}
21+
>
22+
<path d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71' />
23+
<path d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71' />
24+
</svg>
25+
)
26+
}

0 commit comments

Comments
 (0)