iop/apps/client/lib/widgets/runtime_panel_sections.dart

360 lines
No EOL
10 KiB
Dart

import 'package:flutter/material.dart';
import '../control_plane_status_client.dart';
class RuntimePanelDomainAgentsSection extends StatelessWidget {
final FleetEdgeView edge;
const RuntimePanelDomainAgentsSection({required this.edge, super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Domain Agents',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(height: 12),
_DomainAgentsGrid(edge: edge),
],
);
}
}
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 _DomainAgentsGrid extends StatelessWidget {
final FleetEdgeView edge;
const _DomainAgentsGrid({required this.edge});
@override
Widget build(BuildContext context) {
final agents = edge.domainAgents;
if (agents.isEmpty) {
return _DomainAgentsEmpty();
}
return LayoutBuilder(
builder: (context, constraints) {
final crossAxisCount = constraints.maxWidth > 600 ? 2 : 1;
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: agents.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
mainAxisExtent: 150,
),
itemBuilder: (context, index) {
return _DomainAgentCard(agent: agents[index]);
},
);
},
);
}
}
class _DomainAgentsEmpty extends StatelessWidget {
const _DomainAgentsEmpty();
@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 Domain Agents reported for this Edge.',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 13),
),
),
);
}
}
class _DomainAgentCard extends StatelessWidget {
final EdgeDomainAgentSummaryView agent;
const _DomainAgentCard({required this.agent});
@override
Widget build(BuildContext context) {
final stateColor = _getLifecycleStateColor(agent.lifecycleState);
return Card(
color: const Color(0xFF1E293B),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: stateColor.withValues(alpha: 0.3)),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
agent.agentKind.toUpperCase(),
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.white),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: stateColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
),
child: Text(
agent.lifecycleState.toUpperCase(),
style: TextStyle(color: stateColor, fontSize: 10, fontWeight: FontWeight.bold),
),
),
],
),
const SizedBox(height: 8),
Text(
agent.summary.isNotEmpty ? agent.summary : 'Available: ${agent.available}',
style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (agent.activeCommandId.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
'Active Command: ${agent.activeCommandId}',
style: const TextStyle(color: Color(0xFF818CF8), fontSize: 11, fontFamily: 'monospace'),
overflow: TextOverflow.ellipsis,
),
],
const Spacer(),
],
),
),
);
}
Color _getLifecycleStateColor(String state) {
switch (state.toLowerCase()) {
case 'ready':
case 'success':
return const Color(0xFF10B981);
case 'busy':
case 'running':
return const Color(0xFFF59E0B);
case 'error':
case 'failed':
return const Color(0xFFEF4444);
case 'unknown':
default:
return const Color(0xFF94A3B8);
}
}
}
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);
}
}
}