From f288a22ce632e4b1b3490c76dc35dafa03f6d299 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 10:54:18 -0700 Subject: [PATCH 1/2] improvement(logs): add copy raw trace button to trace view header --- .../components/trace-view/trace-view.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 0a90434e568..726bc2bebe8 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -45,6 +45,7 @@ import { parseTime, } from '@/app/workspace/[workspaceId]/logs/components/log-details/utils' import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' const DEFAULT_TREE_PANE_WIDTH = 240 const MIN_TREE_PANE_WIDTH = 200 @@ -819,6 +820,7 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa */ export const TraceView = memo(function TraceView({ traceSpans, runCostDollars }: TraceViewProps) { const treeRef = useRef(null) + const { copied: traceCopied, copy: copyTrace } = useCopyToClipboard() const [searchQuery, setSearchQuery] = useState('') const [treePaneWidth, setTreePaneWidth] = useState(DEFAULT_TREE_PANE_WIDTH) const treePaneWidthRef = useRef(DEFAULT_TREE_PANE_WIDTH) @@ -1042,6 +1044,26 @@ export const TraceView = memo(function TraceView({ traceSpans, runCostDollars }: placeholder='Filter spans' className='w-[140px]' /> + + + + + + {traceCopied ? 'Copied' : 'Copy raw trace'} + + @@ -148,84 +135,3 @@ export function FileCards({ files, isExecutionFile = false, workspaceId }: FileC ) } - -export function FileDownload({ - file, - isExecutionFile = false, - className, - workspaceId, -}: { - file: FileData - isExecutionFile?: boolean - className?: string - workspaceId?: string -}) { - const [isDownloading, setIsDownloading] = useState(false) - const router = useRouter() - - const handleDownload = () => { - if (isDownloading) return - - setIsDownloading(true) - - try { - logger.info(`Initiating download for file: ${file.name}`) - - if (file.key.startsWith('url/')) { - if (file.url) { - window.open(file.url, '_blank') - logger.info(`Opened URL-type file directly: ${file.url}`) - return - } - throw new Error('URL is required for URL-type files') - } - - let resolvedWorkspaceId = workspaceId - if (!resolvedWorkspaceId && isExecutionFile) { - resolvedWorkspaceId = extractWorkspaceIdFromExecutionKey(file.key) || undefined - } else if (!resolvedWorkspaceId) { - const segments = file.key.split('/') - if (segments.length >= 2 && /^[a-f0-9-]{36}$/.test(segments[0])) { - resolvedWorkspaceId = segments[0] - } - } - - if (isExecutionFile) { - const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=execution` - window.open(serveUrl, '_blank') - logger.info(`Opened execution file serve URL: ${serveUrl}`) - } else { - const viewerUrl = resolvedWorkspaceId ? getViewerUrl(file.key, resolvedWorkspaceId) : null - - if (viewerUrl) { - router.push(viewerUrl) - logger.info(`Navigated to viewer URL: ${viewerUrl}`) - } else { - logger.warn( - `Could not construct viewer URL for file: ${file.name}, falling back to serve URL` - ) - const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace` - window.open(serveUrl, '_blank') - } - } - } catch (error) { - logger.error(`Failed to download file ${file.name}:`, error) - } finally { - setIsDownloading(false) - } - } - - return ( - - ) -} - -export default FileCards diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/index.ts index fae73a3ef9e..d384b447553 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/index.ts @@ -1 +1 @@ -export { FileCards, FileDownload } from './file-download' +export { FileCards } from './file-download' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 726bc2bebe8..fc4030998e8 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -429,7 +429,7 @@ function DetailCodeSection({ const [isOpen, setIsOpen] = useState(defaultOpen) const [isContextMenuOpen, setIsContextMenuOpen] = useState(false) const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }) - const [copied, setCopied] = useState(false) + const { copied, copy } = useCopyToClipboard({ resetMs: 1500 }) const contentRef = useRef(null) const { @@ -460,9 +460,7 @@ function DetailCodeSection({ } function handleCopy() { - navigator.clipboard.writeText(jsonString) - setCopied(true) - setTimeout(() => setCopied(false), 1500) + copy(jsonString) setIsContextMenuOpen(false) } diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 271e07ac503..0078febbf52 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -42,6 +42,7 @@ import { TriggerBadge, } from '@/app/workspace/[workspaceId]/logs/utils' import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { usePermissionConfig } from '@/hooks/use-permission-config' import { formatCost } from '@/providers/utils' import { useLogDetailsUIStore } from '@/stores/logs/store' @@ -60,8 +61,7 @@ function creditLabel(credits: number, dollars: number): string { export const WorkflowOutputSection = memo( function WorkflowOutputSection({ output }: { output: Record }) { const contentRef = useRef(null) - const [copied, setCopied] = useState(false) - const copyTimerRef = useRef(null) + const { copied, copy } = useCopyToClipboard({ resetMs: 1500 }) const [isContextMenuOpen, setIsContextMenuOpen] = useState(false) const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }) @@ -90,19 +90,10 @@ export const WorkflowOutputSection = memo( } function handleCopy() { - navigator.clipboard.writeText(jsonString) - setCopied(true) - if (copyTimerRef.current !== null) window.clearTimeout(copyTimerRef.current) - copyTimerRef.current = window.setTimeout(() => setCopied(false), 1500) + copy(jsonString) setIsContextMenuOpen(false) } - useEffect(() => { - return () => { - if (copyTimerRef.current !== null) window.clearTimeout(copyTimerRef.current) - } - }, []) - function handleSearch() { activateSearch() setIsContextMenuOpen(false) @@ -273,22 +264,15 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP const [isExecutionSnapshotOpen, setIsExecutionSnapshotOpen] = useState(false) const [activeTab, setActiveTab] = useState('overview') const [prevLogId, setPrevLogId] = useState(log.id) - const [copiedRunId, setCopiedRunId] = useState(false) + const { copied: copiedRunId, copy: copyRunId } = useCopyToClipboard({ resetMs: 1500 }) if (prevLogId !== log.id) { setPrevLogId(log.id) setActiveTab('overview') } - const copiedRunIdTimerRef = useRef(null) const scrollAreaRef = useRef(null) - useEffect(() => { - return () => { - if (copiedRunIdTimerRef.current !== null) window.clearTimeout(copiedRunIdTimerRef.current) - } - }, []) - const { config: permissionConfig } = usePermissionConfig() useEffect(() => { @@ -394,11 +378,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP ...(showTraceTab ? [{ value: 'trace', label: 'Trace' }] : []), ]} value={resolvedTab} - onChange={(v) => { - const tab = v as LogDetailsTab - setActiveTab(tab) - onActiveTabChange?.(tab) - }} + onChange={(v) => setActiveTab(v as LogDetailsTab)} /> {/* Overview Tab */} @@ -442,25 +422,9 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP tabIndex={0} aria-label='Copy run ID' className='flex h-10 min-w-0 cursor-pointer items-center justify-between gap-4 px-3 transition-colors hover-hover:bg-[var(--surface-2)]' - onClick={() => { - navigator.clipboard.writeText(log.executionId!) - if (copiedRunIdTimerRef.current) clearTimeout(copiedRunIdTimerRef.current) - setCopiedRunId(true) - copiedRunIdTimerRef.current = window.setTimeout( - () => setCopiedRunId(false), - 1500 - ) - }} + onClick={() => copyRunId(log.executionId!)} onKeyDown={(event) => - handleKeyboardActivation(event, () => { - navigator.clipboard.writeText(log.executionId!) - if (copiedRunIdTimerRef.current) clearTimeout(copiedRunIdTimerRef.current) - setCopiedRunId(true) - copiedRunIdTimerRef.current = window.setTimeout( - () => setCopiedRunId(false), - 1500 - ) - }) + handleKeyboardActivation(event, () => copyRunId(log.executionId!)) } >