360 lines
No EOL
10 KiB
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);
|
|
}
|
|
}
|
|
} |