From bfe541565b1dd039127d0e44038d66f5a55b7515 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Wed, 6 May 2026 12:41:36 +0100 Subject: [PATCH] feat(ui): toggle httpLog per-forward in add/edit wizard Adds an HTTP-log enable toggle to the wizard's confirmation step so users can flip httpLog on a forward without editing YAML by hand. Behaviour: - 'h' on the confirmation step toggles HTTPLog when not focused on the alias text input. When focus is on alias, 'h' is treated as text so users can still type aliases like 'host' or 'http-proxy'. - The confirmation summary shows '[x] enabled' or '[ ] disabled'. - New forwards: toggle on -> &HTTPLogSpec{Enabled: true}; off -> nil. - Edit mode: pre-populates the toggle from the existing forward and preserves any advanced HTTPLog fields the user had configured in YAML (logFile, includeHeaders, maxBodySize, filterPath) by copying the original spec on save. Toggling off discards the advanced fields (consistent with 'absent in YAML = disabled'). State changes: - ForwardStatus gains *config.HTTPLogSpec so the wizard can see the full original spec on edit. - AddWizardState gains httpLog bool + httpLogOriginal *HTTPLogSpec. Three new tests: - TestHandleAddWizardKeys_HToggleHTTPLog - TestHandleAddWizardKeys_HOnAliasFocusIsTextInput - TestEditPrefill_PreservesHTTPLog --- internal/ui/bubbletea_ui.go | 1 + internal/ui/handlers_test.go | 80 ++++++++++++++++++++++++++++++++++ internal/ui/table.go | 1 + internal/ui/wizard_handlers.go | 39 +++++++++++++++++ internal/ui/wizard_state.go | 13 +++--- internal/ui/wizard_views.go | 8 +++- 6 files changed, 136 insertions(+), 6 deletions(-) diff --git a/internal/ui/bubbletea_ui.go b/internal/ui/bubbletea_ui.go index b4624ca..71014d8 100644 --- a/internal/ui/bubbletea_ui.go +++ b/internal/ui/bubbletea_ui.go @@ -200,6 +200,7 @@ func (ui *BubbleTeaUI) AddForward(id string, fwd *config.Forward) { Alias: alias, Type: resourceType, Resource: resourceName, + HTTPLog: fwd.HTTPLog, RemotePort: fwd.Port, LocalPort: fwd.LocalPort, Status: "Starting", diff --git a/internal/ui/handlers_test.go b/internal/ui/handlers_test.go index f2f03da..60d1732 100644 --- a/internal/ui/handlers_test.go +++ b/internal/ui/handlers_test.go @@ -997,3 +997,83 @@ func TestHandleRemoveWizardKeys_EnterOnYesStillConfirms(t *testing.T) { assert.NotNil(t, cmd, "Enter on Yes must still dispatch removeForwardsCmd") } + +// TestHandleAddWizardKeys_HToggleHTTPLog verifies that pressing 'h' on the +// confirmation step (when not focused on the alias text input) flips the +// httpLog flag on the wizard state. +func TestHandleAddWizardKeys_HToggleHTTPLog(t *testing.T) { + ui := NewBubbleTeaUI(nil, "1.0.0") + ui.SetWizardDependencies(nil, &config.Mutator{}, "/path/to/config") + + ui.mu.Lock() + ui.viewMode = ViewModeAddWizard + ui.addWizard = newAddWizardState() + ui.addWizard.step = StepConfirmation + ui.addWizard.confirmationFocus = FocusButtons + ui.addWizard.inputMode = InputModeList + ui.mu.Unlock() + + m := model{ui: ui, termWidth: 120, termHeight: 40} + + require.False(t, m.ui.addWizard.httpLog, "httpLog should default to false") + + keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("h")} + m.handleAddWizardKeys(keyMsg) + assert.True(t, m.ui.addWizard.httpLog, "first 'h' should enable httpLog") + + m.handleAddWizardKeys(keyMsg) + assert.False(t, m.ui.addWizard.httpLog, "second 'h' should disable httpLog") +} + +// TestHandleAddWizardKeys_HOnAliasFocusIsTextInput verifies that 'h' is +// treated as a regular character when the alias text input has focus, so the +// user can still type aliases like "host" or "http-proxy". +func TestHandleAddWizardKeys_HOnAliasFocusIsTextInput(t *testing.T) { + ui := NewBubbleTeaUI(nil, "1.0.0") + ui.SetWizardDependencies(nil, &config.Mutator{}, "/path/to/config") + + ui.mu.Lock() + ui.viewMode = ViewModeAddWizard + ui.addWizard = newAddWizardState() + ui.addWizard.step = StepConfirmation + ui.addWizard.confirmationFocus = FocusAlias + ui.addWizard.inputMode = InputModeList + ui.mu.Unlock() + + m := model{ui: ui, termWidth: 120, termHeight: 40} + + keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("h")} + m.handleAddWizardKeys(keyMsg) + + assert.False(t, m.ui.addWizard.httpLog, "httpLog must NOT toggle when alias has focus") + assert.Contains(t, m.ui.addWizard.textInput, "h", "'h' should land in alias text input") +} + +// TestEditPrefill_PreservesHTTPLog verifies that opening the wizard in edit +// mode for a forward whose ForwardStatus has HTTPLog set initialises the +// wizard's httpLog flag and httpLogOriginal pointer correctly. +func TestEditPrefill_PreservesHTTPLog(t *testing.T) { + ui := NewBubbleTeaUI(nil, "1.0.0") + disco := &k8s.Discovery{} + ui.SetWizardDependencies(disco, &config.Mutator{}, "/path/to/config") + + fwd := &config.Forward{ + Resource: "pod/api", + Port: 8080, + LocalPort: 8080, + HTTPLog: &config.HTTPLogSpec{Enabled: true, IncludeHeaders: true, MaxBodySize: 4096}, + } + ui.AddForward("api", fwd) + + m := model{ui: ui, termWidth: 120, termHeight: 40} + + keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("e")} + m.handleMainViewKeys(keyMsg) + + require.NotNil(t, m.ui.addWizard, "wizard should be active after 'e'") + assert.True(t, m.ui.addWizard.isEditing) + assert.True(t, m.ui.addWizard.httpLog, "httpLog flag should reflect existing forward") + require.NotNil(t, m.ui.addWizard.httpLogOriginal, "original spec should be retained for advanced fields") + assert.True(t, m.ui.addWizard.httpLogOriginal.IncludeHeaders) + assert.Equal(t, 4096, m.ui.addWizard.httpLogOriginal.MaxBodySize) +} diff --git a/internal/ui/table.go b/internal/ui/table.go index 7ba730e..4e59e48 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -11,6 +11,7 @@ import ( // ForwardStatus represents the current status of a port forward type ForwardStatus struct { + HTTPLog *config.HTTPLogSpec Context string Namespace string Alias string diff --git a/internal/ui/wizard_handlers.go b/internal/ui/wizard_handlers.go index ddcd6b4..261f877 100644 --- a/internal/ui/wizard_handlers.go +++ b/internal/ui/wizard_handlers.go @@ -119,6 +119,8 @@ func (m model) handleMainViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.ui.addWizard.remotePort = selectedForward.RemotePort m.ui.addWizard.localPort = selectedForward.LocalPort m.ui.addWizard.alias = selectedForward.Alias + m.ui.addWizard.httpLogOriginal = selectedForward.HTTPLog + m.ui.addWizard.httpLog = selectedForward.HTTPLog != nil && selectedForward.HTTPLog.Enabled // Determine resource type from the resource string if strings.HasPrefix(selectedForward.Type, "service") { @@ -429,6 +431,29 @@ func (m model) handleAddWizardKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } } + case "h": + // In confirmation step (when not typing into the alias field), 'h' + // toggles whether this forward has HTTP traffic logging enabled. + // When the alias field is focused, fall through to text input below. + if wizard.step == StepConfirmation && wizard.confirmationFocus != FocusAlias { + wizard.httpLog = !wizard.httpLog + return m, nil + } + // Otherwise treat as text input (filter or alias). + canTypeText := wizard.inputMode == InputModeText || + (wizard.step == StepConfirmation && wizard.confirmationFocus == FocusAlias) || + (wizard.inputMode == InputModeList && isFilterableStep(wizard.step)) + if canTypeText { + if wizard.inputMode == InputModeList && isFilterableStep(wizard.step) { + wizard.searchFilter += "h" + wizard.cursor = 0 + wizard.scrollOffset = 0 + } else { + wizard.handleTextInput('h') + } + } + return m, nil + case "enter": return m.handleAddWizardEnter() @@ -671,6 +696,20 @@ func (m model) handleAddWizardEnter() (tea.Model, tea.Cmd) { fwd.Resource = "service/" + wizard.resourceValue } + // HTTPLog: when toggled on, preserve any advanced fields the + // user had configured in YAML (logFile, includeHeaders, etc.) + // so the wizard does not silently strip them. When toggled + // off, leave HTTPLog nil (= absent in YAML = disabled). + if wizard.httpLog { + if wizard.httpLogOriginal != nil { + spec := *wizard.httpLogOriginal + spec.Enabled = true + fwd.HTTPLog = &spec + } else { + fwd.HTTPLog = &config.HTTPLogSpec{Enabled: true} + } + } + wizard.loading = true // If editing, use atomic update operation diff --git a/internal/ui/wizard_state.go b/internal/ui/wizard_state.go index c9997ba..a323abb 100644 --- a/internal/ui/wizard_state.go +++ b/internal/ui/wizard_state.go @@ -3,6 +3,7 @@ package ui import ( "strings" + "github.com/lukaszraczylo/kportal/internal/config" "github.com/lukaszraczylo/kportal/internal/k8s" ) @@ -110,6 +111,7 @@ func (r ResourceType) Description() string { // AddWizardState maintains the state for the add port forward wizard type AddWizardState struct { error error + httpLogOriginal *config.HTTPLogSpec resourceValue string originalID string portCheckMsg string @@ -119,16 +121,16 @@ type AddWizardState struct { selector string selectedContext string selectedNamespace string - pods []k8s.PodInfo - contexts []string + services []k8s.ServiceInfo detectedPorts []k8s.PortInfo matchingPods []k8s.PodInfo - services []k8s.ServiceInfo + contexts []string namespaces []string - scrollOffset int + pods []k8s.PodInfo + localPort int selectedResourceType ResourceType step AddWizardStep - localPort int + scrollOffset int cursor int remotePort int inputMode InputMode @@ -136,6 +138,7 @@ type AddWizardState struct { portAvailable bool isEditing bool loading bool + httpLog bool } // newAddWizardState creates a new add wizard state initialized to the first step diff --git a/internal/ui/wizard_views.go b/internal/ui/wizard_views.go index ea9229d..2010bc7 100644 --- a/internal/ui/wizard_views.go +++ b/internal/ui/wizard_views.go @@ -510,6 +510,12 @@ func (m model) renderConfirmation() string { b.WriteString(fmt.Sprintf(" Local Port: %d\n", wizard.localPort)) b.WriteString(" Protocol: tcp\n") + httpLogMark := "[ ] disabled" + if wizard.httpLog { + httpLogMark = "[x] enabled" + } + b.WriteString(fmt.Sprintf(" HTTP Log: %s\n", httpLogMark)) + b.WriteString("\n") // Show alias field with focus indicator @@ -538,7 +544,7 @@ func (m model) renderConfirmation() string { } b.WriteString("\n") - b.WriteString(wrapHelpText("↑/↓/Tab: Navigate Enter: Confirm Esc: Back", wizardHelpWidth(m.termWidth))) + b.WriteString(wrapHelpText("↑/↓/Tab: Navigate h: Toggle HTTP Log Enter: Confirm Esc: Back", wizardHelpWidth(m.termWidth))) return b.String() }