224 lines
5.9 KiB
Dart
224 lines
5.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../control_plane_status_client.dart';
|
|
|
|
class RuntimePanelOperationsHistorySection extends StatelessWidget {
|
|
final bool isLoading;
|
|
final String? error;
|
|
final EdgeOperationsResponseView? response;
|
|
|
|
const RuntimePanelOperationsHistorySection({
|
|
required this.isLoading,
|
|
required this.error,
|
|
required this.response,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Operations Execution History',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_OperationsHistoryList(
|
|
isLoading: isLoading,
|
|
error: error,
|
|
response: response,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OperationsHistoryList extends StatelessWidget {
|
|
final bool isLoading;
|
|
final String? error;
|
|
final EdgeOperationsResponseView? response;
|
|
|
|
const _OperationsHistoryList({
|
|
required this.isLoading,
|
|
required this.error,
|
|
required this.response,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// not-loaded: fetch has not completed yet
|
|
if (isLoading && response == null) {
|
|
return const _OperationsHistoryLoading();
|
|
}
|
|
|
|
// error from repository fetch
|
|
if (error != null) {
|
|
return _OperationsHistoryError(message: error!);
|
|
}
|
|
|
|
// loaded-empty: fetch completed with empty operations
|
|
final operations = response?.operations ?? const <EdgeCommandRecordView>[];
|
|
if (operations.isEmpty) {
|
|
return const _OperationsHistoryEmpty();
|
|
}
|
|
|
|
return ListView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: operations.length,
|
|
itemBuilder: (context, index) {
|
|
return _OperationRecordCard(record: operations[index]);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OperationsHistoryLoading extends StatelessWidget {
|
|
const _OperationsHistoryLoading();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(24.0),
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6366F1)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OperationsHistoryError extends StatelessWidget {
|
|
final String message;
|
|
|
|
const _OperationsHistoryError({required this.message});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Text(
|
|
'Error: $message',
|
|
style: const TextStyle(color: Color(0xFFEF4444)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OperationsHistoryEmpty extends StatelessWidget {
|
|
const _OperationsHistoryEmpty();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(24),
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1E293B),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: const Center(
|
|
child: Text(
|
|
'No operation executions recorded.',
|
|
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 13),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OperationRecordCard extends StatelessWidget {
|
|
final EdgeCommandRecordView record;
|
|
|
|
const _OperationRecordCard({required this.record});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final opColor = _getOpStatusColor(record.status);
|
|
|
|
return Card(
|
|
color: const Color(0xFF1E293B),
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
side: BorderSide(color: const Color(0xFF334155)),
|
|
),
|
|
child: ListTile(
|
|
leading: Icon(
|
|
record.status.toLowerCase() == 'success'
|
|
? Icons.check_circle
|
|
: Icons.pending,
|
|
color: opColor,
|
|
),
|
|
title: Text(
|
|
record.operation,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'ID: ${record.commandId} | Selector: ${record.targetSelector.isNotEmpty ? record.targetSelector : "all"}',
|
|
style: const TextStyle(color: Color(0xFF64748B), fontSize: 11),
|
|
),
|
|
if (record.summary.isNotEmpty) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
record.summary,
|
|
style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
|
),
|
|
],
|
|
if (record.error.isNotEmpty) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Error: ${record.error}',
|
|
style: const TextStyle(color: Color(0xFFEF4444), fontSize: 12),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
trailing: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: opColor.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
record.status.toUpperCase(),
|
|
style: TextStyle(
|
|
color: opColor,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Color _getOpStatusColor(String status) {
|
|
switch (status.toLowerCase()) {
|
|
case 'success':
|
|
return const Color(0xFF10B981);
|
|
case 'accepted':
|
|
case 'pending':
|
|
return const Color(0xFFF59E0B);
|
|
case 'error':
|
|
case 'failed':
|
|
return const Color(0xFFEF4444);
|
|
default:
|
|
return const Color(0xFF94A3B8);
|
|
}
|
|
}
|
|
}
|