chore(tui): visual polish — detail sections, column headers, alert detail #37

Merged
lerko merged 20 commits from chore/ux-polish into main 2026-05-28 20:40:29 +00:00
6 changed files with 144 additions and 30 deletions
Showing only changes of commit d05bbd007b - Show all commits
+21 -3
View File
@@ -148,8 +148,26 @@ 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
for _, a := range m.alerts {
if n := len([]rune(a.Name)); n > maxName {
maxName = n
}
}
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]
return m.renderTable( return m.renderTable(
[]string{"#", "", "NAME", "TYPE", "CONFIG", "LAST SENT"}, headers,
len(m.alerts), len(m.alerts),
func(start, end int) [][]string { func(start, end int) [][]string {
var rows [][]string var rows [][]string
@@ -159,7 +177,7 @@ func (m Model) viewAlertsTab() string {
rows = append(rows, []string{ rows = append(rows, []string{
fmt.Sprintf("%d", i+1), fmt.Sprintf("%d", i+1),
fmtAlertHealth(h), fmtAlertHealth(h),
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, 15)), m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)),
fmtAlertType(a.Type), fmtAlertType(a.Type),
fmtAlertConfig(struct { fmtAlertConfig(struct {
Type string Type string
@@ -170,7 +188,7 @@ func (m Model) viewAlertsTab() string {
} }
return rows return rows
}, },
nil, nil, widths, nil,
) )
} }
+17 -4
View File
@@ -2,10 +2,11 @@ package tui
import ( import (
"fmt" "fmt"
"gitea.lerkolabs.com/lerko/uptop/internal/models"
"strconv" "strconv"
"time" "time"
"gitea.lerkolabs.com/lerko/uptop/internal/models"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
@@ -92,8 +93,20 @@ 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{
{"#", "#", 4, 4, false},
{"TITLE", "TITLE", 12, 28, true},
{"TYPE", "TYPE", 10, 14, false},
{"MON", "MONITORS", 10, 20, false},
{"STATUS", "STATUS", 8, 12, false},
{"START", "STARTED", 10, 16, false},
{"ENDS", "ENDS", 10, 16, false},
}
headers, widths := m.computeTableLayout(cols, 0)
titleW := widths[1]
return m.renderTable( return m.renderTable(
[]string{"#", "TITLE", "TYPE", "MONITORS", "STATUS", "STARTED", "ENDS"}, headers,
len(m.maintenanceWindows), len(m.maintenanceWindows),
func(start, end int) [][]string { func(start, end int) [][]string {
var rows [][]string var rows [][]string
@@ -102,7 +115,7 @@ func (m Model) viewMaintTab() string {
mw := m.maintenanceWindows[i] mw := m.maintenanceWindows[i]
rows = append(rows, []string{ rows = append(rows, []string{
strconv.Itoa(i + 1), strconv.Itoa(i + 1),
m.zones.Mark(fmt.Sprintf("maint-%d", i), limitStr(mw.Title, 24)), m.zones.Mark(fmt.Sprintf("maint-%d", i), limitStr(mw.Title, titleW-2)),
fmtMaintType(mw.Type), fmtMaintType(mw.Type),
fmtMaintMonitor(mw.MonitorID, allSites), fmtMaintMonitor(mw.MonitorID, allSites),
fmtMaintStatus(mw), fmtMaintStatus(mw),
@@ -112,7 +125,7 @@ func (m Model) viewMaintTab() string {
} }
return rows return rows
}, },
[]int{6, 0, 14, 20, 12, 16, 16}, widths,
nil, nil,
) )
} }
+12 -4
View File
@@ -10,16 +10,24 @@ func (m Model) viewNodesTab() string {
return "\n No probe nodes connected." return "\n No probe nodes connected."
} }
colWidths := []int{0, 12, 20, 10, 8} cols := []colDef{
{"NAME", "NAME", 12, 24, true},
{"REGION", "REGION", 8, 14, false},
{"SEEN", "LAST SEEN", 8, 20, false},
{"VER", "VERSION", 8, 12, false},
{"STATUS", "STATUS", 8, 10, false},
}
headers, widths := m.computeTableLayout(cols, 0)
nameW := widths[0]
return m.renderTable( return m.renderTable(
[]string{"NAME", "REGION", "LAST SEEN", "VERSION", "STATUS"}, headers,
len(m.nodes), len(m.nodes),
func(start, end int) [][]string { func(start, end int) [][]string {
var rows [][]string var rows [][]string
for i := start; i < end; i++ { for i := start; i < end; i++ {
node := m.nodes[i] node := m.nodes[i]
name := limitStr(node.Name, 20) name := limitStr(node.Name, nameW-2)
if name == "" { if name == "" {
name = node.ID name = node.ID
} }
@@ -37,7 +45,7 @@ func (m Model) viewNodesTab() string {
} }
return rows return rows
}, },
colWidths, widths,
nil, nil,
) )
} }
+9 -16
View File
@@ -341,23 +341,16 @@ type tableLayout struct {
} }
func (m Model) computeLayout() tableLayout { func (m Model) computeLayout() tableLayout {
type colDef struct {
short string
full string
minWidth int
maxWidth int
}
cols := []colDef{ cols := []colDef{
{"#", "#", 4, 4}, {"#", "#", 4, 4, false},
{"", "", 0, 0}, // NAME (dynamic) {"", "", 0, 0, false}, // NAME (special)
{"TYPE", "TYPE", 8, 10}, {"TYPE", "TYPE", 8, 10, false},
{"STATUS", "STATUS", 8, 10}, {"STATUS", "STATUS", 8, 10, false},
{"LAT", "LATENCY", 7, 10}, {"LAT", "LATENCY", 7, 10, false},
{"UP%", "UPTIME", 8, 8}, {"UP%", "UPTIME", 8, 8, false},
{"", "", 0, 0}, // HISTORY (dynamic) {"", "", 0, 0, false}, // HISTORY (special)
{"SSL", "SSL", 5, 5}, {"SSL", "SSL", 5, 5, false},
{"RT", "RETRIES", 5, 9}, {"RT", "RETRIES", 5, 9, false},
} }
numCols := len(cols) numCols := len(cols)
+12 -3
View File
@@ -32,8 +32,17 @@ 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{
{"#", "#", 4, 4, false},
{"USER", "USERNAME", 10, 18, false},
{"ROLE", "ROLE", 8, 10, false},
{"KEY", "PUBLIC KEY", 20, 60, true},
}
headers, widths := m.computeTableLayout(cols, 0)
userW := widths[1]
return m.renderTable( return m.renderTable(
[]string{"#", "USERNAME", "ROLE", "PUBLIC KEY"}, headers,
len(m.users), len(m.users),
func(start, end int) [][]string { func(start, end int) [][]string {
var rows [][]string var rows [][]string
@@ -41,14 +50,14 @@ func (m Model) viewUsersTab() string {
u := m.users[i] u := m.users[i]
rows = append(rows, []string{ rows = append(rows, []string{
fmt.Sprintf("%d", i+1), fmt.Sprintf("%d", i+1),
m.zones.Mark(fmt.Sprintf("user-%d", i), limitStr(u.Username, 15)), m.zones.Mark(fmt.Sprintf("user-%d", i), limitStr(u.Username, userW-2)),
fmtRole(u.Role), fmtRole(u.Role),
fmtKey(u.PublicKey), fmtKey(u.PublicKey),
}) })
} }
return rows return rows
}, },
nil, nil, widths, nil,
) )
} }
+73
View File
@@ -15,6 +15,79 @@ var (
type StyleOverride func(row, col int) *lipgloss.Style type StyleOverride func(row, col int) *lipgloss.Style
type colDef struct {
short string
full string
minWidth int
maxWidth int
flex bool
}
func (m Model) computeTableLayout(cols []colDef, maxContentWidth int) ([]string, []int) {
numCols := len(cols)
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 {
if items == 0 { if items == 0 {
return "" return ""