feat(editor): live step status on the graph during runs

Step run events were previously visible only on the run tab.
Operators who triggered a run there and then switched to
the graph tab saw a static graph — the canvas had no idea
something was executing.

Wire the run events through the FlowEditorController so all
three tabs share the same status snapshot:

 - New StepRunStatus enum on the controller
   (idle / running / done / failed / awaiting).
 - controller.stepStatuses exposes the live map.
 - controller.updateStepStatus is called from the run tab's
   event handler — one source of truth, both views read it.
 - FlowCanvas drops its own stepStatuses parameter and
   pulls from the controller instead, so the canvas now
   shows the running pulse + done check + error cross + pause
   icon in each step's header live, in lockstep with the
   step list on the run tab.

When a fresh run starts (controller.running = true) the
status map is cleared so stale events from a previous run
don't paint the new one.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-01 01:47:30 +02:00
parent e7cd9ca13c
commit 6b70ba65fd
4 changed files with 57 additions and 13 deletions

View file

@ -54,12 +54,7 @@ const double _inputsY = 80;
class FlowCanvas extends StatefulWidget {
final FlowEditorController controller;
final Map<String, FlowNodeStatus> stepStatuses;
const FlowCanvas({
super.key,
required this.controller,
this.stepStatuses = const {},
});
const FlowCanvas({super.key, required this.controller});
@override
State<FlowCanvas> createState() => _FlowCanvasState();
@ -194,7 +189,8 @@ class _FlowCanvasState extends State<FlowCanvas> {
final pos = layout.positions[step.id];
if (pos == null) return const SizedBox.shrink();
final selected = widget.controller.selectedStepId == step.id;
final status = widget.stepStatuses[step.id] ?? FlowNodeStatus.idle;
final raw = widget.controller.stepStatuses[step.id] ?? StepRunStatus.idle;
final status = _toNodeStatus(raw);
return Positioned(
left: pos.x,
top: pos.y,
@ -591,6 +587,16 @@ class _DropTarget {
});
}
FlowNodeStatus _toNodeStatus(StepRunStatus s) {
return switch (s) {
StepRunStatus.idle => FlowNodeStatus.idle,
StepRunStatus.running => FlowNodeStatus.running,
StepRunStatus.done => FlowNodeStatus.done,
StepRunStatus.failed => FlowNodeStatus.failed,
StepRunStatus.awaiting => FlowNodeStatus.awaiting,
};
}
class _DotGridPainter extends CustomPainter {
final Color color;
_DotGridPainter({required this.color});

View file

@ -519,18 +519,31 @@ class _RunTabState extends State<RunTab> {
switch (event) {
case StepStarted():
_liveSteps[event.stepId] = const _StepState(kind: _StepKind.running);
widget.controller.updateStepStatus(
event.stepId,
StepRunStatus.running,
);
case StepCompleted():
_liveSteps[event.stepId] = _StepState(
kind: _StepKind.done,
durationMs: event.durationMs,
);
widget.controller.updateStepStatus(event.stepId, StepRunStatus.done);
case StepFailed():
_liveSteps[event.stepId] = _StepState(
kind: _StepKind.error,
error: event.error,
);
widget.controller.updateStepStatus(
event.stepId,
StepRunStatus.failed,
);
case StepAwaitingApproval():
_liveSteps[event.stepId] = const _StepState(kind: _StepKind.awaiting);
widget.controller.updateStepStatus(
event.stepId,
StepRunStatus.awaiting,
);
}
});
}