@@ -652,6 +652,7 @@ type Model struct {
652652 selectedLBL7Policy map [string ]interface {} // Currently selected policy for detail view
653653 lbL7PolicyDetailActionIdx int // Selected action in policy detail (0=Edit, 1=Delete)
654654 lbL7PolicyDetailConfirm bool // Confirm mode in policy detail
655+ lbL7RuleDetailIdx int // Currently displayed rule index in L7 Rules view
655656 // Background detail-view refresh (set by auto-refresh timer, cleared by data handlers)
656657 detailRefreshId string
657658 detailRefreshName string
@@ -2184,6 +2185,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
21842185 case lbL7RuleCreatedMsg :
21852186 m .wizard = WizardData {}
21862187 m .mode = LBL7RulesView
2188+ m .lbL7RuleDetailIdx = 0
21872189 if msg .err != nil {
21882190 m .notification = fmt .Sprintf ("❌ Erreur: %s" , msg .err .Error ())
21892191 m .notificationExpiry = time .Now ().Add (8 * time .Second )
@@ -3321,6 +3323,17 @@ func (m Model) renderContentBox(width int) string {
33213323 // Combine title and content
33223324 fullContent := title + "\n \n " + contentStr
33233325
3326+ // Clamp content height so the nav bars and footer are never pushed off screen.
3327+ // ~12 lines are consumed by header, nav bars, spacing and footer outside this box.
3328+ if m .height > 14 {
3329+ maxLines := m .height - 12
3330+ lines := strings .Split (fullContent , "\n " )
3331+ if len (lines ) > maxLines {
3332+ lines = lines [:maxLines ]
3333+ fullContent = strings .Join (lines , "\n " )
3334+ }
3335+ }
3336+
33243337 // Level 3 (inTableFocus): highlight content box border in green to show focus is inside the table
33253338 boxStyle := contentBoxStyle
33263339 if m .inTableFocus {
@@ -7179,104 +7192,64 @@ func (m Model) renderLBL7RulesView(width int) string {
71797192 actionsBox := renderBox ("Actions (Enter to execute)" , createBtn , width - 4 )
71807193 content .WriteString (actionsBox + "\n \n " )
71817194
7182- var rulesContent strings.Builder
7183- if len (rules ) == 0 {
7184- rulesContent .WriteString (lipgloss .NewStyle ().Foreground (lipgloss .Color ("#666666" )).
7185- Render (" No L7 Rules defined for this policy." ))
7186- } else {
7187- // Table header
7188- hType := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#888888" )).Width (18 ).Render ("Type" )
7189- hComp := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#888888" )).Width (16 ).Render ("Comparison" )
7190- hKey := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#888888" )).Width (16 ).Render ("Key" )
7191- hVal := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#888888" )).Width (22 ).Render ("Value" )
7192- hInv := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#888888" )).Width (8 ).Render ("Invert" )
7193- hSt := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#888888" )).Width (14 ).Render ("Status" )
7194- rulesContent .WriteString (" " + hType + hComp + hKey + hVal + hInv + hSt + "\n " )
7195- rulesContent .WriteString (" " + strings .Repeat ("─" , min (width - 10 , 92 )) + "\n " )
7196-
7197- for _ , r := range rules {
7198- rType := truncate (getStringValue (r , "type" , "-" ), 16 )
7199- rComp := truncate (getStringValue (r , "compareType" , "-" ), 14 )
7200- rKey := truncate (getStringValue (r , "key" , "-" ), 14 )
7201- rVal := truncate (getStringValue (r , "value" , "-" ), 20 )
7202- invertStr := "No"
7203- if inv , ok := r ["invert" ].(bool ); ok && inv {
7204- invertStr = "Yes"
7205- }
7206- rStatus := getStringValue (r , "provisioningStatus" , getStringValue (r , "operatingStatus" , "-" ))
7195+ // Summary line: rule count + pagination hint
7196+ titleLine := fmt .Sprintf ("L7 Rules (%d) — Policy: %s" , len (rules ), policyName )
72077197
7208- statusColor := lipgloss .Color ("#00FF7F" )
7209- if strings .ToLower (rStatus ) != "active" && strings .ToLower (rStatus ) != "online" {
7210- if strings .ToLower (rStatus ) == "error" {
7211- statusColor = lipgloss .Color ("#FF6B6B" )
7212- } else {
7213- statusColor = lipgloss .Color ("#FFD700" )
7214- }
7215- }
7216- invertColor := lipgloss .Color ("#CCCCCC" )
7217- if invertStr == "Yes" {
7218- invertColor = lipgloss .Color ("#FFD700" )
7219- }
7198+ if len (rules ) == 0 {
7199+ empty := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#666666" )).Render (" No L7 Rules defined for this policy." )
7200+ content .WriteString (renderBox (titleLine , empty , width - 4 ))
7201+ return content .String ()
7202+ }
72207203
7221- rulesContent .WriteString (
7222- " " +
7223- lipgloss .NewStyle ().Foreground (lipgloss .Color ("#FFFFFF" )).Width (18 ).Render (rType ) +
7224- lipgloss .NewStyle ().Foreground (lipgloss .Color ("#CCCCCC" )).Width (16 ).Render (rComp ) +
7225- lipgloss .NewStyle ().Foreground (lipgloss .Color ("#CCCCCC" )).Width (16 ).Render (rKey ) +
7226- lipgloss .NewStyle ().Foreground (lipgloss .Color ("#CCCCCC" )).Width (22 ).Render (rVal ) +
7227- lipgloss .NewStyle ().Foreground (invertColor ).Width (8 ).Render (invertStr ) +
7228- lipgloss .NewStyle ().Foreground (statusColor ).Width (14 ).Render (rStatus ) + "\n " ,
7229- )
7230- }
7204+ // Clamp index
7205+ idx := m .lbL7RuleDetailIdx
7206+ if idx < 0 {
7207+ idx = 0
7208+ }
7209+ if idx >= len (rules ) {
7210+ idx = len (rules ) - 1
72317211 }
72327212
7233- rulesBox := renderBox ( fmt . Sprintf ( "L7 Rules (%d) — Policy: %s" , len ( rules ), policyName ), rulesContent . String (), width - 4 )
7234- content . WriteString ( rulesBox )
7213+ // Pagination indicator in title
7214+ pagTitle := fmt . Sprintf ( "%s [◄ %d/%d ►]" , titleLine , idx + 1 , len ( rules ) )
72357215
7236- // If rules exist, show detail of first rule below (expanded view per rule)
7237- if len (rules ) > 0 {
7238- content .WriteString ("\n " )
7239- for idx , r := range rules {
7240- rType := getStringValue (r , "type" , "N/A" )
7241- rComp := getStringValue (r , "compareType" , "N/A" )
7242- rKey := getStringValue (r , "key" , "-" )
7243- rVal := getStringValue (r , "value" , "-" )
7244- invertStr := "No"
7245- if inv , ok := r ["invert" ].(bool ); ok && inv {
7246- invertStr = "Yes"
7247- }
7248- rStatus := getStringValue (r , "provisioningStatus" , getStringValue (r , "operatingStatus" , "N/A" ))
7249- rID := getStringValue (r , "id" , "N/A" )
7250-
7251- var detailContent strings.Builder
7252- detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("ID" ), valueSt .Render (truncate (rID , 36 ))))
7253- detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("Type" ), valueSt .Render (rType )))
7254- detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("Comparison" ), valueSt .Render (rComp )))
7255- if rKey != "-" {
7256- detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("Key" ), valueSt .Render (rKey )))
7257- }
7258- detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("Value" ), valueSt .Render (rVal )))
7259- invertStyle := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#CCCCCC" ))
7260- if invertStr == "Yes" {
7261- invertStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("#FFD700" ))
7262- }
7263- detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("Invert" ), invertStyle .Render (invertStr )))
7216+ r := rules [idx ]
7217+ rType := getStringValue (r , "ruleType" , getStringValue (r , "type" , "N/A" ))
7218+ rComp := getStringValue (r , "compareType" , "N/A" )
7219+ rKey := getStringValue (r , "key" , "-" )
7220+ rVal := getStringValue (r , "value" , "-" )
7221+ rID := getStringValue (r , "id" , "N/A" )
7222+ invertStr := "No"
7223+ if inv , ok := r ["invert" ].(bool ); ok && inv {
7224+ invertStr = "Yes"
7225+ }
7226+ rStatus := getStringValue (r , "provisioningStatus" , getStringValue (r , "operatingStatus" , "N/A" ))
72647227
7265- statusColor := lipgloss .Color ("#00FF7F" )
7266- if strings .ToLower (rStatus ) != "active" && strings .ToLower (rStatus ) != "online" {
7267- statusColor = lipgloss .Color ("#FFD700" )
7268- if strings .ToLower (rStatus ) == "error" {
7269- statusColor = lipgloss .Color ("#FF6B6B" )
7270- }
7271- }
7272- detailContent .WriteString (fmt .Sprintf ("%s %s" , labelSt .Render ("Supply Status" ),
7273- lipgloss .NewStyle ().Foreground (statusColor ).Render (rStatus )))
7228+ var detailContent strings.Builder
7229+ detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("ID" ), valueSt .Render (truncate (rID , 36 ))))
7230+ detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("Type" ), valueSt .Render (rType )))
7231+ detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("Comparison" ), valueSt .Render (rComp )))
7232+ if rKey != "-" {
7233+ detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("Key" ), valueSt .Render (rKey )))
7234+ }
7235+ detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("Value" ), valueSt .Render (rVal )))
7236+ invertStyle := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#CCCCCC" ))
7237+ if invertStr == "Yes" {
7238+ invertStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("#FFD700" ))
7239+ }
7240+ detailContent .WriteString (fmt .Sprintf ("%s %s\n " , labelSt .Render ("Invert" ), invertStyle .Render (invertStr )))
72747241
7275- box := renderBox (fmt .Sprintf ("Rule #%d" , idx + 1 ), detailContent .String (), (width - 4 )/ 2 )
7276- content .WriteString (box + "\n " )
7242+ statusColor := lipgloss .Color ("#00FF7F" )
7243+ if strings .ToLower (rStatus ) != "active" && strings .ToLower (rStatus ) != "online" {
7244+ statusColor = lipgloss .Color ("#FFD700" )
7245+ if strings .ToLower (rStatus ) == "error" {
7246+ statusColor = lipgloss .Color ("#FF6B6B" )
72777247 }
72787248 }
7249+ detailContent .WriteString (fmt .Sprintf ("%s %s" , labelSt .Render ("Prov. Status" ),
7250+ lipgloss .NewStyle ().Foreground (statusColor ).Render (rStatus )))
72797251
7252+ content .WriteString (renderBox (pagTitle , detailContent .String (), width - 4 ))
72807253 return content .String ()
72817254}
72827255
@@ -7855,6 +7828,8 @@ func (m Model) renderFooter() string {
78557828 help = "Type key name • Enter: Confirm • Esc: Cancel"
78567829 } else if m .wizard .step == LBL7RuleWizardStepValue {
78577830 help = "Type value • Enter: Confirm • Esc: Cancel"
7831+ } else if m .mode == LBL7RulesView {
7832+ help = "←→: Navigate rules • Enter: Create Rule • Esc: Back"
78587833 } else {
78597834 help = "↑↓: Navigate • d: Debug • Enter: Select • ←: Back • Esc: Cancel"
78607835 }
@@ -8027,6 +8002,16 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
80278002 }
80288003 return m , nil
80298004 }
8005+ // In LBL7RulesView, paginate backwards through rules
8006+ if m .mode == LBL7RulesView {
8007+ policyID := getStringValue (m .selectedLBL7Policy , "id" , "" )
8008+ if m .lbL7RuleDetailIdx > 0 {
8009+ m .lbL7RuleDetailIdx --
8010+ } else {
8011+ m .lbL7RuleDetailIdx = len (m .lbL7Rules [policyID ]) - 1
8012+ }
8013+ return m , nil
8014+ }
80308015 // In LBListenerDetailView, navigate actions (only when not navigating policies)
80318016 if m .mode == LBListenerDetailView && m .lbL7PolicyListIdx < 0 {
80328017 if m .lbListenerDetailActionIdx > 0 {
@@ -8164,6 +8149,15 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
81648149 }
81658150 return m , nil
81668151 }
8152+ // In LBL7RulesView, paginate forward through rules
8153+ if m .mode == LBL7RulesView {
8154+ policyID := getStringValue (m .selectedLBL7Policy , "id" , "" )
8155+ count := len (m .lbL7Rules [policyID ])
8156+ if count > 0 {
8157+ m .lbL7RuleDetailIdx = (m .lbL7RuleDetailIdx + 1 ) % count
8158+ }
8159+ return m , nil
8160+ }
81678161 // In LBListenerDetailView, navigate actions (0=Edit, 1=Delete, 2=L7 Policies) — only when not navigating policies
81688162 if m .mode == LBListenerDetailView && m .lbL7PolicyListIdx < 0 {
81698163 if m .lbListenerDetailActionIdx < 2 {
0 commit comments