@@ -39,6 +39,7 @@ type detailLoadedMsg struct {
3939 netID string
4040 ports []network.Port
4141 serverNames map [string ]string
42+ sgNames map [string ]string
4243}
4344type detailErrMsg struct {
4445 netID string
@@ -61,6 +62,7 @@ type Model struct {
6162 // Detail state for currently selected network
6263 ports []network.Port
6364 serverNames map [string ]string // DeviceID → server name
65+ sgNames map [string ]string // SG ID → name
6466 detailErr string
6567
6668 // Pane focus and cursors
@@ -230,6 +232,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
230232 m .detailErr = ""
231233 m .ports = msg .ports
232234 m .serverNames = msg .serverNames
235+ m .sgNames = msg .sgNames
233236 m .clampDetailCursors ()
234237 }
235238 return m , nil
@@ -272,6 +275,7 @@ func (m *Model) resetDetailState() {
272275 m .detailErr = ""
273276 m .ports = nil
274277 m .serverNames = nil
278+ m .sgNames = nil
275279 m .subnetCursor = 0
276280 m .subnetsScroll = 0
277281 m .portsCursor = 0
@@ -897,7 +901,15 @@ func (m Model) renderPortsContent(maxWidth, maxHeight int) string {
897901 gap = 2
898902 )
899903
900- // Calculate device name width
904+ // Calculate dynamic column widths
905+ ipsW := len ("IPs" )
906+ for _ , p := range m .ports {
907+ ipStr := m .portIPStr (p )
908+ if len (ipStr ) > ipsW {
909+ ipsW = len (ipStr )
910+ }
911+ }
912+
901913 deviceW := len ("Device" )
902914 for _ , p := range m .ports {
903915 name := m .deviceName (p )
@@ -909,7 +921,6 @@ func (m Model) renderPortsContent(maxWidth, maxHeight int) string {
909921 deviceW = 20
910922 }
911923
912- // Calculate owner width
913924 ownerW := len ("Owner" )
914925 for _ , p := range m .ports {
915926 owner := shortOwner (p .DeviceOwner )
@@ -921,15 +932,19 @@ func (m Model) renderPortsContent(maxWidth, maxHeight int) string {
921932 ownerW = 15
922933 }
923934
924- sep := strings .Repeat (" " , gap )
925- ipsW := maxWidth - 2 - statusW - macW - deviceW - ownerW - gap * 4
926- if ipsW < 10 {
927- ipsW = 10
935+ // Clamp IPs width to remaining space
936+ maxIPs := maxWidth - 2 - statusW - deviceW - ownerW - macW - gap * 4
937+ if maxIPs < 10 {
938+ maxIPs = 10
939+ }
940+ if ipsW > maxIPs {
941+ ipsW = maxIPs
928942 }
929943
944+ sep := strings .Repeat (" " , gap )
930945 headerStyle := lipgloss .NewStyle ().Foreground (shared .ColorMuted ).Bold (true )
931- header := fmt .Sprintf (" %-*s%s%-*s%s%-*s%s%-*s%s%s" ,
932- statusW , "Status" , sep , macW , "MAC " , sep , ipsW , "IPs " , sep , deviceW , "Device " , sep , "Owner " )
946+ header := fmt .Sprintf (" %-*s%s%-*s%s%-*s%s%-*s%s%-* s" ,
947+ statusW , "Status" , sep , ipsW , "IPs " , sep , deviceW , "Device " , sep , ownerW , "Owner " , sep , macW , "MAC " )
933948 headerLine := headerStyle .Render (header )
934949
935950 visibleLines := maxHeight - 1
@@ -956,23 +971,14 @@ func (m Model) renderPortsContent(maxWidth, maxHeight int) string {
956971 prefix = "\u25b8 "
957972 }
958973
959- statusColor := shared .ColorSuccess
960- if p .Status != "ACTIVE" {
961- statusColor = shared .ColorWarning
962- }
963- statusStr := lipgloss .NewStyle ().Foreground (statusColor ).Width (statusW ).Render (shared .StatusIcon (p .Status ) + p .Status )
974+ // Build status string as plain text for alignment, color the whole line
975+ statusIcon := shared .StatusIcon (p .Status )
976+ statusPlain := fmt .Sprintf ("%-*s" , statusW , statusIcon + p .Status )
964977
965- var ips []string
966- for _ , ip := range p .FixedIPs {
967- ips = append (ips , ip .IPAddress )
968- }
969- ipStr := strings .Join (ips , ", " )
978+ ipStr := m .portIPStr (p )
970979 if len (ipStr ) > ipsW {
971980 ipStr = ipStr [:ipsW - 1 ] + "\u2026 "
972981 }
973- if len (ips ) == 0 {
974- ipStr = "\u2014 "
975- }
976982
977983 device := m .deviceName (p )
978984 if len (device ) > deviceW {
@@ -984,18 +990,73 @@ func (m Model) renderPortsContent(maxWidth, maxHeight int) string {
984990 owner = owner [:ownerW - 1 ] + "\u2026 "
985991 }
986992
987- line := fmt .Sprintf ("%s%s%s%-*s%s%-*s%s%-*s%s%s" ,
988- prefix , statusStr , sep , ipsW , ipStr , sep , deviceW , device , sep , ownerW , owner , sep , p .MACAddress )
993+ mac := p .MACAddress
994+
995+ plainLine := fmt .Sprintf ("%s%-*s%s%-*s%s%-*s%s%-*s%s%-*s" ,
996+ prefix , statusW , statusPlain , sep , ipsW , ipStr , sep , deviceW , device , sep , ownerW , owner , sep , macW , mac )
989997
990998 if selected {
991- line = selectedBg .Render (line )
999+ lines = append (lines , selectedBg .Render (plainLine ))
1000+ } else {
1001+ // Color the status portion
1002+ statusColor := shared .ColorSuccess
1003+ if p .Status != "ACTIVE" {
1004+ statusColor = shared .ColorWarning
1005+ }
1006+ if ! p .AdminStateUp {
1007+ statusColor = shared .ColorError
1008+ }
1009+ coloredStatus := lipgloss .NewStyle ().Foreground (statusColor ).Render (statusPlain )
1010+ rest := fmt .Sprintf ("%s%-*s%s%-*s%s%-*s%s%-*s" ,
1011+ sep , ipsW , ipStr , sep , deviceW , device , sep , ownerW , owner , sep , macW , mac )
1012+ lines = append (lines , prefix + coloredStatus + rest )
1013+ }
1014+
1015+ // Show extra detail for the selected port
1016+ if selected {
1017+ detailStyle := lipgloss .NewStyle ().Foreground (shared .ColorMuted )
1018+ adminStr := "up"
1019+ if ! p .AdminStateUp {
1020+ adminStr = lipgloss .NewStyle ().Foreground (shared .ColorError ).Render ("down" )
1021+ }
1022+ detailLine := fmt .Sprintf (" ID: %s Admin: %s" , p .ID [:min (8 , len (p .ID ))]+ "\u2026 " , adminStr )
1023+ if p .Name != "" {
1024+ detailLine += " Name: " + p .Name
1025+ }
1026+ lines = append (lines , detailStyle .Render (detailLine ))
1027+
1028+ // Show security groups
1029+ if len (p .SecurityGroups ) > 0 {
1030+ var sgStrs []string
1031+ for _ , sgID := range p .SecurityGroups {
1032+ if name , ok := m .sgNames [sgID ]; ok {
1033+ sgStrs = append (sgStrs , name )
1034+ } else if len (sgID ) > 8 {
1035+ sgStrs = append (sgStrs , sgID [:8 ]+ "\u2026 " )
1036+ } else {
1037+ sgStrs = append (sgStrs , sgID )
1038+ }
1039+ }
1040+ sgLine := " SGs: " + strings .Join (sgStrs , ", " )
1041+ lines = append (lines , detailStyle .Render (sgLine ))
1042+ }
9921043 }
993- lines = append (lines , line )
9941044 }
9951045
9961046 return strings .Join (lines , "\n " )
9971047}
9981048
1049+ func (m Model ) portIPStr (p network.Port ) string {
1050+ if len (p .FixedIPs ) == 0 {
1051+ return "\u2014 "
1052+ }
1053+ var ips []string
1054+ for _ , ip := range p .FixedIPs {
1055+ ips = append (ips , ip .IPAddress )
1056+ }
1057+ return strings .Join (ips , ", " )
1058+ }
1059+
9991060func (m Model ) deviceName (p network.Port ) string {
10001061 if name , ok := m .serverNames [p .DeviceID ]; ok && name != "" {
10011062 return name
@@ -1188,8 +1249,27 @@ func (m Model) fetchDetail(netID string) tea.Cmd {
11881249 return fetchedPorts [i ].MACAddress < fetchedPorts [j ].MACAddress
11891250 })
11901251
1252+ // Resolve security group names
1253+ sgIDs := make (map [string ]bool )
1254+ for _ , p := range fetchedPorts {
1255+ for _ , sgID := range p .SecurityGroups {
1256+ sgIDs [sgID ] = true
1257+ }
1258+ }
1259+ sgNameMap := make (map [string ]string )
1260+ if len (sgIDs ) > 0 {
1261+ sgs , err := network .ListSecurityGroups (context .Background (), networkClient )
1262+ if err == nil {
1263+ for _ , sg := range sgs {
1264+ if sgIDs [sg .ID ] {
1265+ sgNameMap [sg .ID ] = sg .Name
1266+ }
1267+ }
1268+ }
1269+ }
1270+
11911271 if computeClient == nil {
1192- return detailLoadedMsg {netID : netID , ports : fetchedPorts }
1272+ return detailLoadedMsg {netID : netID , ports : fetchedPorts , sgNames : sgNameMap }
11931273 }
11941274
11951275 // Collect device IDs that look like compute instances
@@ -1215,15 +1295,14 @@ func (m Model) fetchDetail(netID string) tea.Cmd {
12151295 // Also resolve router device IDs
12161296 for _ , p := range fetchedPorts {
12171297 if strings .HasPrefix (p .DeviceOwner , "network:router_interface" ) && p .DeviceID != "" {
1218- // DeviceID is the router ID — try to get the router name
12191298 router , err := network .GetRouter (context .Background (), networkClient , p .DeviceID )
12201299 if err == nil && router != nil {
12211300 srvNames [p .DeviceID ] = router .Name
12221301 }
12231302 }
12241303 }
12251304
1226- return detailLoadedMsg {netID : netID , ports : fetchedPorts , serverNames : srvNames }
1305+ return detailLoadedMsg {netID : netID , ports : fetchedPorts , serverNames : srvNames , sgNames : sgNameMap }
12271306 }
12281307}
12291308
0 commit comments