mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-09 23:59:45 +00:00
bfe541565b
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
242 lines
5.7 KiB
Go
242 lines
5.7 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/lukaszraczylo/kportal/internal/config"
|
|
)
|
|
|
|
// ForwardStatus represents the current status of a port forward
|
|
type ForwardStatus struct {
|
|
HTTPLog *config.HTTPLogSpec
|
|
Context string
|
|
Namespace string
|
|
Alias string
|
|
Type string
|
|
Resource string
|
|
Status string
|
|
RemotePort int
|
|
LocalPort int
|
|
}
|
|
|
|
// TableUI manages the terminal table display
|
|
type TableUI struct {
|
|
forwards map[string]*ForwardStatus
|
|
mu sync.RWMutex
|
|
verbose bool
|
|
}
|
|
|
|
// NewTableUI creates a new table UI manager
|
|
func NewTableUI(verbose bool) *TableUI {
|
|
return &TableUI{
|
|
forwards: make(map[string]*ForwardStatus),
|
|
verbose: verbose,
|
|
}
|
|
}
|
|
|
|
// AddForward registers a new forward for display
|
|
func (t *TableUI) AddForward(id string, fwd *config.Forward) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
// Parse resource type and name
|
|
resourceType := "pod"
|
|
resourceName := fwd.Resource
|
|
|
|
parts := strings.Split(fwd.Resource, "/")
|
|
if len(parts) == 2 {
|
|
resourceType = parts[0]
|
|
resourceName = parts[1]
|
|
}
|
|
|
|
status := &ForwardStatus{
|
|
Context: fwd.GetContext(),
|
|
Namespace: fwd.GetNamespace(),
|
|
Alias: fwd.Alias,
|
|
Type: resourceType,
|
|
Resource: resourceName,
|
|
RemotePort: fwd.Port,
|
|
LocalPort: fwd.LocalPort,
|
|
Status: "Starting",
|
|
}
|
|
|
|
// If no alias, use resource name as display name
|
|
if status.Alias == "" {
|
|
status.Alias = resourceName
|
|
}
|
|
|
|
t.forwards[id] = status
|
|
}
|
|
|
|
// UpdateStatus updates the status of a forward
|
|
func (t *TableUI) UpdateStatus(id string, status string) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
if fwd, ok := t.forwards[id]; ok {
|
|
fwd.Status = status
|
|
}
|
|
}
|
|
|
|
// Render displays the current table
|
|
func (t *TableUI) Render() {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
// Clear screen and move cursor to top
|
|
if !t.verbose {
|
|
fmt.Print("\033[2J\033[H")
|
|
}
|
|
|
|
// Print header
|
|
fmt.Println("kportal - Port Forwarding Status")
|
|
fmt.Println(strings.Repeat("=", 130))
|
|
|
|
// Table header
|
|
fmt.Printf("%-15s %-18s %-25s %-10s %-25s %-12s %-12s %-12s\n",
|
|
"CONTEXT", "NAMESPACE", "ALIAS", "TYPE", "RESOURCE", "REMOTE PORT", "LOCAL PORT", "STATUS")
|
|
fmt.Println(strings.Repeat("-", 130))
|
|
|
|
// Sort forwards by local port for consistent display
|
|
type sortEntry struct {
|
|
fwd *ForwardStatus
|
|
id string
|
|
}
|
|
var entries []sortEntry
|
|
for id, fwd := range t.forwards {
|
|
entries = append(entries, sortEntry{fwd: fwd, id: id})
|
|
}
|
|
|
|
// Simple sort by local port
|
|
for i := 0; i < len(entries); i++ {
|
|
for j := i + 1; j < len(entries); j++ {
|
|
if entries[i].fwd.LocalPort > entries[j].fwd.LocalPort {
|
|
entries[i], entries[j] = entries[j], entries[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Print each forward
|
|
for _, entry := range entries {
|
|
fwd := entry.fwd
|
|
|
|
// Truncate long names
|
|
alias := truncate(fwd.Alias, 25)
|
|
resource := truncate(fwd.Resource, 25)
|
|
|
|
// Color code status with indicator
|
|
statusStr := formatStatusWithIndicator(fwd.Status)
|
|
|
|
// Print the row
|
|
fmt.Printf(" %-15s %-18s %-25s %-10s %-25s %-12d %-12d %s\n",
|
|
fwd.Context,
|
|
fwd.Namespace,
|
|
alias,
|
|
fwd.Type,
|
|
resource,
|
|
fwd.RemotePort,
|
|
fwd.LocalPort,
|
|
statusStr)
|
|
}
|
|
|
|
fmt.Println(strings.Repeat("=", 130))
|
|
fmt.Printf("Total forwards: %d | Press Ctrl+C to stop\n", len(t.forwards))
|
|
|
|
// In verbose mode, add a newline to separate from logs
|
|
if t.verbose {
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
// RenderInitial renders the table once without clearing screen
|
|
func (t *TableUI) RenderInitial() {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
// Print header
|
|
fmt.Println("\nkportal - Port Forwarding Status")
|
|
fmt.Println(strings.Repeat("=", 130))
|
|
|
|
// Table header
|
|
fmt.Printf("%-15s %-18s %-25s %-10s %-25s %-12s %-12s %-12s\n",
|
|
"CONTEXT", "NAMESPACE", "ALIAS", "TYPE", "RESOURCE", "REMOTE PORT", "LOCAL PORT", "STATUS")
|
|
fmt.Println(strings.Repeat("-", 130))
|
|
|
|
// Print message if no forwards yet
|
|
if len(t.forwards) == 0 {
|
|
fmt.Println("Initializing port forwards...")
|
|
}
|
|
|
|
fmt.Println(strings.Repeat("=", 130))
|
|
fmt.Println()
|
|
}
|
|
|
|
// GetForward returns a forward status by ID
|
|
func (t *TableUI) GetForward(id string) *ForwardStatus {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
return t.forwards[id]
|
|
}
|
|
|
|
// Remove removes a forward from the display
|
|
func (t *TableUI) Remove(id string) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
delete(t.forwards, id)
|
|
}
|
|
|
|
// hyperlink wraps text in an OSC 8 terminal hyperlink escape sequence.
|
|
// Clicking the text opens the URL in terminals that support it (Ghostty, iTerm2,
|
|
// Windows Terminal, Kitty, WezTerm, etc.). Unsupported terminals show plain text.
|
|
func hyperlink(url, text string) string {
|
|
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", url, text)
|
|
}
|
|
|
|
// truncate truncates a string to maxLen, adding "..." if needed
|
|
func truncate(s string, maxLen int) string {
|
|
if len(s) <= maxLen {
|
|
return s
|
|
}
|
|
if maxLen <= 3 {
|
|
return s[:maxLen]
|
|
}
|
|
return s[:maxLen-3] + "..."
|
|
}
|
|
|
|
// formatStatusWithIndicator adds color-coded indicator symbols to status
|
|
func formatStatusWithIndicator(status string) string {
|
|
// Check if stdout is a terminal
|
|
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) == 0 {
|
|
// Not a terminal, return plain text with simple indicator
|
|
switch status {
|
|
case "Active":
|
|
return "✓ " + status
|
|
case "Starting":
|
|
return "⋯ " + status
|
|
case "Reconnecting":
|
|
return "↻ " + status
|
|
case "Error", "Failed":
|
|
return "✗ " + status
|
|
default:
|
|
return status
|
|
}
|
|
}
|
|
|
|
// Terminal with color support
|
|
switch status {
|
|
case "Active":
|
|
return "\033[32m●\033[0m " + status // Green circle
|
|
case "Starting":
|
|
return "\033[33m○\033[0m " + status // Yellow circle (hollow)
|
|
case "Reconnecting":
|
|
return "\033[33m◐\033[0m " + status // Yellow half-circle
|
|
case "Error", "Failed":
|
|
return "\033[31m●\033[0m " + status // Red circle
|
|
default:
|
|
return status
|
|
}
|
|
}
|