import 'package:flutter/material.dart'; import '../control_plane_status_client.dart'; Widget _buildProviderCard(ProviderSnapshotView p) { final displayName = p.id.isNotEmpty ? p.id : p.adapter; final displayState = p.health.isNotEmpty ? p.health : p.status; return Padding( padding: const EdgeInsets.only(bottom: 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 6, height: 6, decoration: BoxDecoration( color: _stateColor(p), shape: BoxShape.circle, ), ), const SizedBox(width: 4), Flexible( child: Text( displayName, style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 1), Text( '${p.type} / ${p.category}', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10), ), ], ), ), const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: _stateColor(p).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(3), ), child: Text( displayState.toUpperCase(), style: TextStyle( color: _stateColor(p), fontSize: 8, fontWeight: FontWeight.bold, ), ), ), ], ), if (p.servedModels.isNotEmpty) ...[ const SizedBox(height: 2), Text( 'Models: ${_truncateModels(p.servedModels)}', style: const TextStyle(color: Color(0xFFCBD5E1), fontSize: 10), overflow: TextOverflow.ellipsis, ), ], const SizedBox(height: 2), Wrap( spacing: 8, children: [ Text( 'Load: ${p.inFlight}/${p.capacity}', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10), ), Text( 'Q: ${p.queued}', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10), ), Text( '${(p.loadRatio * 100).toStringAsFixed(1)}%', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10), ), ], ), if (p.lifecycleCapabilities.isNotEmpty) ...[ const SizedBox(height: 2), Wrap( spacing: 3, runSpacing: 2, children: p.lifecycleCapabilities.take(3).map((c) { return Container( padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 1), decoration: BoxDecoration( color: const Color(0xFF6366F1).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2), ), child: Text( c, style: const TextStyle(color: Color(0xFFA5B4FC), fontSize: 8), ), ); }).toList(), ), ], ], ), ); } Color _stateColor(ProviderSnapshotView p) { final health = p.health.toLowerCase(); final status = p.status.toLowerCase(); // Health가 있으면 health가 status를 우선한다. if (health.isNotEmpty) { if (health == 'healthy' || health == 'ok' || health == 'available') { return const Color(0xFF10B981); } if (health == 'unhealthy' || health == 'error' || health == 'failed' || health == 'unavailable') { return const Color(0xFFEF4444); } if (health == 'degraded' || health == 'draining') { return const Color(0xFFF59E0B); } if (health == 'unknown') { return const Color(0xFF64748B); } } // Health가 없으면 status를 기준으로 한다. if (status == 'active' || status == 'ready' || status == 'available') { return const Color(0xFF10B981); } if (status == 'error' || status == 'failed' || status == 'unavailable') { return const Color(0xFFEF4444); } if (status == 'draining') { return const Color(0xFFF59E0B); } if (status == 'unknown') { return const Color(0xFF64748B); } return const Color(0xFF64748B); } String _truncateModels(List models) { if (models.isEmpty) return ''; if (models.length <= 3) return models.join(', '); return '${models.take(3).join(', ')}...'; } 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), 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), SizedBox(height: 16), Text( 'No Nodes Registered', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), 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: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( node.alias.isNotEmpty ? node.alias : node.nodeId, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( 'ID: ${node.nodeId}', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(width: 8), 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(), ), ], if (node.providerSnapshots.isNotEmpty) ...[ const SizedBox(height: 16), const Divider(color: Color(0xFF334155)), const SizedBox(height: 12), const Text( 'Provider Catalog', style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), ...node.providerSnapshots.map((p) => _buildProviderCard(p)), ], ], ), ), ); }, ); } }