mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
+1
-2
@@ -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)
|
||||
|
||||
+3
-97
@@ -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
@@ -1 +1 @@
|
||||
export { FileCards, FileDownload } from './file-download'
|
||||
export { FileCards } from './file-download'
|
||||
|
||||
+24
-4
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user