import 'package:flutter/material.dart'; import 'control_plane_status_client.dart'; class EdgesPanel extends StatelessWidget { final List edges; final bool isLoading; final String? error; final String? selectedEdgeId; final ValueChanged onSelectEdge; final VoidCallback onRefresh; const EdgesPanel({ super.key, required this.edges, required this.isLoading, required this.error, required this.selectedEdgeId, required this.onSelectEdge, required this.onRefresh, }); @override Widget build(BuildContext context) { if (isLoading && edges.isEmpty) { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF6366F1)), ), ); } if (error != null && edges.isEmpty) { return Center( child: Container( padding: const EdgeInsets.all(24), margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFEF4444).withValues(alpha: 0.3)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, color: Color(0xFFEF4444), size: 48), const SizedBox(height: 16), const Text( 'Failed to Load Edges', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 8), Text( error!, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13), textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: onRefresh, icon: const Icon(Icons.refresh), label: const Text('Retry'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6366F1), foregroundColor: Colors.white, ), ), ], ), ), ); } if (edges.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.dns_outlined, color: Color(0xFF6366F1), size: 64), const SizedBox(height: 16), const Text( 'No Edges Registered', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 8), const Text( 'Connect your edges to the Control Plane to see them here.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 14), textAlign: TextAlign.center, ), const SizedBox(height: 20), OutlinedButton.icon( onPressed: onRefresh, icon: const Icon(Icons.refresh), label: const Text('Refresh'), style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF6366F1), side: const BorderSide(color: Color(0xFF6366F1)), ), ), ], ), ); } final selectedEdge = edges.firstWhere( (e) => e.edgeId == selectedEdgeId, orElse: () => edges.first, ); return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Left Column: Edge List Expanded( flex: 2, child: Container( decoration: const BoxDecoration( border: Border(right: BorderSide(color: Color(0xFF334155))), ), child: ListView.builder( itemCount: edges.length, itemBuilder: (context, index) { final edge = edges[index]; final isSelected = edge.edgeId == selectedEdge.edgeId; return InkWell( onTap: () => onSelectEdge(edge.edgeId), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: isSelected ? const Color(0xFF1E293B) : Colors.transparent, border: const Border( bottom: BorderSide(color: Color(0xFF334155), width: 0.5), ), ), child: Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration( shape: BoxShape.circle, color: edge.connected ? const Color(0xFF10B981) : const Color(0xFF64748B), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( edge.edgeName.isNotEmpty ? edge.edgeName : edge.edgeId, style: TextStyle( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? const Color(0xFF6366F1) : Colors.white, fontSize: 15, ), ), const SizedBox(height: 4), Text( 'ID: ${edge.edgeId}', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), ), ], ), ), const Icon(Icons.chevron_right, color: Color(0xFF64748B), size: 16), ], ), ), ); }, ), ), ), // Right Column: Edge Details Expanded( flex: 3, child: Container( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( selectedEdge.edgeName.isNotEmpty ? selectedEdge.edgeName : selectedEdge.edgeId, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white), overflow: TextOverflow.ellipsis, ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: selectedEdge.connected ? const Color(0xFF10B981).withValues(alpha: 0.15) : const Color(0xFF64748B).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(20), border: Border.all( color: selectedEdge.connected ? const Color(0xFF10B981) : const Color(0xFF64748B), width: 1.5, ), ), child: Text( selectedEdge.connected ? 'CONNECTED' : 'DISCONNECTED', style: TextStyle( color: selectedEdge.connected ? const Color(0xFF10B981) : const Color(0xFF94A3B8), fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 24), _buildDetailRow('Edge ID', selectedEdge.edgeId), _buildDetailRow('Version', selectedEdge.version), _buildDetailRow('Protocol', selectedEdge.protocol), _buildDetailRow('Last Seen', selectedEdge.lastSeen.toLocal().toString()), const SizedBox(height: 24), const Text( 'Capabilities', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 12), if (selectedEdge.capabilities.isEmpty) const Text('No capabilities reported', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 13)) else Wrap( spacing: 8, runSpacing: 8, children: selectedEdge.capabilities.map((cap) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF6366F1).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFF6366F1).withValues(alpha: 0.3)), ), child: Text( cap, style: const TextStyle(color: Color(0xFF818CF8), fontSize: 12, fontWeight: FontWeight.w500), ), ); }).toList(), ), ], ), ), ), ], ); } Widget _buildDetailRow(String label, String value) { return Padding( padding: const EdgeInsets.only(bottom: 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12)), const SizedBox(height: 4), Text( value.isNotEmpty ? value : '-', style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), const Divider(color: Color(0xFF334155), height: 1), ], ), ); } } class NodesPanel extends StatelessWidget { final List edges; final String? selectedEdgeId; final EdgeStatusResponseView? selectedEdgeStatus; final bool isLoading; final String? error; final ValueChanged onSelectEdge; final VoidCallback onRefresh; const NodesPanel({ super.key, required this.edges, required this.selectedEdgeId, required this.selectedEdgeStatus, required this.isLoading, required this.error, required this.onSelectEdge, required this.onRefresh, }); @override Widget build(BuildContext context) { if (edges.isEmpty) { return const Center( child: Text( 'Please connect and register at least one Edge first.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 14), ), ); } final currentEdgeId = selectedEdgeId ?? edges.first.edgeId; final hasCurrentEdge = edges.any((e) => e.edgeId == currentEdgeId); final dropdownValue = hasCurrentEdge ? currentEdgeId : (edges.isNotEmpty ? edges.first.edgeId : null); return Container( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Expanded( child: Text( 'Nodes View', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 16), if (edges.isNotEmpty && dropdownValue != null) ...[ const Text('Active Edge: ', style: TextStyle(color: Color(0xFF94A3B8))), const SizedBox(width: 8), DropdownButton( value: dropdownValue, dropdownColor: const Color(0xFF1E293B), style: const TextStyle(color: Colors.white), underline: const SizedBox(), items: edges.map((e) { return DropdownMenuItem( value: e.edgeId, child: Text(e.edgeName.isNotEmpty ? e.edgeName : e.edgeId), ); }).toList(), onChanged: (val) { if (val != null) onSelectEdge(val); }, ), ], const SizedBox(width: 8), IconButton( icon: const Icon(Icons.refresh, color: Color(0xFF6366F1)), onPressed: onRefresh, ), ], ), const SizedBox(height: 20), Expanded(child: _buildNodeContent()), ], ), ); } Widget _buildNodeContent() { if (isLoading) { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF6366F1)), ), ); } if (error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: Color(0xFFEF4444), size: 48), const SizedBox(height: 16), const Text( 'Failed to Load Nodes Status', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 8), Text(error!, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13)), ], ), ); } final nodes = selectedEdgeStatus?.nodes ?? []; if (nodes.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.developer_board_off_outlined, color: Color(0xFF64748B), size: 56), const SizedBox(height: 16), Text( 'No Nodes Registered', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 4), Text( 'No active nodes have registered on this edge.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 13), ), ], ), ); } return ListView.builder( itemCount: nodes.length, itemBuilder: (context, index) { final node = nodes[index]; return Card( color: const Color(0xFF1E293B), margin: const EdgeInsets.only(bottom: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( color: node.connected ? const Color(0xFF10B981).withValues(alpha: 0.3) : const Color(0xFF334155), ), ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( node.alias.isNotEmpty ? node.alias : node.nodeId, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 4), Text( 'ID: ${node.nodeId}', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), ), ], ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: node.connected ? const Color(0xFF10B981).withValues(alpha: 0.1) : const Color(0xFFEF4444).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Text( node.connected ? 'CONNECTED' : 'OFFLINE', style: TextStyle( color: node.connected ? const Color(0xFF10B981) : const Color(0xFFEF4444), fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ], ), if (node.label.isNotEmpty) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF334155), borderRadius: BorderRadius.circular(4), ), child: Text( 'Label: ${node.label}', style: const TextStyle(color: Colors.white, fontSize: 12), ), ), ], if (node.config != null) ...[ const SizedBox(height: 16), const Divider(color: Color(0xFF334155)), const SizedBox(height: 12), Row( children: [ const Icon(Icons.bolt, size: 16, color: Color(0xFF6366F1)), const SizedBox(width: 6), Text( 'Concurrency Limit: ${node.config!.concurrency}', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13), ), ], ), const SizedBox(height: 12), const Text( 'Adapters', style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), if (node.config!.adapters.isEmpty) const Text('No active adapters configured', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 12)) else Wrap( spacing: 8, runSpacing: 8, children: node.config!.adapters.map((adapter) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: adapter.enabled ? const Color(0xFF10B981).withValues(alpha: 0.1) : const Color(0xFFEF4444).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all( color: adapter.enabled ? const Color(0xFF10B981) : const Color(0xFFEF4444), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( adapter.type, style: TextStyle( color: adapter.enabled ? const Color(0xFF10B981) : const Color(0xFFEF4444), fontSize: 12, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 4), Icon( adapter.enabled ? Icons.check : Icons.close, size: 12, color: adapter.enabled ? const Color(0xFF10B981) : const Color(0xFFEF4444), ), ], ), ); }).toList(), ), ], ], ), ), ); }, ); } } class ExecutionLogsPanel extends StatelessWidget { final List edges; final String? selectedEdgeId; final List events; final bool isLoading; final String? error; final ValueChanged onSelectEdge; final VoidCallback onRefresh; const ExecutionLogsPanel({ super.key, required this.edges, required this.selectedEdgeId, required this.events, required this.isLoading, required this.error, required this.onSelectEdge, required this.onRefresh, }); @override Widget build(BuildContext context) { if (edges.isEmpty) { return const Center( child: Text( 'Please connect and register at least one Edge first.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 14), ), ); } final currentEdgeId = selectedEdgeId ?? edges.first.edgeId; final hasCurrentEdge = edges.any((e) => e.edgeId == currentEdgeId); final dropdownValue = hasCurrentEdge ? currentEdgeId : (edges.isNotEmpty ? edges.first.edgeId : null); return Container( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Expanded( child: Text( 'Lifecycle Events & Logs', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 16), if (edges.isNotEmpty && dropdownValue != null) ...[ const Text('Filter Edge: ', style: TextStyle(color: Color(0xFF94A3B8))), const SizedBox(width: 8), DropdownButton( value: dropdownValue, dropdownColor: const Color(0xFF1E293B), style: const TextStyle(color: Colors.white), underline: const SizedBox(), items: edges.map((e) { return DropdownMenuItem( value: e.edgeId, child: Text(e.edgeName.isNotEmpty ? e.edgeName : e.edgeId), ); }).toList(), onChanged: (val) { if (val != null) onSelectEdge(val); }, ), ], const SizedBox(width: 8), IconButton( icon: const Icon(Icons.refresh, color: Color(0xFF6366F1)), onPressed: onRefresh, ), ], ), const SizedBox(height: 20), Expanded(child: _buildLogsContent()), ], ), ); } Widget _buildLogsContent() { if (isLoading) { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF6366F1)), ), ); } if (error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: Color(0xFFEF4444), size: 48), const SizedBox(height: 16), const Text( 'Failed to Load Events', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 8), Text(error!, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13)), ], ), ); } if (events.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.history_toggle_off, color: Color(0xFF64748B), size: 56), const SizedBox(height: 16), Text( 'No Events Captured', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 4), Text( 'No lifecycle events have been recorded for this edge yet.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 13), ), ], ), ); } return ListView.builder( itemCount: events.length, itemBuilder: (context, index) { final event = events[index]; final eventColor = _getEventColor(event.type); final eventIcon = _getEventIcon(event.type); return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(8), border: Border( left: BorderSide(color: eventColor, width: 4), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(eventIcon, size: 16, color: eventColor), const SizedBox(width: 8), Text( event.type.toUpperCase(), style: TextStyle(color: eventColor, fontSize: 12, fontWeight: FontWeight.bold), ), const Spacer(), Text( event.timestamp.toLocal().toString().substring(11, 19), style: const TextStyle(color: Color(0xFF64748B), fontSize: 12), ), ], ), const SizedBox(height: 8), Text( event.reason, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Row( children: [ const Text('Node: ', style: TextStyle(color: Color(0xFF64748B), fontSize: 12)), Text( event.alias.isNotEmpty ? event.alias : (event.nodeId.isNotEmpty ? event.nodeId : 'Edge System'), style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), ), if (event.source.isNotEmpty) ...[ const SizedBox(width: 12), const Text('Source: ', style: TextStyle(color: Color(0xFF64748B), fontSize: 12)), Text( event.source, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), ), ], ], ), ], ), ); }, ); } Color _getEventColor(String type) { switch (type.toLowerCase()) { case 'error': case 'failed': return const Color(0xFFEF4444); case 'warning': case 'warn': return const Color(0xFFF59E0B); case 'success': case 'connected': case 'online': return const Color(0xFF10B981); case 'info': default: return const Color(0xFF6366F1); } } IconData _getEventIcon(String type) { switch (type.toLowerCase()) { case 'error': case 'failed': return Icons.error_outline; case 'warning': case 'warn': return Icons.warning_amber_outlined; case 'success': case 'connected': case 'online': return Icons.check_circle_outline; case 'info': default: return Icons.info_outline; } } }