refactor(tui): consistent chrome across all views
CI / test (pull_request) Successful in 2m36s
CI / lint (pull_request) Successful in 56s
CI / vulncheck (pull_request) Successful in 51s

- Extract divider() and emptyState() helpers to format.go
- All empty states now use bordered box with accent color
- Detail and alert detail panels get header/section dividers
- SLA label width 14→16 to match detail/alert panels
- Logs key hints moved from content to dashboard footer
- History/SLA panels use shared divider helper
This commit is contained in:
2026-06-04 15:08:29 -04:00
parent e0cb0adebd
commit ba75be194d
11 changed files with 60 additions and 48 deletions
+26
View File
@@ -2,11 +2,37 @@ package tui
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models" "gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
"github.com/charmbracelet/lipgloss"
) )
func (m Model) dividerWidth() int {
w := m.termWidth - chromePadH - 4
if w < 40 {
w = 40
}
return w
}
func (m Model) divider() string {
return " " + subtleStyle.Render(strings.Repeat("─", m.dividerWidth()))
}
func (m Model) emptyState(message, hint string) string {
content := message
if hint != "" {
content += "\n\n" + subtleStyle.Render(hint)
}
return "\n" + lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(m.theme.Accent).
Padding(1, 3).
Render(content)
}
func limitStr(text string, max int) string { func limitStr(text string, max int) string {
runes := []rune(text) runes := []rune(text)
if len(runes) > max { if len(runes) > max {
+6 -4
View File
@@ -164,7 +164,7 @@ func fmtAlertLastSent(h monitor.AlertHealth) string {
func (m Model) viewAlertsTab() string { func (m Model) viewAlertsTab() string {
if len(m.alerts) == 0 { if len(m.alerts) == 0 {
return "\n No alert channels configured. Press [n] to add one." return m.emptyState("No alert channels configured.", "[n] Add your first alert")
} }
var headers []string var headers []string
@@ -214,7 +214,8 @@ func (m Model) viewAlertDetailPanel() string {
var b strings.Builder var b strings.Builder
b.WriteString(subtleStyle.Render(" Alerts > ") + titleStyle.Render(a.Name) + "\n\n") b.WriteString(subtleStyle.Render(" Alerts > ") + titleStyle.Render(a.Name) + "\n")
b.WriteString(m.divider() + "\n")
row := func(label, value string) { row := func(label, value string) {
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value) fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
@@ -240,12 +241,13 @@ func (m Model) viewAlertDetailPanel() string {
row("Last Error", dangerStyle.Render(limitStr(h.LastError, 60))) row("Last Error", dangerStyle.Render(limitStr(h.LastError, 60)))
} }
b.WriteString("\n" + subtleStyle.Render(" CONFIGURATION") + "\n") b.WriteString(m.divider() + "\n")
b.WriteString(subtleStyle.Render(" CONFIGURATION") + "\n")
for k, v := range a.Settings { for k, v := range a.Settings {
row(k, v) row(k, v)
} }
b.WriteString("\n\n") b.WriteString(m.divider() + "\n")
b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [t] Test [q] Quit")) b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [t] Test [q] Quit"))
return lipgloss.NewStyle().Padding(1, 2).Render(b.String()) return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
+2 -2
View File
@@ -85,7 +85,7 @@ func renderLogLine(line string) string {
func (m Model) viewLogsTab() string { func (m Model) viewLogsTab() string {
content := m.logViewport.View() content := m.logViewport.View()
if strings.TrimSpace(content) == "" || content == "Waiting for logs..." { if strings.TrimSpace(content) == "" || content == "Waiting for logs..." {
return "\n No log entries yet. Logs appear as monitors run checks." return m.emptyState("No log entries yet.", "Logs appear as monitors run checks")
} }
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")
@@ -112,7 +112,7 @@ func (m Model) viewLogsTab() string {
} }
header := subtleStyle.Render(fmt.Sprintf( header := subtleStyle.Render(fmt.Sprintf(
" %d entries [↑/↓] Scroll [PgUp/PgDn] Page [f] Filter: %s", shown, filterLabel)) " %d entries Filter: %s", shown, filterLabel))
if m.logFilterImportant && shown < total { if m.logFilterImportant && shown < total {
header += subtleStyle.Render(fmt.Sprintf(" (%d hidden)", total-shown)) header += subtleStyle.Render(fmt.Sprintf(" (%d hidden)", total-shown))
+1 -1
View File
@@ -93,7 +93,7 @@ func (m Model) isMonitorInMaintenance(monitorID int) bool {
func (m Model) viewMaintTab() string { func (m Model) viewMaintTab() string {
if len(m.maintenanceWindows) == 0 { if len(m.maintenanceWindows) == 0 {
return "\n No maintenance windows or incidents. Press [n] to create one." return m.emptyState("No maintenance windows or incidents.", "[n] Create one")
} }
var headers []string var headers []string
+1 -1
View File
@@ -7,7 +7,7 @@ import (
func (m Model) viewNodesTab() string { func (m Model) viewNodesTab() string {
if len(m.nodes) == 0 { if len(m.nodes) == 0 {
return "\n No probe nodes connected." return m.emptyState("No probe nodes connected.", "")
} }
var headers []string var headers []string
+1 -10
View File
@@ -101,16 +101,7 @@ func (m Model) computeLayout() tableLayout {
func (m Model) viewSitesTab() string { func (m Model) viewSitesTab() string {
if len(m.sites) == 0 { if len(m.sites) == 0 {
welcome := lipgloss.NewStyle(). return m.emptyState(titleStyle.Render("uptop")+"\n\nNo monitors configured yet.", "[n] Add your first monitor")
Border(lipgloss.RoundedBorder()).
BorderForeground(m.theme.Accent).
Padding(1, 3).
Render(
titleStyle.Render("uptop") + "\n\n" +
"No monitors configured yet.\n\n" +
subtleStyle.Render("[n] Add your first monitor"),
)
return "\n" + welcome
} }
layout := m.computeLayout() layout := m.computeLayout()
+1 -1
View File
@@ -29,7 +29,7 @@ func fmtKey(key string) string {
func (m Model) viewUsersTab() string { func (m Model) viewUsersTab() string {
if len(m.users) == 0 { if len(m.users) == 0 {
return "\n No users configured. Press [n] to add one." return m.emptyState("No users configured.", "[n] Add a user")
} }
var headers []string var headers []string
+1 -1
View File
@@ -266,7 +266,7 @@ func (m Model) renderFooter(stats dashboardStats) string {
case 1: case 1:
keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit" keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit"
case 2: case 2:
keys = "[f]Filter [T]Theme [Tab]Switch [q]Quit" keys = "[↑/↓]Scroll [PgUp/PgDn]Page [f]Filter [T]Theme [Tab]Switch [q]Quit"
case 4: case 4:
keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit" keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit"
case 5: case 5:
+5 -3
View File
@@ -30,7 +30,8 @@ func (m Model) viewDetailPanel() string {
if breadcrumb == "" { if breadcrumb == "" {
breadcrumb = subtleStyle.Render(" Sites > ") + titleStyle.Render(site.Name) breadcrumb = subtleStyle.Render(" Sites > ") + titleStyle.Render(site.Name)
} }
b.WriteString(breadcrumb + "\n\n") b.WriteString(breadcrumb + "\n")
b.WriteString(m.divider() + "\n")
row := func(label, value string) { row := func(label, value string) {
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value) fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
@@ -200,7 +201,7 @@ func (m Model) viewDetailPanel() string {
b.WriteString(" " + subtleStyle.Render("[h] History") + "\n") b.WriteString(" " + subtleStyle.Render("[h] History") + "\n")
} }
b.WriteString("\n") b.WriteString(m.divider() + "\n")
const sparkWidth = 40 const sparkWidth = 40
if site.Type == "push" { if site.Type == "push" {
b.WriteString(" " + heartbeatSparkline(hist.Statuses, sparkWidth)) b.WriteString(" " + heartbeatSparkline(hist.Statuses, sparkWidth))
@@ -242,7 +243,8 @@ func (m Model) viewDetailPanel() string {
} }
} }
b.WriteString("\n\n") b.WriteString("\n")
b.WriteString(m.divider() + "\n")
b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [h] History [s] SLA [q] Quit")) b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [h] History [s] SLA [q] Quit"))
return lipgloss.NewStyle().Padding(1, 2).Render(b.String()) return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
+4 -7
View File
@@ -153,16 +153,13 @@ func (m Model) viewHistoryPanel() string {
header += " " + subtleStyle.Render("[q] Back") header += " " + subtleStyle.Render("[q] Back")
b.WriteString(header + "\n") b.WriteString(header + "\n")
divWidth := m.termWidth - chromePadH - 4 divWidth := m.dividerWidth()
if divWidth < 40 { b.WriteString(m.divider() + "\n")
divWidth = 40
}
b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
sparkline := stateChangeSparkline(m.historyChanges, divWidth) sparkline := stateChangeSparkline(m.historyChanges, divWidth)
if sparkline != "" { if sparkline != "" {
b.WriteString(" " + sparkline + "\n") b.WriteString(" " + sparkline + "\n")
b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n") b.WriteString(m.divider() + "\n")
} }
fmt.Fprintf(&b, " %-18s %-17s %-12s %s\n", fmt.Fprintf(&b, " %-18s %-17s %-12s %s\n",
@@ -177,7 +174,7 @@ func (m Model) viewHistoryPanel() string {
b.WriteString(m.historyViewport.View()) b.WriteString(m.historyViewport.View())
} }
b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n") b.WriteString("\n" + m.divider() + "\n")
stats := computeHistoryStats(m.historyChanges) stats := computeHistoryStats(m.historyChanges)
parts := []string{fmt.Sprintf("%d events", stats.totalEvents)} parts := []string{fmt.Sprintf("%d events", stats.totalEvents)}
+12 -18
View File
@@ -27,20 +27,14 @@ func (m Model) viewSLAPanel() string {
header := " " + titleStyle.Render("SLA REPORT: "+m.slaSiteName) header := " " + titleStyle.Render("SLA REPORT: "+m.slaSiteName)
header += " " + subtleStyle.Render("[q] Back") header += " " + subtleStyle.Render("[q] Back")
b.WriteString(header + "\n") b.WriteString(header + "\n")
b.WriteString(m.divider() + "\n")
divWidth := m.termWidth - chromePadH - 4
if divWidth < 40 {
divWidth = 40
}
b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
period := slaPeriods[m.slaPeriodIdx] period := slaPeriods[m.slaPeriodIdx]
b.WriteString(" " + subtleStyle.Render("Period: Last "+period.label) + "\n\n") b.WriteString(" " + subtleStyle.Render("Period: Last "+period.label) + "\n\n")
r := m.slaReport r := m.slaReport
// Uptime bar barWidth := m.dividerWidth() - 30
barWidth := divWidth - 30
if barWidth < 10 { if barWidth < 10 {
barWidth = 10 barWidth = 10
} }
@@ -52,23 +46,23 @@ func (m Model) viewSLAPanel() string {
if r.UptimePct < 99.0 { if r.UptimePct < 99.0 {
uptimeColor = dangerStyle uptimeColor = dangerStyle
} }
fmt.Fprintf(&b, " %-14s %s %s\n", subtleStyle.Render("Uptime"), uptimeColor.Render(fmt.Sprintf("%s%%", fmtPct(r.UptimePct))), bar) fmt.Fprintf(&b, " %-16s %s %s\n", subtleStyle.Render("Uptime"), uptimeColor.Render(fmt.Sprintf("%s%%", fmtPct(r.UptimePct))), bar)
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("Downtime"), fmtDuration(r.Downtime)) fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("Downtime"), fmtDuration(r.Downtime))
fmt.Fprintf(&b, " %-14s %d\n", subtleStyle.Render("Outages"), r.OutageCount) fmt.Fprintf(&b, " %-16s %d\n", subtleStyle.Render("Outages"), r.OutageCount)
if r.OutageCount > 0 { if r.OutageCount > 0 {
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("Longest"), fmtDuration(r.LongestOut)) fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("Longest"), fmtDuration(r.LongestOut))
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("MTTR"), fmtDuration(r.MTTR)) fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("MTTR"), fmtDuration(r.MTTR))
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("MTBF"), fmtDuration(r.MTBF)) fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("MTBF"), fmtDuration(r.MTBF))
} }
b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n") b.WriteString("\n" + m.divider() + "\n")
if len(m.slaDailyBreakdown) > 0 { if len(m.slaDailyBreakdown) > 0 {
b.WriteString(m.slaViewport.View()) b.WriteString(m.slaViewport.View())
} }
b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n") b.WriteString("\n" + m.divider() + "\n")
var keys []string var keys []string
for i, p := range slaPeriods { for i, p := range slaPeriods {
@@ -80,7 +74,7 @@ func (m Model) viewSLAPanel() string {
} }
} }
b.WriteString(" " + strings.Join(keys, " ")) b.WriteString(" " + strings.Join(keys, " "))
b.WriteString(" " + subtleStyle.Render("[j/k/↑/↓] Scroll")) b.WriteString(" " + subtleStyle.Render("[j/k/↑/↓] Scroll [q/Esc] Back"))
return lipgloss.NewStyle().Padding(1, 2).Render(b.String()) return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
} }
@@ -88,7 +82,7 @@ func (m Model) viewSLAPanel() string {
func (m Model) buildSLADailyContent() string { func (m Model) buildSLADailyContent() string {
var b strings.Builder var b strings.Builder
barWidth := m.termWidth - chromePadH - 30 barWidth := m.dividerWidth() - 30
if barWidth < 10 { if barWidth < 10 {
barWidth = 10 barWidth = 10
} }