248 lines
7.0 KiB
Dart
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...',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|