From 60496a4590140cb1aeda0cdf98a35abf557a71b3 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 30 Mar 2026 17:07:21 -0700 Subject: [PATCH 1/6] feat(logs): add copy link and deep link support for log entries --- .../log-row-context-menu.tsx | 7 +++ .../app/workspace/[workspaceId]/logs/logs.tsx | 60 +++++++++++-------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx index c4b232c8dac..b65ec3077eb 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx @@ -1,6 +1,7 @@ 'use client' import { memo } from 'react' +import { Link } from 'lucide-react' import { DropdownMenu, DropdownMenuContent, @@ -17,6 +18,7 @@ interface LogRowContextMenuProps { onClose: () => void log: WorkflowLog | null onCopyExecutionId: () => void + onCopyLink: () => void onOpenWorkflow: () => void onOpenPreview: () => void onToggleWorkflowFilter: () => void @@ -35,6 +37,7 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({ onClose, log, onCopyExecutionId, + onCopyLink, onOpenWorkflow, onOpenPreview, onToggleWorkflowFilter, @@ -71,6 +74,10 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({ Copy Execution ID + + + Copy Link + diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 85de3bebd0e..1f93934cc82 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -266,16 +266,17 @@ export default function Logs() { isSidebarOpen: false, }) const isInitialized = useRef(false) + const pendingExecutionIdRef = useRef(null) const [searchQuery, setSearchQuery] = useState('') const debouncedSearchQuery = useDebounce(searchQuery, 300) useEffect(() => { - const urlSearch = new URLSearchParams(window.location.search).get('search') || '' - if (urlSearch && urlSearch !== searchQuery) { - setSearchQuery(urlSearch) - } - // eslint-disable-next-line react-hooks/exhaustive-deps + const params = new URLSearchParams(window.location.search) + const urlSearch = params.get('search') + if (urlSearch) setSearchQuery(urlSearch) + const urlExecutionId = params.get('executionId') + if (urlExecutionId) pendingExecutionIdRef.current = urlExecutionId }, []) const isLive = true @@ -298,7 +299,6 @@ export default function Logs() { const [contextMenuOpen, setContextMenuOpen] = useState(false) const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }) const [contextMenuLog, setContextMenuLog] = useState(null) - const contextMenuRef = useRef(null) const [isPreviewOpen, setIsPreviewOpen] = useState(false) const [previewLogId, setPreviewLogId] = useState(null) @@ -417,28 +417,28 @@ export default function Logs() { useFolders(workspaceId) + logsRef.current = sortedLogs + selectedLogIndexRef.current = selectedLogIndex + selectedLogIdRef.current = selectedLogId + logsRefetchRef.current = logsQuery.refetch + activeLogRefetchRef.current = activeLogQuery.refetch + logsQueryRef.current = { + isFetching: logsQuery.isFetching, + hasNextPage: logsQuery.hasNextPage ?? false, + fetchNextPage: logsQuery.fetchNextPage, + } + useEffect(() => { - logsRef.current = sortedLogs - }, [sortedLogs]) - useEffect(() => { - selectedLogIndexRef.current = selectedLogIndex - }, [selectedLogIndex]) - useEffect(() => { - selectedLogIdRef.current = selectedLogId - }, [selectedLogId]) - useEffect(() => { - logsRefetchRef.current = logsQuery.refetch - }, [logsQuery.refetch]) - useEffect(() => { - activeLogRefetchRef.current = activeLogQuery.refetch - }, [activeLogQuery.refetch]) - useEffect(() => { - logsQueryRef.current = { - isFetching: logsQuery.isFetching, - hasNextPage: logsQuery.hasNextPage ?? false, - fetchNextPage: logsQuery.fetchNextPage, + if (!pendingExecutionIdRef.current || sortedLogs.length === 0) return + const targetExecutionId = pendingExecutionIdRef.current + const found = sortedLogs.find((l) => l.executionId === targetExecutionId) + if (found) { + pendingExecutionIdRef.current = null + dispatch({ type: 'TOGGLE_LOG', logId: found.id }) + } else if (!logsQuery.hasNextPage) { + pendingExecutionIdRef.current = null } - }, [logsQuery.isFetching, logsQuery.hasNextPage, logsQuery.fetchNextPage]) + }, [sortedLogs, logsQuery.hasNextPage]) useEffect(() => { const timers = refreshTimersRef.current @@ -494,6 +494,13 @@ export default function Logs() { } }, [contextMenuLog]) + const handleCopyLink = useCallback(() => { + if (contextMenuLog?.executionId) { + const url = `${window.location.origin}/workspace/${workspaceId}/logs?executionId=${contextMenuLog.executionId}` + navigator.clipboard.writeText(url) + } + }, [contextMenuLog, workspaceId]) + const handleOpenWorkflow = useCallback(() => { const wfId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId if (wfId) { @@ -1165,6 +1172,7 @@ export default function Logs() { onClose={handleCloseContextMenu} log={contextMenuLog} onCopyExecutionId={handleCopyExecutionId} + onCopyLink={handleCopyLink} onOpenWorkflow={handleOpenWorkflow} onOpenPreview={handleOpenPreview} onToggleWorkflowFilter={handleToggleWorkflowFilter} From 5c5a4d8a9b25bfeb410a9e08b62f0169b9ca53dc Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 30 Mar 2026 17:22:52 -0700 Subject: [PATCH 2/6] fix(logs): fetch next page when deep linked log is beyond initial page --- apps/sim/app/workspace/[workspaceId]/logs/logs.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 1f93934cc82..6cfa0a97381 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -437,6 +437,8 @@ export default function Logs() { dispatch({ type: 'TOGGLE_LOG', logId: found.id }) } else if (!logsQuery.hasNextPage) { pendingExecutionIdRef.current = null + } else if (!logsQueryRef.current.isFetching) { + logsQueryRef.current.fetchNextPage() } }, [sortedLogs, logsQuery.hasNextPage]) From 0fc52984893b204cfb58b065664bc087ca8d1ee1 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 30 Mar 2026 17:28:05 -0700 Subject: [PATCH 3/6] fix(logs): move Link icon to emcn and handle clipboard rejections --- .../log-row-context-menu.tsx | 3 +-- .../app/workspace/[workspaceId]/logs/logs.tsx | 4 +-- apps/sim/components/emcn/icons/index.ts | 1 + apps/sim/components/emcn/icons/link.tsx | 26 +++++++++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 apps/sim/components/emcn/icons/link.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx index b65ec3077eb..0a283a401a2 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx @@ -1,7 +1,6 @@ 'use client' import { memo } from 'react' -import { Link } from 'lucide-react' import { DropdownMenu, DropdownMenuContent, @@ -9,7 +8,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/emcn' -import { Copy, Eye, ListFilter, SquareArrowUpRight, X } from '@/components/emcn/icons' +import { Copy, Eye, Link, ListFilter, SquareArrowUpRight, X } from '@/components/emcn/icons' import type { WorkflowLog } from '@/stores/logs/filters/types' interface LogRowContextMenuProps { diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 6cfa0a97381..94d2fa32f30 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -492,14 +492,14 @@ export default function Logs() { const handleCopyExecutionId = useCallback(() => { if (contextMenuLog?.executionId) { - navigator.clipboard.writeText(contextMenuLog.executionId) + navigator.clipboard.writeText(contextMenuLog.executionId).catch(() => {}) } }, [contextMenuLog]) const handleCopyLink = useCallback(() => { if (contextMenuLog?.executionId) { const url = `${window.location.origin}/workspace/${workspaceId}/logs?executionId=${contextMenuLog.executionId}` - navigator.clipboard.writeText(url) + navigator.clipboard.writeText(url).catch(() => {}) } }, [contextMenuLog, workspaceId]) diff --git a/apps/sim/components/emcn/icons/index.ts b/apps/sim/components/emcn/icons/index.ts index 0e97f5bce57..bf109bf5ba0 100644 --- a/apps/sim/components/emcn/icons/index.ts +++ b/apps/sim/components/emcn/icons/index.ts @@ -42,6 +42,7 @@ export { Key } from './key' export { KeySquare } from './key-square' export { Layout } from './layout' export { Library } from './library' +export { Link } from './link' export { ListFilter } from './list-filter' export { Loader } from './loader' export { Lock } from './lock' diff --git a/apps/sim/components/emcn/icons/link.tsx b/apps/sim/components/emcn/icons/link.tsx new file mode 100644 index 00000000000..46ae6a130ea --- /dev/null +++ b/apps/sim/components/emcn/icons/link.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * Link icon component + * @param props - SVG properties including className, size, etc. + */ +export function Link(props: SVGProps) { + return ( + + ) +} From be5b3dfcd70a9e0ea72f1a48267dde978ab3fd9d Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 30 Mar 2026 17:59:41 -0700 Subject: [PATCH 4/6] 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 --- apps/sim/app/workspace/[workspaceId]/logs/logs.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 94d2fa32f30..7c02660131c 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -429,7 +429,7 @@ export default function Logs() { } useEffect(() => { - if (!pendingExecutionIdRef.current || sortedLogs.length === 0) return + if (!pendingExecutionIdRef.current) return const targetExecutionId = pendingExecutionIdRef.current const found = sortedLogs.find((l) => l.executionId === targetExecutionId) if (found) { @@ -437,10 +437,10 @@ export default function Logs() { dispatch({ type: 'TOGGLE_LOG', logId: found.id }) } else if (!logsQuery.hasNextPage) { pendingExecutionIdRef.current = null - } else if (!logsQueryRef.current.isFetching) { + } else if (!logsQuery.isFetching) { logsQueryRef.current.fetchNextPage() } - }, [sortedLogs, logsQuery.hasNextPage]) + }, [sortedLogs, logsQuery.hasNextPage, logsQuery.isFetching]) useEffect(() => { const timers = refreshTimersRef.current From 1715669a482e719a8ae8ba47968cfbc117c44655 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 30 Mar 2026 18:11:37 -0700 Subject: [PATCH 5/6] fix(logs): guard deep-link ref clear until query has succeeded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/sim/app/workspace/[workspaceId]/logs/logs.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 7c02660131c..478710ef7b9 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -435,12 +435,12 @@ export default function Logs() { if (found) { pendingExecutionIdRef.current = null dispatch({ type: 'TOGGLE_LOG', logId: found.id }) - } else if (!logsQuery.hasNextPage) { + } else if (!logsQuery.hasNextPage && logsQuery.status === 'success') { pendingExecutionIdRef.current = null } else if (!logsQuery.isFetching) { logsQueryRef.current.fetchNextPage() } - }, [sortedLogs, logsQuery.hasNextPage, logsQuery.isFetching]) + }, [sortedLogs, logsQuery.hasNextPage, logsQuery.isFetching, logsQuery.status]) useEffect(() => { const timers = refreshTimersRef.current From 48bdbe58319a3853993cc277743fee84e37b35f2 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 30 Mar 2026 18:25:16 -0700 Subject: [PATCH 6/6] fix(logs): guard fetchNextPage call until query has succeeded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/sim/app/workspace/[workspaceId]/logs/logs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 478710ef7b9..098f23158a8 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -437,7 +437,7 @@ export default function Logs() { dispatch({ type: 'TOGGLE_LOG', logId: found.id }) } else if (!logsQuery.hasNextPage && logsQuery.status === 'success') { pendingExecutionIdRef.current = null - } else if (!logsQuery.isFetching) { + } else if (!logsQuery.isFetching && logsQuery.status === 'success') { logsQueryRef.current.fetchNextPage() } }, [sortedLogs, logsQuery.hasNextPage, logsQuery.isFetching, logsQuery.status])