Allow configuring forwards-table columns, order, and width (#82)

* Allow configuring forwards-table columns, order, and width

Adds an optional tui.columns section to .kportal.yaml so the visible
columns, their order, and their widths can be customized instead of
always rendering the hardcoded 8-column layout. Omitting the section
keeps the existing default columns/order/widths.

Closes #81

* fix(ui): keep the plain table layout and check column widths

The plain verbose table must look the same when no tui section is
set. It had narrower columns and shorter port headers than before. The
default layout is now identical to the old one, and a golden test
guards it.

A status column that is not last now pads by visible width. Table
rules follow the column widths. The UI applies new columns only after a
reload succeeds, including the SIGHUP path of the verbose table.

The validator rejects a column width above 200. The README describes
the tui.columns setting.

Refs #81

* fix(httplog): replace the deprecated ReverseProxy Director with Rewrite

Staticcheck reports SA1019 because Go 1.26 deprecates Director. The
proxy now sets the target in a Rewrite function. Rewrite drops the
X-Forwarded-For header that Director sent, so the function calls
SetXForwarded to keep that header.

A new test checks the target port, the path and the header.

* fix(release): drop the deprecated cask url verified key

Homebrew warns about the verified parameter of the cask url stanza.
The setting is not needed. GoReleaser check now passes without a
deprecation warning.

The postflight warning remains. GoReleaser generates it from the
post install hook, and the upstream fix is not released yet.

Refs #79

---------

Co-authored-by: Lukasz Raczylo <[email protected]>
This commit is contained in:
Bruno Costa
2026-10-02 02:24:11 +01:00
committed by GitHub
co-authored by lukaszraczylo
parent 232c0f1162
commit f3d3f259a1
15 changed files with 849 additions and 61 deletions
+72
View File
@@ -85,6 +85,7 @@ func (v *Validator) ValidateConfigWithOptions(cfg *Config, allowEmpty bool) []Va
if allowEmpty && cfg.IsEmpty() {
// Still validate health check and reliability if present (they don't require forwards)
errs = append(errs, v.validateSpecDurations(cfg)...)
errs = append(errs, v.validateTUI(cfg)...)
return errs
}
@@ -111,6 +112,9 @@ func (v *Validator) ValidateConfigWithOptions(cfg *Config, allowEmpty bool) []Va
// Validate duration fields in specs
errs = append(errs, v.validateSpecDurations(cfg)...)
// Validate TUI table column configuration
errs = append(errs, v.validateTUI(cfg)...)
return errs
}
@@ -507,6 +511,74 @@ func (v *Validator) validateMDNS(cfg *Config) []ValidationError {
return errs
}
// maxTableColumnWidth caps tui.columns width so a typo cannot blow up the table.
const maxTableColumnWidth = 200
// validTableColumnNames are the recognized forwards-table column identifiers.
var validTableColumnNames = map[string]bool{
"context": true,
"namespace": true,
"alias": true,
"type": true,
"resource": true,
"remote": true,
"local": true,
"status": true,
}
// validateTUI validates the tui.columns configuration.
func (v *Validator) validateTUI(cfg *Config) []ValidationError {
var errs []ValidationError
if cfg.TUI == nil {
return errs
}
seen := make(map[string]bool)
for i, col := range cfg.TUI.Columns {
name := strings.ToLower(strings.TrimSpace(col.Name))
if name == "" {
errs = append(errs, ValidationError{
Field: fmt.Sprintf("tui.columns[%d].name", i),
Message: "Column name cannot be empty",
})
continue
}
if !validTableColumnNames[name] {
errs = append(errs, ValidationError{
Field: fmt.Sprintf("tui.columns[%d].name", i),
Message: fmt.Sprintf("Invalid column name '%s' (must be one of: context, namespace, alias, type, resource, remote, local, status)", col.Name),
})
continue
}
if seen[name] {
errs = append(errs, ValidationError{
Field: fmt.Sprintf("tui.columns[%d].name", i),
Message: fmt.Sprintf("Duplicate column '%s' in tui.columns", name),
})
continue
}
seen[name] = true
if col.Width < 0 {
errs = append(errs, ValidationError{
Field: fmt.Sprintf("tui.columns[%d].width", i),
Message: fmt.Sprintf("Column '%s' width cannot be negative", name),
})
} else if col.Width > maxTableColumnWidth {
errs = append(errs, ValidationError{
Field: fmt.Sprintf("tui.columns[%d].width", i),
Message: fmt.Sprintf("Column '%s' width cannot exceed %d", name, maxTableColumnWidth),
})
}
}
return errs
}
// isValidHostname checks if a string is a valid RFC 1123 hostname.
// Hostnames must start with alphanumeric, contain only alphanumeric and hyphens,
// and be 1-63 characters long.