improvement(logs): add copy raw trace button to trace view header (#4968)

* improvement(logs): add copy raw trace button to trace view header

* improvement(logs): cleanup pass over log-details module
This commit is contained in:
Waleed
2026-06-11 11:18:01 -07:00
committed by GitHub
parent d41f0a0ef1
commit b8daa0f993
5 changed files with 36 additions and 147 deletions
@@ -1,7 +1,7 @@
'use client'
import type React from 'react'
import { useRef, useState } from 'react'
import { useState } from 'react'
import { AlertCircle } from 'lucide-react'
import { createPortal } from 'react-dom'
import {
@@ -65,7 +65,6 @@ export function ExecutionSnapshot({
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 })
const menuRef = useRef<HTMLDivElement>(null)
function closeMenu() {
setIsMenuOpen(false)
@@ -1,9 +1,8 @@
'use client'
import { useState } from 'react'
import { createLogger } from '@sim/logger'
import { useRouter } from 'next/navigation'
import { Button, Loader } from '@/components/emcn'
import { Button } from '@/components/emcn'
import { Download } from '@/components/emcn/icons'
import { extractWorkspaceIdFromExecutionKey, getViewerUrl } from '@/lib/uploads/utils/file-utils'
@@ -41,14 +40,9 @@ function formatFileSize(bytes: number): string {
}
function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps) {
const [isDownloading, setIsDownloading] = useState(false)
const router = useRouter()
const handleDownload = () => {
if (isDownloading) return
setIsDownloading(true)
try {
logger.info(`Initiating download for file: ${file.name}`)
@@ -91,8 +85,6 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
}
} catch (error) {
logger.error(`Failed to download file ${file.name}:`, error)
} finally {
setIsDownloading(false)
}
}
@@ -113,14 +105,9 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
variant='ghost'
className='!h-[20px] !px-1.5 !py-0 text-xs'
onClick={handleDownload}
disabled={isDownloading}
>
{isDownloading ? (
<Loader className='mr-1 size-[10px]' animate />
) : (
<Download className='mr-1 size-[10px]' />
)}
{isDownloading ? 'Opening...' : 'Download'}
<Download className='mr-1 size-[10px]' />
Download
</Button>
</div>
</div>
@@ -148,84 +135,3 @@ export function FileCards({ files, isExecutionFile = false, workspaceId }: FileC
</div>
)
}
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 (
<Button
variant='ghost'
className={`h-7 px-2 text-xs ${className}`}
onClick={handleDownload}
disabled={isDownloading}
>
{isDownloading ? <Loader className='size-3' animate /> : <Download className='size-[14px]' />}
{isDownloading ? 'Downloading...' : 'Download'}
</Button>
)
}
export default FileCards
@@ -1 +1 @@
export { FileCards, FileDownload } from './file-download'
export { FileCards } from './file-download'
@@ -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
@@ -428,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<HTMLDivElement>(null)
const {
@@ -459,9 +460,7 @@ function DetailCodeSection({
}
function handleCopy() {
navigator.clipboard.writeText(jsonString)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
copy(jsonString)
setIsContextMenuOpen(false)
}
@@ -819,6 +818,7 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa
*/
export const TraceView = memo(function TraceView({ traceSpans, runCostDollars }: TraceViewProps) {
const treeRef = useRef<HTMLDivElement>(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 +1042,26 @@ export const TraceView = memo(function TraceView({ traceSpans, runCostDollars }:
placeholder='Filter spans'
className='w-[140px]'
/>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
type='button'
variant='ghost'
className='!p-1'
onClick={() => copyTrace(JSON.stringify(traceSpans, null, 2))}
aria-label='Copy raw trace'
>
{traceCopied ? (
<Check className='size-[12px] text-[var(--text-success)]' />
) : (
<Clipboard className='size-[12px]' />
)}
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
{traceCopied ? 'Copied' : 'Copy raw trace'}
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
@@ -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<string, unknown> }) {
const contentRef = useRef<HTMLDivElement>(null)
const [copied, setCopied] = useState(false)
const copyTimerRef = useRef<number | null>(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<LogDetailsTab>('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<number | null>(null)
const scrollAreaRef = useRef<HTMLDivElement>(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!))
}
>
<span className='flex-shrink-0 font-medium text-[var(--text-tertiary)] text-caption'>