mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-10-11 04:27:10 +00:00
* 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]>
161 lines
4.9 KiB
Go
161 lines
4.9 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/lukaszraczylo/kportal/internal/config"
|
|
)
|
|
|
|
// ColumnKey identifies a forwards-table column.
|
|
type ColumnKey string
|
|
|
|
// Recognized column keys, matching the names accepted in tui.columns.
|
|
const (
|
|
ColKeyContext ColumnKey = "context"
|
|
ColKeyNamespace ColumnKey = "namespace"
|
|
ColKeyAlias ColumnKey = "alias"
|
|
ColKeyType ColumnKey = "type"
|
|
ColKeyResource ColumnKey = "resource"
|
|
ColKeyRemote ColumnKey = "remote"
|
|
ColKeyLocal ColumnKey = "local"
|
|
ColKeyStatus ColumnKey = "status"
|
|
)
|
|
|
|
// ResolvedColumn describes a single column ready for rendering: its header
|
|
// text, pad width, truncation limit, and identity.
|
|
type ResolvedColumn struct {
|
|
Key ColumnKey
|
|
Header string
|
|
// Width is the padded width, used by the plain table only.
|
|
Width int
|
|
// Max is the truncation limit; zero leaves the value untruncated.
|
|
Max int
|
|
}
|
|
|
|
// tuiDefaultColumns is the Bubble Tea column set and order, used when the
|
|
// config does not specify tui.columns.
|
|
var tuiDefaultColumns = []ResolvedColumn{
|
|
{Key: ColKeyContext, Header: "CONTEXT", Width: ColumnWidthContext, Max: ColumnWidthContext},
|
|
{Key: ColKeyNamespace, Header: "NAMESPACE", Width: ColumnWidthNamespace, Max: ColumnWidthNamespace},
|
|
{Key: ColKeyAlias, Header: "ALIAS", Width: ColumnWidthAlias, Max: ColumnWidthAlias},
|
|
{Key: ColKeyType, Header: "TYPE", Width: ColumnWidthType, Max: ColumnWidthType},
|
|
{Key: ColKeyResource, Header: "RESOURCE", Width: ColumnWidthResource, Max: ColumnWidthResource},
|
|
{Key: ColKeyRemote, Header: "REMOTE"},
|
|
{Key: ColKeyLocal, Header: "LOCAL"},
|
|
{Key: ColKeyStatus, Header: "STATUS"},
|
|
}
|
|
|
|
// plainDefaultColumns is the -verbose table column set, widths and headers as
|
|
// rendered before columns became configurable.
|
|
var plainDefaultColumns = []ResolvedColumn{
|
|
{Key: ColKeyContext, Header: "CONTEXT", Width: 15},
|
|
{Key: ColKeyNamespace, Header: "NAMESPACE", Width: 18},
|
|
{Key: ColKeyAlias, Header: "ALIAS", Width: 25, Max: 25},
|
|
{Key: ColKeyType, Header: "TYPE", Width: 10},
|
|
{Key: ColKeyResource, Header: "RESOURCE", Width: 25, Max: 25},
|
|
{Key: ColKeyRemote, Header: "REMOTE PORT", Width: 12},
|
|
{Key: ColKeyLocal, Header: "LOCAL PORT", Width: 12},
|
|
{Key: ColKeyStatus, Header: "STATUS", Width: 12},
|
|
}
|
|
|
|
// ResolveColumns builds the Bubble Tea columns from the tui.columns
|
|
// configuration. A nil config, or one with no tui.columns entries, yields the
|
|
// built-in default column set and order.
|
|
func ResolveColumns(cfg *config.Config) []ResolvedColumn {
|
|
return resolveColumns(cfg, tuiDefaultColumns)
|
|
}
|
|
|
|
// plainStatusWidth fits the longest status with indicator ("↻ Reconnecting"),
|
|
// so a status column that is not last cannot push later columns out of line.
|
|
const plainStatusWidth = 14
|
|
|
|
// ResolvePlainColumns is ResolveColumns for the -verbose table.
|
|
func ResolvePlainColumns(cfg *config.Config) []ResolvedColumn {
|
|
defaults := plainDefaultColumns
|
|
if len(cfg.GetTableColumns()) > 0 {
|
|
defaults = slices.Clone(plainDefaultColumns)
|
|
defaults[columnIndex(defaults, ColKeyStatus)].Width = plainStatusWidth
|
|
}
|
|
return resolveColumns(cfg, defaults)
|
|
}
|
|
|
|
func resolveColumns(cfg *config.Config, defaults []ResolvedColumn) []ResolvedColumn {
|
|
var configured []config.TableColumn
|
|
if cfg != nil {
|
|
configured = cfg.GetTableColumns()
|
|
}
|
|
|
|
byKey := make(map[ColumnKey]ResolvedColumn, len(defaults))
|
|
for _, c := range defaults {
|
|
byKey[c.Key] = c
|
|
}
|
|
|
|
resolved := make([]ResolvedColumn, 0, len(configured))
|
|
for _, c := range configured {
|
|
col, ok := byKey[ColumnKey(strings.ToLower(strings.TrimSpace(c.Name)))]
|
|
if !ok {
|
|
// Unknown column names are rejected by the validator before this
|
|
// point is reached; skip defensively rather than render garbage.
|
|
continue
|
|
}
|
|
|
|
if c.Width > 0 {
|
|
col.Width = c.Width
|
|
col.Max = c.Width
|
|
}
|
|
|
|
resolved = append(resolved, col)
|
|
}
|
|
|
|
if len(resolved) == 0 {
|
|
return slices.Clone(defaults)
|
|
}
|
|
|
|
return resolved
|
|
}
|
|
|
|
// columnValue returns the display value for the given column of a forward,
|
|
// truncated to the column's Max when set.
|
|
func columnValue(col ResolvedColumn, fwd *ForwardStatus) string {
|
|
switch col.Key {
|
|
case ColKeyContext:
|
|
return clip(fwd.Context, col.Max)
|
|
case ColKeyNamespace:
|
|
return clip(fwd.Namespace, col.Max)
|
|
case ColKeyAlias:
|
|
return clip(fwd.Alias, col.Max)
|
|
case ColKeyType:
|
|
return clip(fwd.Type, col.Max)
|
|
case ColKeyResource:
|
|
return clip(fwd.Resource, col.Max)
|
|
case ColKeyRemote:
|
|
return fmt.Sprintf("%d", fwd.RemotePort)
|
|
case ColKeyLocal:
|
|
return fmt.Sprintf("%d", fwd.LocalPort)
|
|
case ColKeyStatus:
|
|
return fwd.Status
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// clip truncates s to limit runes; a non-positive limit leaves it untouched.
|
|
func clip(s string, limit int) string {
|
|
if limit <= 0 {
|
|
return s
|
|
}
|
|
return truncate(s, limit)
|
|
}
|
|
|
|
// columnIndex returns the position of key within cols, or -1 if absent.
|
|
func columnIndex(cols []ResolvedColumn, key ColumnKey) int {
|
|
for i, c := range cols {
|
|
if c.Key == key {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|