chore(tui): visual polish — detail sections, column headers, alert detail #37
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
Reference in New Issue
Block a user