Skip to content

Commit 4bd0f31

Browse files
authored
improvement: runpath edges, blocks, active (#2008)
1 parent f8070f9 commit 4bd0f31

File tree

10 files changed

+153
-89
lines changed

10 files changed

+153
-89
lines changed

apps/sim/app/globals.css

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -74,30 +74,6 @@
7474
animation: dash-animation 1.5s linear infinite !important;
7575
}
7676

77-
/**
78-
* Active block ring animation - cycles through gray tones using box-shadow
79-
*/
80-
@keyframes ring-pulse-colors {
81-
0%,
82-
100% {
83-
box-shadow: 0 0 0 4px var(--surface-14);
84-
}
85-
33% {
86-
box-shadow: 0 0 0 4px var(--surface-12);
87-
}
88-
66% {
89-
box-shadow: 0 0 0 4px var(--surface-15);
90-
}
91-
}
92-
93-
.dark .animate-ring-pulse {
94-
animation: ring-pulse-colors 2s ease-in-out infinite !important;
95-
}
96-
97-
.light .animate-ring-pulse {
98-
animation: ring-pulse-colors 2s ease-in-out infinite !important;
99-
}
100-
10177
/**
10278
* Dark color tokens - single source of truth for all colors (dark-only)
10379
*/
@@ -135,6 +111,7 @@
135111
--border-strong: #d1d1d1;
136112
--divider: #e5e5e5;
137113
--border-muted: #eeeeee;
114+
--border-success: #d5d5d5;
138115

139116
/* Brand & state */
140117
--brand-400: #8e4cfb;
@@ -250,6 +227,7 @@
250227
--border-strong: #303030;
251228
--divider: #393939;
252229
--border-muted: #424242;
230+
--border-success: #575757;
253231

254232
/* Brand & state */
255233
--brand-400: #8e4cfb;

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
343343
handleClick,
344344
hasRing,
345345
ringStyles,
346+
runPathStatus,
346347
} = useBlockCore({ blockId: id, data, isPending })
347348

