Skip to content

Commit d836cd9

Browse files
committed
fix(ui): Filter workflows properly for step counting and navigation
Pre-compute filtered step indices when skip failed conditions is enabled. Both forward and backward stepping now correctly skip filtered steps. Step counter and progress bar now show filtered counts. Handle edge case when toggling filter while on a filtered step by moving to nearest valid.
1 parent 8fe4d61 commit d836cd9

File tree

3 files changed

+111
-32
lines changed

3 files changed

+111
-32
lines changed

ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@goplasmatic/dataflow-ui",
3-
"version": "2.0.12",
3+
"version": "2.0.13",
44
"type": "module",
55
"description": "React visualization library for dataflow-rs workflow engine",
66
"author": "Plasmatic Engineering <shankar@goplasmatic.io>",

ui/src/components/workflow-visualizer/context/DebuggerContext.tsx

Lines changed: 101 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ const initialState: DebuggerState = {
2727
skipFailedConditions: false,
2828
};
2929

30+
/**
31+
* Get filtered step indices based on skipFailedConditions setting.
32+
* Returns indices of steps that should be shown during debugging.
33+
*/
34+
function getFilteredStepIndices(trace: ExecutionTrace | null, skipFailedConditions: boolean): number[] {
35+
if (!trace || trace.steps.length === 0) {
36+
return [];
37+
}
38+
if (!skipFailedConditions) {
39+
return trace.steps.map((_, i) => i);
40+
}
41+
return trace.steps
42+
.map((step, i) => ({ step, index: i }))
43+
.filter(({ step }) => step.result !== 'skipped')
44+
.map(({ index }) => index);
45+
}
46+
3047
/**
3148
* Debugger reducer
3249
*/
@@ -110,25 +127,30 @@ function debuggerReducer(state: DebuggerState, action: DebuggerAction): Debugger
110127
return state;
111128
}
112129

113-
let nextIndex = state.currentStepIndex + 1;
114-
115-
// If skip failed conditions is enabled, find the next executed step
116-
if (state.skipFailedConditions) {
117-
while (
118-
nextIndex < state.trace.steps.length &&
119-
state.trace.steps[nextIndex].result === 'skipped'
120-
) {
121-
nextIndex++;
122-
}
130+
const filteredIndices = getFilteredStepIndices(state.trace, state.skipFailedConditions);
131+
if (filteredIndices.length === 0) {
132+
return state;
123133
}
124134

125-
// If at or past end, just pause
126-
if (nextIndex >= state.trace.steps.length) {
135+
// Find current position in filtered list
136+
const currentFilteredPos = filteredIndices.findIndex(i => i === state.currentStepIndex);
137+
138+
let nextIndex: number;
139+
if (state.currentStepIndex === -1) {
140+
// At ready state, go to first filtered step
141+
nextIndex = filteredIndices[0];
142+
} else if (currentFilteredPos === -1) {
143+
// Current step is not in filtered list (shouldn't happen), go to first
144+
nextIndex = filteredIndices[0];
145+
} else if (currentFilteredPos >= filteredIndices.length - 1) {
146+
// At end of filtered steps, pause
127147
return {
128148
...state,
129-
currentStepIndex: state.trace.steps.length - 1, // Go to last step
130-
playbackState: 'paused', // Auto-pause at end
149+
playbackState: 'paused',
131150
};
151+
} else {
152+
// Move to next filtered step
153+
nextIndex = filteredIndices[currentFilteredPos + 1];
132154
}
133155

134156
return {
@@ -137,14 +159,38 @@ function debuggerReducer(state: DebuggerState, action: DebuggerAction): Debugger
137159
};
138160
}
139161

140-
case 'STEP_BACKWARD':
141-
// Allow going back to -1 (ready state)
142-
if (state.currentStepIndex <= -1) return state;
162+
case 'STEP_BACKWARD': {
163+
if (!state.trace || state.currentStepIndex <= -1) {
164+
return state;
165+
}
166+
167+
const filteredIndices = getFilteredStepIndices(state.trace, state.skipFailedConditions);
168+
if (filteredIndices.length === 0) {
169+
return {
170+
...state,
171+
currentStepIndex: -1,
172+
playbackState: 'paused',
173+
};
174+
}
175+
176+
// Find current position in filtered list
177+
const currentFilteredPos = filteredIndices.findIndex(i => i === state.currentStepIndex);
178+
179+
let prevIndex: number;
180+
if (currentFilteredPos <= 0) {
181+
// At or before first filtered step, go to ready state
182+
prevIndex = -1;
183+
} else {
184+
// Move to previous filtered step
185+
prevIndex = filteredIndices[currentFilteredPos - 1];
186+
}
187+
143188
return {
144189
...state,
145-
currentStepIndex: state.currentStepIndex - 1,
146-
playbackState: 'paused', // Pause on manual step
190+
currentStepIndex: prevIndex,
191+
playbackState: 'paused',
147192
};
193+
}
148194

149195
case 'GO_TO_STEP':
150196
if (!state.trace || action.index < 0 || action.index >= state.trace.steps.length) return state;
@@ -160,11 +206,28 @@ function debuggerReducer(state: DebuggerState, action: DebuggerAction): Debugger
160206
playbackSpeed: Math.max(100, Math.min(2000, action.speed)),
161207
};
162208

