Files
zaloopipe/frontend/lib/screens/run_screen.dart
T

248 lines
7.0 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import '../api/client.dart';
import '../api/models.dart';
import '../widgets/run_status_badge.dart';
import '../widgets/step_progress.dart';
import 'log_viewer.dart';
class RunScreen extends StatefulWidget {
final Pipeline pipeline;
final APIClient api;
const RunScreen({super.key, required this.pipeline, required this.api});
@override
State<RunScreen> createState() => _RunScreenState();
}
class _RunScreenState extends State<RunScreen> {
String? _runId;
RunState? _runState;
String? _error;
bool _starting = true;
Timer? _pollTimer;
Future<void> _start() async {
try {
final data = await widget.api.startRun(widget.pipeline.id);
setState(() {
_runId = data['run_id'] as String;
_runState = RunState(
runId: data['run_id'] as String,
pipelineId: data['pipeline_id'] as String? ?? '',
status: parseRunStatus(data['status'] as String? ?? 'running'),
currentStep: -1,
steps: [],
);
_starting = false;
});
_startPolling();
} catch (e) {
setState(() {
_error = e.toString();
_starting = false;
});
}
}
void _startPolling() {
_pollTimer = Timer.periodic(const Duration(seconds: 2), (_) => _poll());
}
Future<void> _poll() async {
if (_runId == null) return;
try {
final state = await widget.api.getRunStatus(_runId!);
setState(() {
_runState = state;
});
if (state.isFinished) {
_pollTimer?.cancel();
}
} catch (e) {
// Silently ignore polling errors
}
}
@override
void dispose() {
_pollTimer?.cancel();
super.dispose();
}
@override
void initState() {
super.initState();
_start();
}
void _viewLog() {
if (_runId == null) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => LogViewerScreen(
runId: _runId!,
api: widget.api,
onStatusChanged: (finished) {
if (finished) _pollTimer?.cancel();
},
),
),
);
}
@override
Widget build(BuildContext context) {
if (_starting) {
return Scaffold(
appBar: AppBar(title: Text('Run: ${widget.pipeline.name}')),
body: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Starting run...'),
],
),
),
);
}
if (_error != null) {
return Scaffold(
appBar: AppBar(title: Text('Run: ${widget.pipeline.name}')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text(_error!, textAlign: TextAlign.center),
const SizedBox(height: 16),
FilledButton(
onPressed: () => Navigator.pop(context),
child: const Text('Go Back'),
),
],
),
),
);
}
if (_runId == null || _runState == null) {
return Scaffold(
appBar: AppBar(title: Text('Run: ${widget.pipeline.name}')),
body: const Center(child: Text('Failed to start run')),
);
}
final state = _runState!;
return Scaffold(
appBar: AppBar(
title: Text('Run: ${widget.pipeline.name}'),
actions: [RunStatusBadge(status: state.status)],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Run ID',
style: TextStyle(color: Colors.grey, fontSize: 12),
),
Text(
_runId!,
style: const TextStyle(fontFamily: 'monospace'),
),
const SizedBox(height: 8),
Row(
children: [
const Text(
'Status: ',
style: TextStyle(fontWeight: FontWeight.bold),
),
RunStatusBadge(status: state.status),
const SizedBox(width: 12),
if (state.currentStep >= 0)
Text(
'Step ${state.currentStep + 1}/${state.steps.length}',
),
],
),
],
),
),
),
const SizedBox(height: 20),
const Text(
'Steps',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
StepProgress(steps: state.steps, totalSteps: state.steps.length),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: SizedBox(
height: 48,
child: FilledButton.icon(
onPressed: _viewLog,
icon: const Icon(Icons.article_outlined),
label: const Text('View Log'),
),
),
),
if (state.isFinished) ...[
const SizedBox(width: 12),
Expanded(
child: SizedBox(
height: 48,
child: FilledButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.check),
label: const Text('Done'),
),
),
),
],
],
),
if (!state.isFinished)
Padding(
padding: const EdgeInsets.only(top: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(width: 12),
Text(
state.status == RunStatus.running
? 'Running... (polling every 2s)'
: 'Waiting...',
),
],
),
),
],
),
),
);
}
}