diff --git a/internal/tui/tab_alerts.go b/internal/tui/tab_alerts.go index 0ac4316..a8ba6a7 100644 --- a/internal/tui/tab_alerts.go +++ b/internal/tui/tab_alerts.go @@ -148,8 +148,26 @@ func (m Model) viewAlertsTab() string { 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( - []string{"#", "", "NAME", "TYPE", "CONFIG", "LAST SENT"}, + headers, len(m.alerts), func(start, end int) [][]string { var rows [][]string @@ -159,7 +177,7 @@ func (m Model) viewAlertsTab() string { rows = append(rows, []string{ fmt.Sprintf("%d", i+1), 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), fmtAlertConfig(struct { Type string @@ -170,7 +188,7 @@ func (m Model) viewAlertsTab() string { } return rows }, - nil, nil, + widths, nil, ) } diff --git a/internal/tui/tab_maint.go b/internal/tui/tab_maint.go index 68d6141..0c061ca 100644 --- a/internal/tui/tab_maint.go +++ b/internal/tui/tab_maint.go @@ -2,10 +2,11 @@ package tui import ( "fmt" - "gitea.lerkolabs.com/lerko/uptop/internal/models" "strconv" "time" + "gitea.lerkolabs.com/lerko/uptop/internal/models" + tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "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." } + 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( - []string{"#", "TITLE", "TYPE", "MONITORS", "STATUS", "STARTED", "ENDS"}, + headers, len(m.maintenanceWindows), func(start, end int) [][]string { var rows [][]string @@ -102,7 +115,7 @@ func (m Model) viewMaintTab() string { mw := m.maintenanceWindows[i] rows = append(rows, []string{ 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), fmtMaintMonitor(mw.MonitorID, allSites), fmtMaintStatus(mw), @@ -112,7 +125,7 @@ func (m Model) viewMaintTab() string { } return rows }, - []int{6, 0, 14, 20, 12, 16, 16}, + widths, nil, ) } diff --git a/internal/tui/tab_nodes.go b/internal/tui/tab_nodes.go index a4a049c..6946e6d 100644 --- a/internal/tui/tab_nodes.go +++ b/internal/tui/tab_nodes.go @@ -10,16 +10,24 @@ func (m Model) viewNodesTab() string { 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( - []string{"NAME", "REGION", "LAST SEEN", "VERSION", "STATUS"}, + headers, len(m.nodes), func(start, end int) [][]string { var rows [][]string for i := start; i < end; i++ { node := m.nodes[i] - name := limitStr(node.Name, 20) + name := limitStr(node.Name, nameW-2) if name == "" { name = node.ID } @@ -37,7 +45,7 @@ func (m Model) viewNodesTab() string { } return rows }, - colWidths, + widths, nil, ) } diff --git a/internal/tui/tab_sites.go b/internal/tui/tab_sites.go index 12787e7..fecba98 100644 --- a/internal/tui/tab_sites.go +++ b/internal/tui/tab_sites.go @@ -341,23 +341,16 @@ type tableLayout struct { } func (m Model) computeLayout() tableLayout { - type colDef struct { - short string - full string - minWidth int - maxWidth int - } - cols := []colDef{ - {"#", "#", 4, 4}, - {"", "", 0, 0}, // NAME (dynamic) - {"TYPE", "TYPE", 8, 10}, - {"STATUS", "STATUS", 8, 10}, - {"LAT", "LATENCY", 7, 10}, - {"UP%", "UPTIME", 8, 8}, - {"", "", 0, 0}, // HISTORY (dynamic) - {"SSL", "SSL", 5, 5}, - {"RT", "RETRIES", 5, 9}, + {"#", "#", 4, 4, false}, + {"", "", 0, 0, false}, // NAME (special) + {"TYPE", "TYPE", 8, 10, false}, + {"STATUS", "STATUS", 8, 10, false}, + {"LAT", "LATENCY", 7, 10, false}, + {"UP%", "UPTIME", 8, 8, false}, + {"", "", 0, 0, false}, // HISTORY (special) + {"SSL", "SSL", 5, 5, false}, + {"RT", "RETRIES", 5, 9, false}, } numCols := len(cols) diff --git a/internal/tui/tab_users.go b/internal/tui/tab_users.go index 4793933..10e4108 100644 --- a/internal/tui/tab_users.go +++ b/internal/tui/tab_users.go @@ -32,8 +32,17 @@ func (m Model) viewUsersTab() string { 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( - []string{"#", "USERNAME", "ROLE", "PUBLIC KEY"}, + headers, len(m.users), func(start, end int) [][]string { var rows [][]string @@ -41,14 +50,14 @@ func (m Model) viewUsersTab() string { u := m.users[i] rows = append(rows, []string{ 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), fmtKey(u.PublicKey), }) } return rows }, - nil, nil, + widths, nil, ) } diff --git a/internal/tui/table_helpers.go b/internal/tui/table_helpers.go index ed5397e..371ecee 100644 --- a/internal/tui/table_helpers.go +++ b/internal/tui/table_helpers.go @@ -15,6 +15,79 @@ var ( 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 { if items == 0 { return ""