Skip to content

Commit 88d55a0

Browse files
committed
fix test run UI issues
1 parent 93144a4 commit 88d55a0

File tree

2 files changed

+85
-5
lines changed

2 files changed

+85
-5
lines changed

web-ui/src/components/graph/useTaskGraph.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,35 @@ export function useTaskGraph(
277277
maxLane = Math.max(maxLane, lane);
278278
}
279279

280+
// Compute per-row Y positions with extra gap at convergence/divergence transitions
281+
// When closing edges from a multi-lane row converge to a node that also fans out
282+
// to the next multi-lane row, the smoothstep curves overlap. Add extra gap there.
283+
const rowLaneCounts = new Map<number, number>();
284+
for (const r of sortedRows) {
285+
rowLaneCounts.set(r, byRow.get(r)!.length);
286+
}
287+
288+
const rowYPositions = new Map<number, number>();
289+
let currentY = 0;
290+
for (let i = 0; i < sortedRows.length; i++) {
291+
const r = sortedRows[i];
292+
rowYPositions.set(r, currentY);
293+
294+
if (i < sortedRows.length - 1) {
295+
let gap = VERTICAL_GAP;
296+
const thisLanes = rowLaneCounts.get(r) || 1;
297+
const nextLanes = rowLaneCounts.get(sortedRows[i + 1]) || 1;
298+
299+
// Add extra gap when multi-lane rows are close together
300+
// (converging edges from this row + diverging edges to next row would overlap)
301+
if (thisLanes > 1 || nextLanes > 1) {
302+
gap += 30;
303+
}
304+
305+
currentY += NODE_HEIGHT + gap;
306+
}
307+
}
308+
280309
// Convert to React Flow format
281310
const totalWidth = (maxLane + 1) * (NODE_WIDTH + HORIZONTAL_GAP) - HORIZONTAL_GAP;
282311
const offsetX = -totalWidth / 2;
@@ -294,7 +323,7 @@ export function useTaskGraph(
294323
type: 'taskNode',
295324
position: {
296325
x: offsetX + lane * (NODE_WIDTH + HORIZONTAL_GAP),
297-
y: nodeRow * (NODE_HEIGHT + VERTICAL_GAP),
326+
y: rowYPositions.get(nodeRow) || 0,
298327
},
299328
data: {
300329
task,

web-ui/src/pages/TestRun.tsx

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import TaskDetails from '../components/task/TaskDetails';
1111
import { TaskGraph } from '../components/graph';
1212
import { formatDuration, formatRelativeTime } from '../utils/time';
1313
import * as api from '../api/client';
14-
import type { SSEEvent, TaskDetails as TaskDetailsType, TaskLogEntry, TestRunDetails } from '../types/api';
14+
import type { SSEEvent, TaskDetails as TaskDetailsType, TaskLogEntry, TaskState, TestRunDetails } from '../types/api';
1515

1616
type ViewMode = 'list' | 'graph';
1717

@@ -75,7 +75,7 @@ function TestRun() {
7575
pendingTaskRefreshRef.current.add(taskIndex);
7676

7777
if (!refreshTimerRef.current) {
78-
refreshTimerRef.current = setTimeout(flushTaskRefresh, 5000);
78+
refreshTimerRef.current = setTimeout(flushTaskRefresh, 1000);
7979
}
8080
},
8181
[flushTaskRefresh]
@@ -149,8 +149,59 @@ function TestRun() {
149149
}
150150
break;
151151
case 'task.started':
152-
case 'task.completed':
153-
case 'task.failed':
152+
if (event.taskIndex !== undefined) {
153+
// Immediate optimistic update
154+
queryClient.setQueryData(queryKeys.testRunDetails(runIdNum), (oldData?: TestRunDetails) => {
155+
if (!oldData?.tasks) return oldData;
156+
return {
157+
...oldData,
158+
tasks: oldData.tasks.map((task) =>
159+
task.index === event.taskIndex
160+
? { ...task, started: true, status: 'running' as const, start_time: Date.now() }
161+
: task
162+
),
163+
};
164+
});
165+
scheduleTaskRefresh(event.taskIndex);
166+
}
167+
break;
168+
case 'task.completed': {
169+
if (event.taskIndex !== undefined) {
170+
const data = event.data as { result?: string };
171+
const result = data.result === 'success' ? 'success' : data.result === 'failure' ? 'failure' : 'none';
172+
queryClient.setQueryData(queryKeys.testRunDetails(runIdNum), (oldData?: TestRunDetails) => {
173+
if (!oldData?.tasks) return oldData;
174+
return {
175+
...oldData,
176+
tasks: oldData.tasks.map((task) =>
177+
task.index === event.taskIndex
178+
? { ...task, completed: true, status: 'complete' as const, result: result as TaskState['result'], stop_time: Date.now() }
179+
: task
180+
),
181+
};
182+
});
183+
scheduleTaskRefresh(event.taskIndex);
184+
}
185+
break;
186+
}
187+
case 'task.failed': {
188+
if (event.taskIndex !== undefined) {
189+
const data = event.data as { error?: string };
190+
queryClient.setQueryData(queryKeys.testRunDetails(runIdNum), (oldData?: TestRunDetails) => {
191+
if (!oldData?.tasks) return oldData;
192+
return {
193+
...oldData,
194+
tasks: oldData.tasks.map((task) =>
195+
task.index === event.taskIndex
196+
? { ...task, completed: true, status: 'complete' as const, result: 'failure' as const, result_error: data.error ?? '', stop_time: Date.now() }
197+
: task
198+
),
199+
};
200+
});
201+
scheduleTaskRefresh(event.taskIndex);
202+
}
203+
break;
204+
}
154205
case 'task.progress':
155206
if (event.taskIndex !== undefined) {
156207
scheduleTaskRefresh(event.taskIndex);

0 commit comments

Comments
 (0)