refactor(tui): two-tier responsive table layout (compact/wide at 120 cols)
Replace continuous surplus distribution with two fixed layouts per table. Breakpoint at 120 columns — matches how btop/k9s do it. Compact (<120): short headers (LAT, UP%, RT, ST, MON, SENT, VER), tight fixed widths, no surplus guessing. Wide (≥120): full headers (LATENCY, UPTIME, RETRIES, STATUS, MONITORS, LAST SENT, VERSION), generous widths. Sites tab keeps content-aware NAME sizing + sparkline flex. All other tabs (Alerts, Maint, Nodes, Users) use simple fixed tiers. Removed old computeTableLayout/colDef/tierCol/pickTier — no longer needed.
This commit is contained in:
+11
-17
@@ -148,23 +148,17 @@ func (m Model) viewAlertsTab() string {
|
|||||||
return "\n No alert channels configured. Press [n] to add one."
|
return "\n No alert channels configured. Press [n] to add one."
|
||||||
}
|
}
|
||||||
|
|
||||||
maxName := 0
|
var headers []string
|
||||||
for _, a := range m.alerts {
|
var widths []int
|
||||||
if n := len([]rune(a.Name)); n > maxName {
|
if m.isWide() {
|
||||||
maxName = n
|
headers = []string{"#", "", "NAME", "TYPE", "CONFIG", "LAST SENT"}
|
||||||
}
|
widths = []int{4, 3, 18, 12, 40, 12}
|
||||||
|
} else {
|
||||||
|
headers = []string{"#", "", "NAME", "TYPE", "CONFIG", "SENT"}
|
||||||
|
widths = []int{4, 3, 14, 10, 24, 8}
|
||||||
}
|
}
|
||||||
|
|
||||||
cols := []colDef{
|
|
||||||
{"#", "#", 4, 4, false},
|
|
||||||
{"", "", 3, 3, false},
|
|
||||||
{"NAME", "NAME", 10, 20, false},
|
|
||||||
{"TYPE", "TYPE", 10, 12, false},
|
|
||||||
{"CONFIG", "CONFIG", 15, 40, true},
|
|
||||||
{"SENT", "LAST SENT", 8, 12, false},
|
|
||||||
}
|
|
||||||
headers, widths := m.computeTableLayout(cols, 0)
|
|
||||||
nameW := widths[2]
|
nameW := widths[2]
|
||||||
|
cfgW := widths[4]
|
||||||
|
|
||||||
return m.renderTable(
|
return m.renderTable(
|
||||||
headers,
|
headers,
|
||||||
@@ -179,10 +173,10 @@ func (m Model) viewAlertsTab() string {
|
|||||||
fmtAlertHealth(h),
|
fmtAlertHealth(h),
|
||||||
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)),
|
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)),
|
||||||
fmtAlertType(a.Type),
|
fmtAlertType(a.Type),
|
||||||
fmtAlertConfig(struct {
|
limitStr(fmtAlertConfig(struct {
|
||||||
Type string
|
Type string
|
||||||
Settings map[string]string
|
Settings map[string]string
|
||||||
}{a.Type, a.Settings}),
|
}{a.Type, a.Settings}), cfgW-2),
|
||||||
fmtAlertLastSent(h),
|
fmtAlertLastSent(h),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,16 +100,15 @@ func (m Model) viewMaintTab() string {
|
|||||||
return "\n No maintenance windows or incidents. Press [n] to create one."
|
return "\n No maintenance windows or incidents. Press [n] to create one."
|
||||||
}
|
}
|
||||||
|
|
||||||
cols := []colDef{
|
var headers []string
|
||||||
{"#", "#", 4, 4, false},
|
var widths []int
|
||||||
{"TITLE", "TITLE", 12, 24, false},
|
if m.isWide() {
|
||||||
{"TYPE", "TYPE", 13, 14, false},
|
headers = []string{"#", "TITLE", "TYPE", "MONITORS", "STATUS", "STARTED", "ENDS"}
|
||||||
{"MON", "MONITORS", 14, 22, true},
|
widths = []int{4, 24, 14, 22, 12, 16, 16}
|
||||||
{"ST", "STATUS", 11, 12, false},
|
} else {
|
||||||
{"START", "STARTED", 14, 16, false},
|
headers = []string{"#", "TITLE", "TYPE", "MON", "ST", "START", "ENDS"}
|
||||||
{"ENDS", "ENDS", 14, 16, false},
|
widths = []int{4, 14, 13, 14, 11, 14, 14}
|
||||||
}
|
}
|
||||||
headers, widths := m.computeTableLayout(cols, 0)
|
|
||||||
titleW := widths[1]
|
titleW := widths[1]
|
||||||
monW := widths[3]
|
monW := widths[3]
|
||||||
timeW := widths[5]
|
timeW := widths[5]
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ func (m Model) viewNodesTab() string {
|
|||||||
return "\n No probe nodes connected."
|
return "\n No probe nodes connected."
|
||||||
}
|
}
|
||||||
|
|
||||||
cols := []colDef{
|
var headers []string
|
||||||
{"NAME", "NAME", 12, 24, true},
|
var widths []int
|
||||||
{"REGION", "REGION", 8, 14, false},
|
if m.isWide() {
|
||||||
{"SEEN", "LAST SEEN", 8, 20, false},
|
headers = []string{"NAME", "REGION", "LAST SEEN", "VERSION", "STATUS"}
|
||||||
{"VER", "VERSION", 8, 12, false},
|
widths = []int{24, 14, 16, 12, 10}
|
||||||
{"STATUS", "STATUS", 8, 10, false},
|
} else {
|
||||||
|
headers = []string{"NAME", "REGION", "SEEN", "VER", "STATUS"}
|
||||||
|
widths = []int{16, 10, 10, 8, 8}
|
||||||
}
|
}
|
||||||
headers, widths := m.computeTableLayout(cols, 0)
|
|
||||||
nameW := widths[0]
|
nameW := widths[0]
|
||||||
|
|
||||||
return m.renderTable(
|
return m.renderTable(
|
||||||
|
|||||||
+26
-70
@@ -341,31 +341,29 @@ type tableLayout struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) computeLayout() tableLayout {
|
func (m Model) computeLayout() tableLayout {
|
||||||
cols := []colDef{
|
wide := m.isWide()
|
||||||
{"#", "#", 4, 4, false},
|
|
||||||
{"", "", 0, 0, false}, // NAME (special)
|
var fixed int
|
||||||
{"TYPE", "TYPE", 8, 10, false},
|
var headers []string
|
||||||
{"STATUS", "STATUS", 8, 10, false},
|
var widths []int
|
||||||
{"LAT", "LATENCY", 7, 10, false},
|
|
||||||
{"UP%", "UPTIME", 8, 8, false},
|
if wide {
|
||||||
{"", "", 0, 0, false}, // HISTORY (special)
|
// # NAME TYPE STATUS LATENCY UPTIME HISTORY SSL RETRIES
|
||||||
{"SSL", "SSL", 5, 5, false},
|
headers = []string{"#", "NAME", "TYPE", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRIES"}
|
||||||
{"RT", "RETRIES", 5, 9, false},
|
widths = []int{4, 0, 10, 10, 10, 8, 0, 7, 9}
|
||||||
|
fixed = 4 + 10 + 10 + 10 + 8 + 7 + 9
|
||||||
|
} else {
|
||||||
|
// # NAME TYPE STATUS LAT UP% HISTORY SSL RT
|
||||||
|
headers = []string{"#", "NAME", "TYPE", "STATUS", "LAT", "UP%", "HISTORY", "SSL", "RT"}
|
||||||
|
widths = []int{4, 0, 8, 8, 7, 7, 0, 5, 5}
|
||||||
|
fixed = 4 + 8 + 8 + 7 + 7 + 5 + 5
|
||||||
}
|
}
|
||||||
|
|
||||||
numCols := len(cols)
|
numCols := len(headers)
|
||||||
borderOverhead := 2 + (numCols - 1) // left + right border + column separators
|
borderOverhead := 2 + (numCols - 1)
|
||||||
usable := m.termWidth - chromePadH - 2 - borderOverhead
|
avail := m.termWidth - chromePadH - 2 - borderOverhead - fixed
|
||||||
if usable < 80 {
|
if avail < 20 {
|
||||||
usable = 80
|
avail = 20
|
||||||
}
|
|
||||||
|
|
||||||
fixedMin := 0
|
|
||||||
for i, c := range cols {
|
|
||||||
if i == 1 || i == 6 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fixedMin += c.minWidth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
maxName := 0
|
maxName := 0
|
||||||
@@ -374,68 +372,26 @@ func (m Model) computeLayout() tableLayout {
|
|||||||
maxName = n
|
maxName = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
maxName += 4 // icon + padding + error preview room
|
maxName += 4
|
||||||
|
|
||||||
avail := usable - fixedMin
|
|
||||||
nameW := avail / 2
|
nameW := avail / 2
|
||||||
if nameW > maxName {
|
if nameW > maxName {
|
||||||
nameW = maxName
|
nameW = maxName
|
||||||
}
|
}
|
||||||
sparkW := avail - nameW - 2
|
|
||||||
|
|
||||||
if nameW < 13 {
|
if nameW < 13 {
|
||||||
nameW = 13
|
nameW = 13
|
||||||
}
|
}
|
||||||
if nameW > 40 {
|
if nameW > 40 {
|
||||||
nameW = 40
|
nameW = 40
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sparkW := avail - nameW
|
||||||
if sparkW < 10 {
|
if sparkW < 10 {
|
||||||
sparkW = 10
|
sparkW = 10
|
||||||
}
|
}
|
||||||
if sparkW > 60 {
|
|
||||||
sparkW = 60
|
|
||||||
}
|
|
||||||
|
|
||||||
surplus := usable - fixedMin - nameW - sparkW - 2
|
widths[1] = nameW
|
||||||
if surplus < 0 {
|
widths[6] = sparkW
|
||||||
surplus = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := make([]string, len(cols))
|
|
||||||
widths := make([]int, len(cols))
|
|
||||||
for i, c := range cols {
|
|
||||||
if i == 1 {
|
|
||||||
headers[i] = "NAME"
|
|
||||||
widths[i] = nameW
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if i == 6 {
|
|
||||||
headers[i] = "HISTORY"
|
|
||||||
widths[i] = sparkW + 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
w := c.minWidth
|
|
||||||
expand := c.maxWidth - c.minWidth
|
|
||||||
if surplus >= expand {
|
|
||||||
w = c.maxWidth
|
|
||||||
surplus -= expand
|
|
||||||
} else if surplus > 0 {
|
|
||||||
w += surplus
|
|
||||||
surplus = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if w >= len(c.full)+2 {
|
|
||||||
headers[i] = c.full
|
|
||||||
} else {
|
|
||||||
headers[i] = c.short
|
|
||||||
}
|
|
||||||
widths[i] = w
|
|
||||||
}
|
|
||||||
|
|
||||||
if surplus > 0 {
|
|
||||||
widths[6] += surplus
|
|
||||||
}
|
|
||||||
|
|
||||||
return tableLayout{
|
return tableLayout{
|
||||||
nameW: nameW,
|
nameW: nameW,
|
||||||
|
|||||||
@@ -32,13 +32,15 @@ func (m Model) viewUsersTab() string {
|
|||||||
return "\n No users configured. Press [n] to add one."
|
return "\n No users configured. Press [n] to add one."
|
||||||
}
|
}
|
||||||
|
|
||||||
cols := []colDef{
|
var headers []string
|
||||||
{"#", "#", 4, 4, false},
|
var widths []int
|
||||||
{"USER", "USERNAME", 10, 18, false},
|
if m.isWide() {
|
||||||
{"ROLE", "ROLE", 8, 10, false},
|
headers = []string{"#", "USERNAME", "ROLE", "PUBLIC KEY"}
|
||||||
{"KEY", "PUBLIC KEY", 20, 60, true},
|
widths = []int{4, 18, 10, 50}
|
||||||
|
} else {
|
||||||
|
headers = []string{"#", "USER", "ROLE", "KEY"}
|
||||||
|
widths = []int{4, 14, 8, 30}
|
||||||
}
|
}
|
||||||
headers, widths := m.computeTableLayout(cols, 0)
|
|
||||||
userW := widths[1]
|
userW := widths[1]
|
||||||
|
|
||||||
return m.renderTable(
|
return m.renderTable(
|
||||||
|
|||||||
@@ -15,77 +15,10 @@ var (
|
|||||||
|
|
||||||
type StyleOverride func(row, col int) *lipgloss.Style
|
type StyleOverride func(row, col int) *lipgloss.Style
|
||||||
|
|
||||||
type colDef struct {
|
const wideBreakpoint = 120
|
||||||
short string
|
|
||||||
full string
|
|
||||||
minWidth int
|
|
||||||
maxWidth int
|
|
||||||
flex bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) computeTableLayout(cols []colDef, maxContentWidth int) ([]string, []int) {
|
func (m Model) isWide() bool {
|
||||||
numCols := len(cols)
|
return m.termWidth >= wideBreakpoint
|
||||||
borderOverhead := 2 + (numCols - 1)
|
|
||||||
usable := m.termWidth - chromePadH - 2 - borderOverhead
|
|
||||||
if usable < 40 {
|
|
||||||
usable = 40
|
|
||||||
}
|
|
||||||
|
|
||||||
fixedMin := 0
|
|
||||||
flexIdx := -1
|
|
||||||
for i, c := range cols {
|
|
||||||
if c.flex {
|
|
||||||
flexIdx = i
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fixedMin += c.minWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
flexW := usable - fixedMin
|
|
||||||
if maxContentWidth > 0 && flexW > maxContentWidth {
|
|
||||||
flexW = maxContentWidth
|
|
||||||
}
|
|
||||||
if flexW < 8 {
|
|
||||||
flexW = 8
|
|
||||||
}
|
|
||||||
|
|
||||||
surplus := usable - fixedMin - flexW
|
|
||||||
if surplus < 0 {
|
|
||||||
surplus = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := make([]string, numCols)
|
|
||||||
widths := make([]int, numCols)
|
|
||||||
for i, c := range cols {
|
|
||||||
if c.flex {
|
|
||||||
headers[i] = c.full
|
|
||||||
widths[i] = flexW
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
w := c.minWidth
|
|
||||||
expand := c.maxWidth - c.minWidth
|
|
||||||
if surplus >= expand {
|
|
||||||
w = c.maxWidth
|
|
||||||
surplus -= expand
|
|
||||||
} else if surplus > 0 {
|
|
||||||
w += surplus
|
|
||||||
surplus = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if w >= len(c.full)+2 {
|
|
||||||
headers[i] = c.full
|
|
||||||
} else {
|
|
||||||
headers[i] = c.short
|
|
||||||
}
|
|
||||||
widths[i] = w
|
|
||||||
}
|
|
||||||
|
|
||||||
if surplus > 0 && flexIdx >= 0 {
|
|
||||||
widths[flexIdx] += surplus
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers, widths
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderTable(headers []string, items int, buildRows func(start, end int) [][]string, colWidths []int, styleOverride StyleOverride) string {
|
func (m Model) renderTable(headers []string, items int, buildRows func(start, end int) [][]string, colWidths []int, styleOverride StyleOverride) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user