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() }