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, ); Widget buildEdgeList(bool isMobile) { return Container( decoration: BoxDecoration( border: isMobile ? const Border(bottom: BorderSide(color: Color(0xFF334155))) : const 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), ], ), ), ); }, ), ); } Widget buildEdgeDetails(bool isMobile) { return Container( padding: EdgeInsets.all(isMobile ? 16 : 24), child: ListView( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( selectedEdge.edgeName.isNotEmpty ? selectedEdge.edgeName : selectedEdge.edgeId, style: TextStyle(fontSize: isMobile ? 18 : 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()), _buildDetailRow('Fleet Health', selectedEdge.health), _buildDetailRow('Node Count', selectedEdge.nodeCount.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.kind.isNotEmpty ? cap.kind : cap.toString(), style: const TextStyle(color: Color(0xFF818CF8), fontSize: 12, fontWeight: FontWeight.w500), ), ); }).toList(), ), ], ), ); } return LayoutBuilder( builder: (context, constraints) { final isMobile = constraints.maxWidth < 600; if (isMobile) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( flex: 2, child: buildEdgeList(true), ), Expanded( flex: 3, child: buildEdgeDetails(true), ), ], ); } else { return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( flex: 2, child: buildEdgeList(false), ), Expanded( flex: 3, child: buildEdgeDetails(false), ), ], ); } }, ); } 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), ], ), ); } }