Skip to content

Commit ff53e0e

Browse files
committed
improvement(preview): nested workflows preview
1 parent a2c062e commit ff53e0e

File tree

19 files changed

+1148
-842
lines changed

19 files changed

+1148
-842
lines changed

apps/sim/app/templates/[id]/template.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { useSession } from '@/lib/auth/auth-client'
3636
import { cn } from '@/lib/core/utils/cn'
3737
import { getBaseUrl } from '@/lib/core/utils/urls'
3838
import type { CredentialRequirement } from '@/lib/workflows/credentials/credential-extractor'
39-
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/preview'
39+
import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview'
4040
import { getBlock } from '@/blocks/registry'
4141
import { useStarTemplate, useTemplate } from '@/hooks/queries/templates'
4242

@@ -330,7 +330,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
330330

331331
try {
332332
return (
333-
<WorkflowPreview
333+
<PreviewWorkflow
334334
workflowState={template.state}
335335
height='100%'
336336
width='100%'

apps/sim/app/templates/components/template-card.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Star, User } from 'lucide-react'
44
import { useParams, useRouter } from 'next/navigation'
55
import { VerifiedBadge } from '@/components/ui/verified-badge'
66
import { cn } from '@/lib/core/utils/cn'
7-
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/preview'
7+
import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview'
88
import { getBlock } from '@/blocks/registry'
99
import { useStarTemplate } from '@/hooks/queries/templates'
1010
import type { WorkflowState } from '@/stores/workflows/workflow/types'
@@ -200,7 +200,7 @@ function TemplateCardInner({
200200
className='pointer-events-none h-[180px] w-full cursor-pointer overflow-hidden rounded-[6px]'
201201
>
202202
{normalizedState && isInView ? (
203-
<WorkflowPreview
203+
<PreviewWorkflow
204204
workflowState={normalizedState}
205205
height={180}
206206
width='100%'

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx

Lines changed: 19 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3+
import { useCallback, useRef, useState } from 'react'
44
import { AlertCircle, Loader2 } from 'lucide-react'
55
import { createPortal } from 'react-dom'
66
import {
@@ -13,13 +13,8 @@ import {
1313
PopoverContent,
1414
PopoverItem,
1515
} from '@/components/emcn'
16-
import { redactApiKeys } from '@/lib/core/security/redaction'
1716
import { cn } from '@/lib/core/utils/cn'
18-
import {
19-
getLeftmostBlockId,
20-
PreviewEditor,
21-
WorkflowPreview,
22-
} from '@/app/workspace/[workspaceId]/w/components/preview'
17+
import { Preview } from '@/app/workspace/[workspaceId]/w/components/preview'
2318
import { useExecutionSnapshot } from '@/hooks/queries/logs'
2419
import type { WorkflowState } from '@/stores/workflows/workflow/types'
2520

@@ -32,13 +27,6 @@ interface TraceSpan {
3227
children?: TraceSpan[]
3328
}
3429

35-
interface BlockExecutionData {
36-
input: unknown
37-
output: unknown
38-
status: string
39-
durationMs: number
40-
}
41-
4230
interface MigratedWorkflowState extends WorkflowState {
4331
_migrated: true
4432
_note?: string
@@ -70,98 +58,35 @@ export function ExecutionSnapshot({
7058
onClose = () => {},
7159
}: ExecutionSnapshotProps) {
7260
const { data, isLoading, error } = useExecutionSnapshot(executionId)
73-
const [pinnedBlockId, setPinnedBlockId] = useState<string | null>(null)
74-
const autoSelectedForExecutionRef = useRef<string | null>(null)
61+
const lastExecutionIdRef = useRef<string | null>(null)
7562

7663
const [isMenuOpen, setIsMenuOpen] = useState(false)
7764
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 })
78-
const [contextMenuBlockId, setContextMenuBlockId] = useState<string | null>(null)
7965
const menuRef = useRef<HTMLDivElement>(null)
8066

8167
const closeMenu = useCallback(() => {
8268
setIsMenuOpen(false)
83-
setContextMenuBlockId(null)
8469
}, [])
8570

8671
const handleCanvasContextMenu = useCallback((e: React.MouseEvent) => {
8772
e.preventDefault()
8873
e.stopPropagation()
89-
setContextMenuBlockId(null)
9074
setMenuPosition({ x: e.clientX, y: e.clientY })
9175
setIsMenuOpen(true)
9276
}, [])
9377

94-
const handleNodeContextMenu = useCallback(
95-
(blockId: string, mousePosition: { x: number; y: number }) => {
96-
setContextMenuBlockId(blockId)
97-
setMenuPosition(mousePosition)
98-
setIsMenuOpen(true)
99-
},
100-
[]
101-
)
102-
10378
const handleCopyExecutionId = useCallback(() => {
10479
navigator.clipboard.writeText(executionId)
10580
closeMenu()
10681
}, [executionId, closeMenu])
10782

108-
const handleOpenDetails = useCallback(() => {
109-
if (contextMenuBlockId) {
110-
setPinnedBlockId(contextMenuBlockId)
111-
}
112-
closeMenu()
113-
}, [contextMenuBlockId, closeMenu])
114-
115-
const blockExecutions = useMemo(() => {
116-
if (!traceSpans || !Array.isArray(traceSpans)) return {}
117-
118-
const blockExecutionMap: Record<string, BlockExecutionData> = {}
119-
120-
const collectBlockSpans = (spans: TraceSpan[]): TraceSpan[] => {
121-
const blockSpans: TraceSpan[] = []
122-
123-
for (const span of spans) {
124-
if (span.blockId) {
125-
blockSpans.push(span)
126-
}
127-
if (span.children && Array.isArray(span.children)) {
128-
blockSpans.push(...collectBlockSpans(span.children))
129-
}
130-
}
131-
132-
return blockSpans
133-
}
134-
135-
const allBlockSpans = collectBlockSpans(traceSpans)
136-
137-
for (const span of allBlockSpans) {
138-
if (span.blockId && !blockExecutionMap[span.blockId]) {
139-
blockExecutionMap[span.blockId] = {
140-
input: redactApiKeys(span.input || {}),
141-
output: redactApiKeys(span.output || {}),
142-
status: span.status || 'unknown',
143-
durationMs: span.duration || 0,
144-
}
145-
}
146-
}
147-
148-
return blockExecutionMap
149-
}, [traceSpans])
150-
15183
const workflowState = data?.workflowState as WorkflowState | undefined
15284

153-
// Auto-select the leftmost block once when data loads for a new executionId
154-
useEffect(() => {
155-
if (
156-
workflowState &&
157-
!isMigratedWorkflowState(workflowState) &&
158-
autoSelectedForExecutionRef.current !== executionId
159-
) {
160-
autoSelectedForExecutionRef.current = executionId
161-
const leftmostId = getLeftmostBlockId(workflowState)
162-
setPinnedBlockId(leftmostId)
163-
}
164-
}, [executionId, workflowState])
85+
// Track execution ID changes for key reset
86+
const executionKey = executionId !== lastExecutionIdRef.current ? executionId : undefined
87+
if (executionId !== lastExecutionIdRef.current) {
88+
lastExecutionIdRef.current = executionId
89+
}
16590

16691
const renderContent = () => {
16792
if (isLoading) {
@@ -226,44 +151,17 @@ export function ExecutionSnapshot({
226151
}
227152

228153
return (
229-
<div
230-
style={{ height, width }}
231-
className={cn(
232-
'flex overflow-hidden',
233-
!isModal && 'rounded-[4px] border border-[var(--border)]',
234-
className
235-
)}
236-
>
237-
<div className='h-full flex-1' onContextMenu={handleCanvasContextMenu}>
238-
<WorkflowPreview
239-
workflowState={workflowState}
240-
isPannable={true}
241-
defaultPosition={{ x: 0, y: 0 }}
242-
defaultZoom={0.8}
243-
onNodeClick={(blockId) => {
244-
setPinnedBlockId(blockId)
245-
}}
246-
onNodeContextMenu={handleNodeContextMenu}
247-
onPaneClick={() => setPinnedBlockId(null)}
248-
cursorStyle='pointer'
249-
executedBlocks={blockExecutions}
250-
selectedBlockId={pinnedBlockId}
251-
/>
252-
</div>
253-
{pinnedBlockId && workflowState.blocks[pinnedBlockId] && (
254-
<PreviewEditor
255-
block={workflowState.blocks[pinnedBlockId]}
256-
executionData={blockExecutions[pinnedBlockId]}
257-
allBlockExecutions={blockExecutions}
258-
workflowBlocks={workflowState.blocks}
259-
workflowVariables={workflowState.variables}
260-
loops={workflowState.loops}
261-
parallels={workflowState.parallels}
262-
isExecutionMode
263-
onClose={() => setPinnedBlockId(null)}
264-
/>
265-
)}
266-
</div>
154+
<Preview
155+
key={executionKey}
156+
workflowState={workflowState}
157+
traceSpans={traceSpans}
158+
className={className}
159+
height={height}
160+
width={width}
161+
onCanvasContextMenu={handleCanvasContextMenu}
162+
showBorder={!isModal}
163+
autoSelectLeftmost
164+
/>
267165
)
268166
}
269167

@@ -287,9 +185,6 @@ export function ExecutionSnapshot({
287185
}}
288186
/>
289187
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
290-
{contextMenuBlockId && (
291-
<PopoverItem onClick={handleOpenDetails}>Open Details</PopoverItem>
292-
)}
293188
<PopoverItem onClick={handleCopyExecutionId}>Copy Execution ID</PopoverItem>
294189
</PopoverContent>
295190
</Popover>,
@@ -304,7 +199,6 @@ export function ExecutionSnapshot({
304199
open={isOpen}
305200
onOpenChange={(open) => {
306201
if (!open) {
307-
setPinnedBlockId(null)
308202
onClose()
309203
}
310204
}}

apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Star, User } from 'lucide-react'
33
import { useParams, useRouter } from 'next/navigation'
44
import { VerifiedBadge } from '@/components/ui/verified-badge'
55
import { cn } from '@/lib/core/utils/cn'
6-
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/preview'
6+
import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview'
77
import { getBlock } from '@/blocks/registry'
88
import { useStarTemplate } from '@/hooks/queries/templates'
99
import type { WorkflowState } from '@/stores/workflows/workflow/types'
@@ -206,7 +206,7 @@ function TemplateCardInner({
206206
className='pointer-events-none h-[180px] w-full overflow-hidden rounded-[6px]'
207207
>
208208
{normalizedState && isInView ? (
209-
<WorkflowPreview
209+
<PreviewWorkflow
210210
workflowState={normalizedState}
211211
height={180}
212212
width='100%'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx

Lines changed: 6 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3+
import { useCallback, useEffect, useMemo, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import {
66
Button,
@@ -17,11 +17,7 @@ import {
1717
} from '@/components/emcn'
1818
import { Skeleton } from '@/components/ui'
1919
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils'
20-
import {
21-
getLeftmostBlockId,
22-
PreviewEditor,
23-
WorkflowPreview,
24-
} from '@/app/workspace/[workspaceId]/w/components/preview'
20+
import { Preview, PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview'
2521
import { useDeploymentVersionState, useRevertToVersion } from '@/hooks/queries/workflows'
2622
import type { WorkflowState } from '@/stores/workflows/workflow/types'
2723
import { Versions } from './components'
@@ -59,8 +55,6 @@ export function GeneralDeploy({
5955
const [showLoadDialog, setShowLoadDialog] = useState(false)
6056
const [showPromoteDialog, setShowPromoteDialog] = useState(false)
6157
const [showExpandedPreview, setShowExpandedPreview] = useState(false)
62-
const [expandedSelectedBlockId, setExpandedSelectedBlockId] = useState<string | null>(null)
63-
const hasAutoSelectedRef = useRef(false)
6458
const [versionToLoad, setVersionToLoad] = useState<number | null>(null)
6559
const [versionToPromote, setVersionToPromote] = useState<number | null>(null)
6660

@@ -135,19 +129,6 @@ export function GeneralDeploy({
135129
const hasDeployedData = deployedState && Object.keys(deployedState.blocks || {}).length > 0
136130
const showLoadingSkeleton = isLoadingDeployedState && !hasDeployedData
137131

138-
// Auto-select the leftmost block once when expanded preview opens
139-
useEffect(() => {
140-
if (showExpandedPreview && workflowToShow && !hasAutoSelectedRef.current) {
141-
hasAutoSelectedRef.current = true
142-
const leftmostId = getLeftmostBlockId(workflowToShow)
143-
setExpandedSelectedBlockId(leftmostId)
144-
}
145-
// Reset when modal closes
146-
if (!showExpandedPreview) {
147-
hasAutoSelectedRef.current = false
148-
}
149-
}, [showExpandedPreview, workflowToShow])
150-
151132
if (showLoadingSkeleton) {
152133
return (
153134
<div className='space-y-[12px]'>
@@ -205,7 +186,7 @@ export function GeneralDeploy({
205186
{workflowToShow ? (
206187
<>
207188
<div className='[&_*]:!cursor-default h-full w-full cursor-default'>
208-
<WorkflowPreview
189+
<PreviewWorkflow
209190
workflowState={workflowToShow}
210191
height='100%'
211192
width='100%'
@@ -306,46 +287,15 @@ export function GeneralDeploy({
306287
</Modal>
307288

308289
{workflowToShow && (
309-
<Modal
310-
open={showExpandedPreview}
311-
onOpenChange={(open) => {
312-
if (!open) {
313-
setExpandedSelectedBlockId(null)
314-
}
315-
setShowExpandedPreview(open)
316-
}}
317-
>
290+
<Modal open={showExpandedPreview} onOpenChange={setShowExpandedPreview}>
318291
<ModalContent size='full' className='flex h-[90vh] flex-col'>
319292
<ModalHeader>
320293
{previewMode === 'selected' && selectedVersionInfo
321294
? selectedVersionInfo.name || `v${selectedVersion}`
322295
: 'Live Workflow'}
323296
</ModalHeader>
324-
<ModalBody className='!p-0 min-h-0 flex-1'>
325-
<div className='flex h-full w-full overflow-hidden'>
326-
<div className='h-full flex-1'>
327-
<WorkflowPreview
328-
workflowState={workflowToShow}
329-
isPannable={true}
330-
defaultPosition={{ x: 0, y: 0 }}
331-
defaultZoom={0.6}
332-
onNodeClick={(blockId) => {
333-
setExpandedSelectedBlockId(blockId)
334-
}}
335-
onPaneClick={() => setExpandedSelectedBlockId(null)}
336-
selectedBlockId={expandedSelectedBlockId}
337-
/>
338-
</div>
339-
{expandedSelectedBlockId && workflowToShow.blocks?.[expandedSelectedBlockId] && (
340-
<PreviewEditor
341-
block={workflowToShow.blocks[expandedSelectedBlockId]}
342-
workflowVariables={workflowToShow.variables}
343-
loops={workflowToShow.loops}
344-
parallels={workflowToShow.parallels}
345-
onClose={() => setExpandedSelectedBlockId(null)}
346-
/>
347-
)}
348-
</div>
297+
<ModalBody className='!p-0 min-h-0 flex-1 overflow-hidden'>
298+
<Preview workflowState={workflowToShow} autoSelectLeftmost />
349299
</ModalBody>
350300
</ModalContent>
351301
</Modal>

0 commit comments

Comments
 (0)