iop/apps/client/lib/widgets/runtime_panel_sections.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);
}
}
}