mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-07 06:25:43 +00:00
bugfixes nov2025 pt3 (#6)
* Minor improvements. * DRY the codebase. * Add version checker / updater.
This commit is contained in:
@@ -46,6 +46,11 @@ type BubbleTeaUI struct {
|
||||
version string
|
||||
errors map[string]string // Track error messages by forward ID
|
||||
|
||||
// Update notification
|
||||
updateAvailable bool
|
||||
updateVersion string
|
||||
updateURL string
|
||||
|
||||
// Modal wizard state
|
||||
viewMode ViewMode
|
||||
addWizard *AddWizardState
|
||||
@@ -96,6 +101,16 @@ func (ui *BubbleTeaUI) SetWizardDependencies(discovery *k8s.Discovery, mutator *
|
||||
ui.configPath = configPath
|
||||
}
|
||||
|
||||
// SetUpdateAvailable sets the update notification to be displayed
|
||||
func (ui *BubbleTeaUI) SetUpdateAvailable(version, url string) {
|
||||
ui.mu.Lock()
|
||||
defer ui.mu.Unlock()
|
||||
|
||||
ui.updateAvailable = true
|
||||
ui.updateVersion = version
|
||||
ui.updateURL = url
|
||||
}
|
||||
|
||||
// Start starts the bubbletea application
|
||||
func (ui *BubbleTeaUI) Start() error {
|
||||
m := model{ui: ui}
|
||||
@@ -169,8 +184,9 @@ func (ui *BubbleTeaUI) UpdateStatus(id string, status string) {
|
||||
if fwd, ok := ui.forwards[id]; ok {
|
||||
fwd.Status = status
|
||||
}
|
||||
// Clear error if status is not Error
|
||||
if status != "Error" {
|
||||
// Only clear error when forward becomes Active again
|
||||
// This keeps error visible during Reconnecting/Starting states
|
||||
if status == "Active" {
|
||||
delete(ui.errors, id)
|
||||
}
|
||||
ui.mu.Unlock()
|
||||
@@ -266,7 +282,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.ui.addWizard = nil
|
||||
m.ui.removeWizard = nil
|
||||
m.ui.mu.Unlock()
|
||||
return m, nil
|
||||
return m, tea.ClearScreen
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -356,6 +372,15 @@ func (m model) renderMainView() string {
|
||||
// Title with version
|
||||
title := fmt.Sprintf("kportal v%s - Port Forwarding Status", m.ui.version)
|
||||
b.WriteString(titleStyle.Render(title))
|
||||
|
||||
// Show update notification if available
|
||||
if m.ui.updateAvailable {
|
||||
updateStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("42")). // Green
|
||||
Bold(true)
|
||||
updateMsg := fmt.Sprintf(" Update available: v%s", m.ui.updateVersion)
|
||||
b.WriteString(updateStyle.Render(updateMsg))
|
||||
}
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Header
|
||||
@@ -574,6 +599,15 @@ func (ui *BubbleTeaUI) moveSelection(delta int) {
|
||||
}
|
||||
}
|
||||
|
||||
// resetDeleteConfirmation resets the delete confirmation dialog state.
|
||||
// Caller must hold ui.mu lock.
|
||||
func (ui *BubbleTeaUI) resetDeleteConfirmation() {
|
||||
ui.deleteConfirming = false
|
||||
ui.deleteConfirmID = ""
|
||||
ui.deleteConfirmAlias = ""
|
||||
ui.deleteConfirmCursor = 0
|
||||
}
|
||||
|
||||
// renderDeleteConfirmation renders the delete confirmation dialog
|
||||
func (m model) renderDeleteConfirmation() string {
|
||||
m.ui.mu.RLock()
|
||||
|
||||
@@ -173,12 +173,8 @@ func (m model) handleDeleteConfirmation(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
// Cancel deletion
|
||||
m.ui.deleteConfirming = false
|
||||
m.ui.deleteConfirmID = ""
|
||||
m.ui.deleteConfirmAlias = ""
|
||||
m.ui.deleteConfirmCursor = 0 // Reset cursor
|
||||
m.ui.resetDeleteConfirmation()
|
||||
m.ui.mu.Unlock()
|
||||
// Force a repaint by returning the model
|
||||
return m, tea.ClearScreen
|
||||
|
||||
case "left", "h", "right", "l":
|
||||
@@ -191,26 +187,18 @@ func (m model) handleDeleteConfirmation(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
// Confirm deletion (either Enter on Yes or pressing 'y')
|
||||
if m.ui.deleteConfirmCursor == 0 || msg.String() == "y" {
|
||||
id := m.ui.deleteConfirmID
|
||||
m.ui.deleteConfirming = false
|
||||
m.ui.deleteConfirmID = ""
|
||||
m.ui.deleteConfirmAlias = ""
|
||||
m.ui.resetDeleteConfirmation()
|
||||
m.ui.mu.Unlock()
|
||||
return m, removeForwardByIDCmd(m.ui.mutator, id)
|
||||
}
|
||||
// Enter on No = cancel
|
||||
m.ui.deleteConfirming = false
|
||||
m.ui.deleteConfirmID = ""
|
||||
m.ui.deleteConfirmAlias = ""
|
||||
m.ui.deleteConfirmCursor = 0 // Reset cursor
|
||||
m.ui.resetDeleteConfirmation()
|
||||
m.ui.mu.Unlock()
|
||||
return m, tea.ClearScreen
|
||||
|
||||
case "n":
|
||||
// Quick 'n' for no
|
||||
m.ui.deleteConfirming = false
|
||||
m.ui.deleteConfirmID = ""
|
||||
m.ui.deleteConfirmAlias = ""
|
||||
m.ui.deleteConfirmCursor = 0 // Reset cursor
|
||||
m.ui.resetDeleteConfirmation()
|
||||
m.ui.mu.Unlock()
|
||||
return m, tea.ClearScreen
|
||||
}
|
||||
@@ -259,10 +247,7 @@ func (m model) handleAddWizardKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
} else {
|
||||
// Go back one step
|
||||
wizard.step--
|
||||
wizard.cursor = 0
|
||||
wizard.clearTextInput()
|
||||
wizard.clearSearchFilter()
|
||||
wizard.error = nil
|
||||
wizard.resetInput()
|
||||
|
||||
// Reset input mode based on the step we're going back to
|
||||
switch wizard.step {
|
||||
@@ -492,7 +477,7 @@ func (m model) handleAddWizardEnter() (tea.Model, tea.Cmd) {
|
||||
} else {
|
||||
// Text mode - manual entry
|
||||
port, err := strconv.Atoi(wizard.textInput)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
if err != nil || !config.IsValidPort(port) {
|
||||
wizard.error = fmt.Errorf("invalid port number")
|
||||
} else {
|
||||
wizard.remotePort = port
|
||||
@@ -504,7 +489,7 @@ func (m model) handleAddWizardEnter() (tea.Model, tea.Cmd) {
|
||||
|
||||
case StepEnterLocalPort:
|
||||
port, err := strconv.Atoi(wizard.textInput)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
if err != nil || !config.IsValidPort(port) {
|
||||
wizard.error = fmt.Errorf("invalid port number")
|
||||
} else {
|
||||
// Check port availability before proceeding
|
||||
@@ -559,9 +544,10 @@ func (m model) handleAddWizardEnter() (tea.Model, tea.Cmd) {
|
||||
|
||||
return m, saveForwardCmd(m.ui.mutator, wizard.selectedContext, wizard.selectedNamespace, fwd)
|
||||
} else {
|
||||
// Cancelled
|
||||
// Cancelled - return to main view with screen clear
|
||||
m.ui.viewMode = ViewModeMain
|
||||
m.ui.addWizard = nil
|
||||
return m, tea.ClearScreen
|
||||
}
|
||||
|
||||
case StepSuccess:
|
||||
@@ -571,9 +557,10 @@ func (m model) handleAddWizardEnter() (tea.Model, tea.Cmd) {
|
||||
m.ui.addWizard.loading = true
|
||||
return m, loadContextsCmd(m.ui.discovery)
|
||||
} else {
|
||||
// Return to main view
|
||||
// Return to main view with screen clear
|
||||
m.ui.viewMode = ViewModeMain
|
||||
m.ui.addWizard = nil
|
||||
return m, tea.ClearScreen
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,5 +815,5 @@ func (m model) handleForwardsRemoved(msg ForwardsRemovedMsg) (tea.Model, tea.Cmd
|
||||
// If there was an error, it will be logged but we don't show it in UI for now
|
||||
// The config watcher will either reload (success) or keep old config (failure)
|
||||
|
||||
return m, nil
|
||||
return m, tea.ClearScreen
|
||||
}
|
||||
|
||||
@@ -363,3 +363,13 @@ func (w *AddWizardState) clearSearchFilter() {
|
||||
w.cursor = 0
|
||||
w.scrollOffset = 0
|
||||
}
|
||||
|
||||
// resetInput clears text input, search filter, and error state.
|
||||
// Use this when navigating between wizard steps.
|
||||
func (w *AddWizardState) resetInput() {
|
||||
w.textInput = ""
|
||||
w.searchFilter = ""
|
||||
w.cursor = 0
|
||||
w.scrollOffset = 0
|
||||
w.error = nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user