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..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 @@ -8,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 { @@ -17,6 +17,7 @@ interface LogRowContextMenuProps { onClose: () => void log: WorkflowLog | null onCopyExecutionId: () => void + onCopyLink: () => void onOpenWorkflow: () => void onOpenPreview: () => void onToggleWorkflowFilter: () => void @@ -35,6 +36,7 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({ onClose, log, onCopyExecutionId, + onCopyLink, onOpenWorkflow, onOpenPreview, onToggleWorkflowFilter, @@ -71,6 +73,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..098f23158a8 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,30 @@ 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) 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 && logsQuery.status === 'success') { + pendingExecutionIdRef.current = null + } else if (!logsQuery.isFetching && logsQuery.status === 'success') { + logsQueryRef.current.fetchNextPage() } - }, [logsQuery.isFetching, logsQuery.hasNextPage, logsQuery.fetchNextPage]) + }, [sortedLogs, logsQuery.hasNextPage, logsQuery.isFetching, logsQuery.status]) useEffect(() => { const timers = refreshTimersRef.current @@ -490,10 +492,17 @@ 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).catch(() => {}) + } + }, [contextMenuLog, workspaceId]) + const handleOpenWorkflow = useCallback(() => { const wfId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId if (wfId) { @@ -1165,6 +1174,7 @@ export default function Logs() { onClose={handleCloseContextMenu} log={contextMenuLog} onCopyExecutionId={handleCopyExecutionId} + onCopyLink={handleCopyLink} onOpenWorkflow={handleOpenWorkflow} onOpenPreview={handleOpenPreview} onToggleWorkflowFilter={handleToggleWorkflowFilter} 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 ( + + ) +}