163-
case 'SET_SKIP_FAILED_CONDITIONS':
209+
case 'SET_SKIP_FAILED_CONDITIONS': {
210+
// If enabling filter and current step would be filtered out, move to nearest valid step
211+
if (action.skip && state.trace && state.currentStepIndex >= 0) {
212+
const currentStep = state.trace.steps[state.currentStepIndex];
213+
if (currentStep && currentStep.result === 'skipped') {
214+
// Find the next non-skipped step, or go to ready state
215+
const filteredIndices = getFilteredStepIndices(state.trace, true);
216+
const nextValidIndex = filteredIndices.find(i => i > state.currentStepIndex);
217+
const prevValidIndex = [...filteredIndices].reverse().find(i => i < state.currentStepIndex);
218+
219+
return {
220+
...state,
221+
skipFailedConditions: action.skip,
222+
currentStepIndex: nextValidIndex ?? prevValidIndex ?? -1,
223+
};
224+
}
225+
}
164226
return {
165227
...state,
166228
skipFailedConditions: action.skip,
167229
};
230+
}
168231

169232
default:
170233
return state;
@@ -204,6 +267,10 @@ interface DebuggerContextValue {
204267
hasTrace: boolean;
205268
progress: number;
206269
totalSteps: number;
270+
/** Current position within filtered steps (0-indexed), -1 if at ready state */
271+
currentFilteredPosition: number;
272+
/** Array of actual step indices that are shown (for navigation) */
273+
filteredStepIndices: number[];
207274
isEngineReady: boolean;
208275
skipFailedConditions: boolean;
209276
}
@@ -349,12 +416,20 @@ export function DebuggerProvider({
349416
? getChangesAtStep(state.trace, state.currentStepIndex)
350417
: [];
351418

352-
const totalSteps = state.trace ? state.trace.steps.length : 0;
419+
// Compute filtered indices for accurate step counting
420+
const filteredStepIndices = getFilteredStepIndices(state.trace, state.skipFailedConditions);
421+
const totalSteps = filteredStepIndices.length;
422+
423+
// Find current position within filtered steps
424+
const currentFilteredPos = state.currentStepIndex >= 0
425+
? filteredStepIndices.findIndex(i => i === state.currentStepIndex)
426+
: -1;
427+
353428
const isAtStart = state.currentStepIndex <= -1; // -1 is "ready" state (before step 0)
354-
const isAtEnd = state.currentStepIndex >= totalSteps - 1 && state.currentStepIndex >= 0;
429+
const isAtEnd = currentFilteredPos >= totalSteps - 1 && currentFilteredPos >= 0;
355430
const hasTrace = state.trace !== null && totalSteps > 0;
356-
const progress = totalSteps > 0 && state.currentStepIndex >= 0
357-
? (state.currentStepIndex + 1) / totalSteps
431+
const progress = totalSteps > 0 && currentFilteredPos >= 0
432+
? (currentFilteredPos + 1) / totalSteps
358433
: 0;
359434

360435
const value: DebuggerContextValue = {
@@ -384,6 +459,8 @@ export function DebuggerProvider({
384459
hasTrace,
385460
progress,
386461
totalSteps,
462+
currentFilteredPosition: currentFilteredPos,
463+
filteredStepIndices,
387464
isEngineReady,
388465
skipFailedConditions: state.skipFailedConditions,
389466
};

ui/src/components/workflow-visualizer/debug/IntegratedDebugToolbar.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,14 @@ export function IntegratedDebugToolbar({
5959
isAtEnd,
6060
hasTrace,
6161
totalSteps,
62+
currentFilteredPosition,
63+
filteredStepIndices,
6264
isEngineReady,
6365
skipFailedConditions,
6466
setSkipFailedConditions,
6567
} = useDebugger();
6668

67-
const { playbackState, currentStepIndex, isExecuting, executionError, trace } = state;
69+
const { playbackState, isExecuting, executionError, trace } = state;
6870

6971
// Track last execution to prevent duplicates
7072
const lastExecutionRef = useRef<{ workflows: string; payload: string } | null>(null);
@@ -123,12 +125,12 @@ export function IntegratedDebugToolbar({
123125
}
124126
}, [hasTrace, stop]);
125127

126-
// Go to last step
128+
// Go to last step (use actual index from filtered list)
127129
const goToLast = useCallback(() => {
128-
if (hasTrace && totalSteps > 0) {
129-
goToStep(totalSteps - 1);
130+
if (hasTrace && filteredStepIndices.length > 0) {
131+
goToStep(filteredStepIndices[filteredStepIndices.length - 1]);
130132
}
131-
}, [hasTrace, totalSteps, goToStep]);
133+
}, [hasTrace, filteredStepIndices, goToStep]);
132134

133135
// Debounced auto-execute when workflows or payload change
134136
useEffect(() => {
@@ -229,10 +231,10 @@ export function IntegratedDebugToolbar({
229231
if (!hasTrace) {
230232
return 'Ready';
231233
}
232-
if (currentStepIndex < 0) {
234+
if (currentFilteredPosition < 0) {
233235
return 'Ready';
234236
}
235-
return `Step ${currentStepIndex + 1} / ${totalSteps}`;
237+
return `Step ${currentFilteredPosition + 1} / ${totalSteps}`;
236238
};
237239

238240
return (

0 commit comments

Comments
 (0)