348349
const currentBlock = currentWorkflow.getBlockById(id)
@@ -750,21 +751,26 @@ export const WorkflowBlock = memo(function WorkflowBlock({
750751
e.stopPropagation()
751752
}}
752753
>
753-
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
754+
<div className='relative z-10 flex min-w-0 flex-1 items-center gap-[10px]'>
754755
<div
755756
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
756-
style={{ backgroundColor: isEnabled ? config.bgColor : 'gray' }}
757+
style={{
758+
backgroundColor: isEnabled ? config.bgColor : 'gray',
759+
}}
757760
>
758761
<config.icon className='h-[16px] w-[16px] text-white' />
759762
</div>
760763
<span
761-
className={cn('truncate font-medium text-[16px]', !isEnabled && 'text-[#808080]')}
764+
className={cn(
765+
'truncate font-medium text-[16px]',
766+
!isEnabled && runPathStatus !== 'success' && 'text-[#808080]'
767+
)}
762768
title={name}
763769
>
764770
{name}
765771
</span>
766772
</div>
767-
<div className='flex flex-shrink-0 items-center gap-2'>
773+
<div className='relative z-10 flex flex-shrink-0 items-center gap-2'>
768774
{isWorkflowSelector && childWorkflowId && (
769775
<>
770776
{typeof childIsDeployed === 'boolean' ? (
@@ -890,6 +896,14 @@ export const WorkflowBlock = memo(function WorkflowBlock({
890896
</Tooltip.Content>
891897
</Tooltip.Root>
892898
)}
899+
{/* {isActive && (
900+
<div className='mr-[2px] ml-2 flex h-[16px] w-[16px] items-center justify-center'>
901+
<div
902+
className='h-full w-full animate-spin-slow rounded-full border-[2.5px] border-[rgba(255,102,0,0.25)] border-t-[var(--warning)]'
903+
aria-hidden='true'
904+
/>
905+
</div>
906+
)} */}
893907
</div>
894908
</div>
895909

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { X } from 'lucide-react'
22
import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath } from 'reactflow'
33
import type { EdgeDiffStatus } from '@/lib/workflows/diff/types'
4+
import { useExecutionStore } from '@/stores/execution/store'
45
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
56

67
interface WorkflowEdgeProps extends EdgeProps {
@@ -43,6 +44,7 @@ export const WorkflowEdge = ({
4344
const diffAnalysis = useWorkflowDiffStore((state) => state.diffAnalysis)
4445
const isShowingDiff = useWorkflowDiffStore((state) => state.isShowingDiff)
4546
const isDiffReady = useWorkflowDiffStore((state) => state.isDiffReady)
47+
const lastRunEdges = useExecutionStore((state) => state.lastRunEdges)
4648

4749
const generateEdgeIdentity = (
4850
sourceId: string,
@@ -78,10 +80,16 @@ export const WorkflowEdge = ({
7880
const dataSourceHandle = (data as { sourceHandle?: string } | undefined)?.sourceHandle
7981
const isErrorEdge = (sourceHandle ?? dataSourceHandle) === 'error'
8082

83+
// Check if this edge was traversed during last execution
84+
const edgeRunStatus = lastRunEdges.get(id)
85+
8186
const getEdgeColor = () => {
8287
if (edgeDiffStatus === 'deleted') return 'var(--text-error)'
8388
if (isErrorEdge) return 'var(--text-error)'
8489
if (edgeDiffStatus === 'new') return 'var(--brand-tertiary)'
90+
// Show run path status if edge was traversed
91+
if (edgeRunStatus === 'success') return 'var(--border-success)'
92+
if (edgeRunStatus === 'error') return 'var(--text-error)'
8593
return 'var(--surface-12)'
8694
}
8795

Lines changed: 15 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useCallback, useMemo } from 'react'
2-
import { cn } from '@/lib/utils'
32
import { useExecutionStore } from '@/stores/execution/store'
43
import { usePanelEditorStore } from '@/stores/panel-new/editor/store'
54
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
65
import { useBlockState } from '../components/workflow-block/hooks'
76
import type { WorkflowBlockProps } from '../components/workflow-block/types'
7+
import { getBlockRingStyles } from '../utils/block-ring-utils'
88
import { useCurrentWorkflow } from './use-current-workflow'
99

1010
interface UseBlockCoreOptions {
@@ -43,60 +43,19 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
4343
}, [blockId, setCurrentBlockId])
4444

4545
// Ring styling based on all states
46-
// Priority: active (animated) > pending > focused > deleted > diff > run path
47-
const { hasRing, ringStyles } = useMemo(() => {
48-
const hasRing =
49-
isActive ||
50-
isPending ||
51-
isFocused ||
52-
diffStatus === 'new' ||
53-
diffStatus === 'edited' ||
54-
isDeletedBlock ||
55-
!!runPathStatus
56-
57-
const ringStyles = cn(
58-
// Executing block: animated ring cycling through gray tones (animation handles all styling)
59-
isActive && 'animate-ring-pulse',
60-
// Non-active states use standard ring utilities
61-
!isActive && hasRing && 'ring-[1.75px]',
62-
// Pending state: warning ring
63-
!isActive && isPending && 'ring-[var(--warning)]',
64-
// Focused (selected) state: brand ring
65-
!isActive && !isPending && isFocused && 'ring-[var(--brand-secondary)]',
66-
// Deleted state (highest priority after active/pending/focused)
67-
!isActive && !isPending && !isFocused && isDeletedBlock && 'ring-[var(--text-error)]',
68-
// Diff states
69-
!isActive &&
70-
!isPending &&
71-
!isFocused &&
72-
!isDeletedBlock &&
73-
diffStatus === 'new' &&
74-
'ring-[#22C55E]',
75-
!isActive &&
76-
!isPending &&
77-
!isFocused &&
78-
!isDeletedBlock &&
79-
diffStatus === 'edited' &&
80-
'ring-[var(--warning)]',
81-
// Run path states (lowest priority - only show if no other states active)
82-
!isActive &&
83-
!isPending &&
84-
!isFocused &&
85-
!isDeletedBlock &&
86-
!diffStatus &&
87-
runPathStatus === 'success' &&
88-
'ring-[var(--surface-14)]',
89-
!isActive &&
90-
!isPending &&
91-
!isFocused &&
92-
!isDeletedBlock &&
93-
!diffStatus &&
94-
runPathStatus === 'error' &&
95-
'ring-[var(--text-error)]'
96-
)
97-
98-
return { hasRing, ringStyles }
99-
}, [isActive, isPending, isFocused, diffStatus, isDeletedBlock, runPathStatus])
46+
// Priority: active (executing) > pending > focused > deleted > diff > run path
47+
const { hasRing, ringClassName: ringStyles } = useMemo(
48+
() =>
49+
getBlockRingStyles({
50+
isActive,
51+
isPending,
52+
isFocused,
53+
isDeletedBlock,
54+
diffStatus,
55+
runPathStatus,
56+
}),
57+
[isActive, isPending, isFocused, isDeletedBlock, diffStatus, runPathStatus]
58+
)
10059

10160
return {
10261
// Workflow context
@@ -116,5 +75,6 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
11675
// Ring styling
11776
hasRing,
11877
ringStyles,
78+
runPathStatus,
11979
}
12080
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export function useWorkflowExecution() {
100100
setDebugContext,
101101
setActiveBlocks,
102102
setBlockRunStatus,
103+
setEdgeRunStatus,
103104
} = useExecutionStore()
104105
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
105106
const executionStream = useExecutionStream()
@@ -892,6 +893,12 @@ export function useWorkflowExecution() {
892893
activeBlocksSet.add(data.blockId)
893894
// Create a new Set to trigger React re-render
894895
setActiveBlocks(new Set(activeBlocksSet))
896+
897+
// Track edges that led to this block as soon as execution starts
898+
const incomingEdges = workflowEdges.filter((edge) => edge.target === data.blockId)
899+
incomingEdges.forEach((edge) => {
900+
setEdgeRunStatus(edge.id, 'success')
901+
})
895902
},
896903

897904
onBlockCompleted: (data) => {
@@ -904,6 +911,8 @@ export function useWorkflowExecution() {
904911
// Track successful block execution in run path
905912
setBlockRunStatus(data.blockId, 'success')
906913

914+
// Edges already tracked in onBlockStarted, no need to track again
915+
907916
// Add to console
908917
addConsole({
909918
input: data.input || {},
@@ -938,7 +947,6 @@ export function useWorkflowExecution() {
938947

939948
// Track failed block execution in run path
940949
setBlockRunStatus(data.blockId, 'error')
941-
942950
// Add error to console
943951
addConsole({
944952
input: data.input || {},
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { cn } from '@/lib/utils'
2+
3+
export type BlockDiffStatus = 'new' | 'edited' | null | undefined
4+
5+
export type BlockRunPathStatus = 'success' | 'error' | undefined
6+
7+
export interface BlockRingOptions {
8+
isActive: boolean
9+
isPending: boolean
10+
isFocused: boolean
11+
isDeletedBlock: boolean
12+
diffStatus: BlockDiffStatus
13+
runPathStatus: BlockRunPathStatus
14+
}
15+
16+
/**
17+
* Derives visual ring visibility and class names for workflow blocks
18+
* based on execution, focus, diff, deletion, and run-path states.
19+
*/
20+
export function getBlockRingStyles(options: BlockRingOptions): {
21+
hasRing: boolean
22+
ringClassName: string
23+
} {
24+
const { isActive, isPending, isFocused, isDeletedBlock, diffStatus, runPathStatus } = options
25+
26+
const hasRing =
27+
isActive ||
28+
isPending ||
29+
isFocused ||
30+
diffStatus === 'new' ||
31+
diffStatus === 'edited' ||
32+
isDeletedBlock ||
33+
!!runPathStatus
34+
35+
const ringClassName = cn(
36+
// Executing block: pulsing success ring with prominent thickness
37+
isActive && 'ring-[3.5px] ring-[var(--border-success)] animate-ring-pulse',
38+
// Non-active states use standard ring utilities
39+
!isActive && hasRing && 'ring-[1.75px]',
40+
// Pending state: warning ring
41+
!isActive && isPending && 'ring-[var(--warning)]',
42+
// Focused (selected) state: brand ring
43+
!isActive && !isPending && isFocused && 'ring-[var(--brand-secondary)]',
44+
// Deleted state (highest priority after active/pending/focused)
45+
!isActive && !isPending && !isFocused && isDeletedBlock && 'ring-[var(--text-error)]',
46+
// Diff states
47+
!isActive &&
48+
!isPending &&
49+
!isFocused &&
50+
!isDeletedBlock &&
51+
diffStatus === 'new' &&
52+
'ring-[#22C55E]',
53+
!isActive &&
54+
!isPending &&
55+
!isFocused &&
56+
!isDeletedBlock &&
57+
diffStatus === 'edited' &&
58+
'ring-[var(--warning)]',
59+
// Run path states (lowest priority - only show if no other states active)
60+
!isActive &&
61+
!isPending &&
62+
!isFocused &&
63+
!isDeletedBlock &&
64+
!diffStatus &&
65+
runPathStatus === 'success' &&
66+
'ring-[var(--border-success)]',
67+
!isActive &&
68+
!isPending &&
69+
!isFocused &&
70+
!isDeletedBlock &&
71+
!diffStatus &&
72+
runPathStatus === 'error' &&
73+
'ring-[var(--text-error)]'
74+
)
75+
76+
return { hasRing, ringClassName }
77+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './auto-layout-utils'
2+
export * from './block-ring-utils'
23
export * from './workflow-execution-utils'

apps/sim/stores/execution/store.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const useExecutionStore = create<ExecutionState & ExecutionActions>()((se
5656
if (isExecuting) {
5757
set({ autoPanDisabled: false })
5858
// Clear run path when starting a new execution
59-
set({ lastRunPath: new Map() })
59+
set({ lastRunPath: new Map(), lastRunEdges: new Map() })
6060
}
6161
},
6262
setIsDebugging: (isDebugging) => set({ isDebugging }),
@@ -69,6 +69,12 @@ export const useExecutionStore = create<ExecutionState & ExecutionActions>()((se
6969
newRunPath.set(blockId, status)
7070
set({ lastRunPath: newRunPath })
7171
},
72-
clearRunPath: () => set({ lastRunPath: new Map() }),
72+
setEdgeRunStatus: (edgeId, status) => {
73+
const { lastRunEdges } = get()
74+
const newRunEdges = new Map(lastRunEdges)
75+
newRunEdges.set(edgeId, status)
76+
set({ lastRunEdges: newRunEdges })
77+
},
78+
clearRunPath: () => set({ lastRunPath: new Map(), lastRunEdges: new Map() }),
7379
reset: () => set(initialState),
7480
}))

0 commit comments

Comments
 (0)