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:
parent
e7cd9ca13c
commit
6b70ba65fd
4 changed files with 57 additions and 13 deletions
|
|
@ -24,6 +24,12 @@ import 'model/auto_layout.dart';
|
||||||
import 'model/flow_graph.dart';
|
import 'model/flow_graph.dart';
|
||||||
import 'model/layout_store.dart';
|
import 'model/layout_store.dart';
|
||||||
|
|
||||||
|
/// Mirrors `FlowNodeStatus` from the flow_node widget, but
|
||||||
|
/// lives in the controller so the run tab can update it
|
||||||
|
/// without importing widgets. The canvas adapts between
|
||||||
|
/// the two.
|
||||||
|
enum StepRunStatus { idle, running, done, failed, awaiting }
|
||||||
|
|
||||||
class FlowEditorController extends ChangeNotifier {
|
class FlowEditorController extends ChangeNotifier {
|
||||||
/// Active flow file name (without `.yaml`). `null` while
|
/// Active flow file name (without `.yaml`). `null` while
|
||||||
/// the editor is empty.
|
/// the editor is empty.
|
||||||
|
|
@ -64,6 +70,14 @@ class FlowEditorController extends ChangeNotifier {
|
||||||
bool get isRunning => _running;
|
bool get isRunning => _running;
|
||||||
bool _running = false;
|
bool _running = false;
|
||||||
|
|
||||||
|
/// Live per-step status for the in-flight or most-recent
|
||||||
|
/// run. The canvas pulls colours and icons out of this map
|
||||||
|
/// so the graph view shows real-time progress even when
|
||||||
|
/// the operator triggered the run from the run tab.
|
||||||
|
Map<String, StepRunStatus> get stepStatuses =>
|
||||||
|
Map.unmodifiable(_stepStatuses);
|
||||||
|
final Map<String, StepRunStatus> _stepStatuses = {};
|
||||||
|
|
||||||
/// Flag flipped during graph -> YAML emission so we don't
|
/// Flag flipped during graph -> YAML emission so we don't
|
||||||
/// loop: the text change listener skips a reparse if it's
|
/// loop: the text change listener skips a reparse if it's
|
||||||
/// already mid-emit.
|
/// already mid-emit.
|
||||||
|
|
@ -121,6 +135,17 @@ class FlowEditorController extends ChangeNotifier {
|
||||||
set running(bool v) {
|
set running(bool v) {
|
||||||
if (_running == v) return;
|
if (_running == v) return;
|
||||||
_running = v;
|
_running = v;
|
||||||
|
if (v) {
|
||||||
|
_stepStatuses.clear();
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the live status of one step. Called by the run
|
||||||
|
/// tab as events arrive from the host's FlowRunDriver.
|
||||||
|
void updateStepStatus(String stepId, StepRunStatus status) {
|
||||||
|
if (_stepStatuses[stepId] == status) return;
|
||||||
|
_stepStatuses[stepId] = status;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,7 @@ const double _inputsY = 80;
|
||||||
|
|
||||||
class FlowCanvas extends StatefulWidget {
|
class FlowCanvas extends StatefulWidget {
|
||||||
final FlowEditorController controller;
|
final FlowEditorController controller;
|
||||||
final Map<String, FlowNodeStatus> stepStatuses;
|
const FlowCanvas({super.key, required this.controller});
|
||||||
const FlowCanvas({
|
|
||||||
super.key,
|
|
||||||
required this.controller,
|
|
||||||
this.stepStatuses = const {},
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FlowCanvas> createState() => _FlowCanvasState();
|
State<FlowCanvas> createState() => _FlowCanvasState();
|
||||||
|
|
@ -194,7 +189,8 @@ class _FlowCanvasState extends State<FlowCanvas> {
|
||||||
final pos = layout.positions[step.id];
|
final pos = layout.positions[step.id];
|
||||||
if (pos == null) return const SizedBox.shrink();
|
if (pos == null) return const SizedBox.shrink();
|
||||||
final selected = widget.controller.selectedStepId == step.id;
|
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(
|
return Positioned(
|
||||||
left: pos.x,
|
left: pos.x,
|
||||||
top: pos.y,
|
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 {
|
class _DotGridPainter extends CustomPainter {
|
||||||
final Color color;
|
final Color color;
|
||||||
_DotGridPainter({required this.color});
|
_DotGridPainter({required this.color});
|
||||||
|
|
|
||||||
|
|
@ -519,18 +519,31 @@ class _RunTabState extends State<RunTab> {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case StepStarted():
|
case StepStarted():
|
||||||
_liveSteps[event.stepId] = const _StepState(kind: _StepKind.running);
|
_liveSteps[event.stepId] = const _StepState(kind: _StepKind.running);
|
||||||
|
widget.controller.updateStepStatus(
|
||||||
|
event.stepId,
|
||||||
|
StepRunStatus.running,
|
||||||
|
);
|
||||||
case StepCompleted():
|
case StepCompleted():
|
||||||
_liveSteps[event.stepId] = _StepState(
|
_liveSteps[event.stepId] = _StepState(
|
||||||
kind: _StepKind.done,
|
kind: _StepKind.done,
|
||||||
durationMs: event.durationMs,
|
durationMs: event.durationMs,
|
||||||
);
|
);
|
||||||
|
widget.controller.updateStepStatus(event.stepId, StepRunStatus.done);
|
||||||
case StepFailed():
|
case StepFailed():
|
||||||
_liveSteps[event.stepId] = _StepState(
|
_liveSteps[event.stepId] = _StepState(
|
||||||
kind: _StepKind.error,
|
kind: _StepKind.error,
|
||||||
error: event.error,
|
error: event.error,
|
||||||
);
|
);
|
||||||
|
widget.controller.updateStepStatus(
|
||||||
|
event.stepId,
|
||||||
|
StepRunStatus.failed,
|
||||||
|
);
|
||||||
case StepAwaitingApproval():
|
case StepAwaitingApproval():
|
||||||
_liveSteps[event.stepId] = const _StepState(kind: _StepKind.awaiting);
|
_liveSteps[event.stepId] = const _StepState(kind: _StepKind.awaiting);
|
||||||
|
widget.controller.updateStepStatus(
|
||||||
|
event.stepId,
|
||||||
|
StepRunStatus.awaiting,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,9 @@ steps:
|
||||||
outputs: {}
|
outputs: {}
|
||||||
''');
|
''');
|
||||||
final initialText = c.codeController.fullText;
|
final initialText = c.codeController.fullText;
|
||||||
c.applyGraphEdit(c.graph.withStepAdded(
|
c.applyGraphEdit(
|
||||||
const FlowStep(id: 'b', use: 'debug.echo@^0'),
|
c.graph.withStepAdded(const FlowStep(id: 'b', use: 'debug.echo@^0')),
|
||||||
));
|
);
|
||||||
expect(c.codeController.fullText, isNot(initialText));
|
expect(c.codeController.fullText, isNot(initialText));
|
||||||
expect(c.codeController.fullText, contains('id: b'));
|
expect(c.codeController.fullText, contains('id: b'));
|
||||||
expect(c.graph.steps, hasLength(2));
|
expect(c.graph.steps, hasLength(2));
|
||||||
|
|
@ -61,9 +61,9 @@ steps:
|
||||||
outputs: {}
|
outputs: {}
|
||||||
''');
|
''');
|
||||||
expect(c.isDirty, isFalse);
|
expect(c.isDirty, isFalse);
|
||||||
c.applyGraphEdit(c.graph.withStepAdded(
|
c.applyGraphEdit(
|
||||||
const FlowStep(id: 'b', use: 'debug.echo@^0'),
|
c.graph.withStepAdded(const FlowStep(id: 'b', use: 'debug.echo@^0')),
|
||||||
));
|
);
|
||||||
expect(c.isDirty, isTrue);
|
expect(c.isDirty, isTrue);
|
||||||
c.markSaved();
|
c.markSaved();
|
||||||
expect(c.isDirty, isFalse);
|
expect(c.isDirty, isFalse);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue