feat: add frontend app, improve static file serving with lazy loading and root redirect
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
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...',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user