feat(tui): consolidate tabs 6→3, add log sidebar #145

Merged
lerko merged 5 commits from feat/tab-consolidation into main 2026-06-20 22:32:33 +00:00
10 changed files with 322 additions and 195 deletions
+2 -2
View File
@@ -106,7 +106,7 @@ func (m *Model) refreshLive() {
m.sites = ordered m.sites = ordered
m.refreshLogContent() m.refreshLogContent()
if m.currentTab == 0 && m.selectedID != 0 { if m.currentTab == tabMonitors && m.selectedID != 0 {
for i, s := range m.sites { for i, s := range m.sites {
if s.ID == m.selectedID { if s.ID == m.selectedID {
m.cursor = i m.cursor = i
@@ -118,7 +118,7 @@ func (m *Model) refreshLive() {
} }
func (m *Model) syncSelectedID() { func (m *Model) syncSelectedID() {
if m.currentTab == 0 && m.cursor < len(m.sites) { if m.currentTab == tabMonitors && m.cursor < len(m.sites) {
m.selectedID = m.sites[m.cursor].ID m.selectedID = m.sites[m.cursor].ID
} }
} }
-20
View File
@@ -107,23 +107,3 @@ func (m *Model) refreshLogContent() {
m.logShown = shown m.logShown = shown
m.logViewport.SetContent(strings.Join(rendered, "\n")) m.logViewport.SetContent(strings.Join(rendered, "\n"))
} }
func (m Model) viewLogsTab() string {
if m.logTotal == 0 {
return m.emptyState("No log entries yet.", "Logs appear as monitors run checks")
}
filterLabel := "All"
if m.logFilterImportant {
filterLabel = "Important"
}
header := m.st.subtleStyle.Render(fmt.Sprintf(
" %d entries Filter: %s", m.logShown, filterLabel))
if m.logFilterImportant && m.logShown < m.logTotal {
header += m.st.subtleStyle.Render(fmt.Sprintf(" (%d hidden)", m.logTotal-m.logShown))
}
return "\n" + header + "\n\n" + m.logViewport.View()
}
+75
View File
@@ -0,0 +1,75 @@
package tui
import (
"strings"
"github.com/charmbracelet/lipgloss"
)
func (m Model) renderCompactLogLine(line string, maxW int) string {
sev := classifyLog(line)
var tag string
switch sev {
case severityDown:
tag = m.st.dangerStyle.Render("▼")
case severityUp:
tag = m.st.specialStyle.Render("▲")
case severityWarn:
tag = m.st.warnStyle.Render("◆")
case severitySystem:
tag = m.st.titleStyle.Render("●")
default:
tag = m.st.subtleStyle.Render("·")
}
ts := ""
msg := line
if len(line) > 10 && line[0] == '[' {
if idx := strings.Index(line, "]"); idx > 0 && idx < 12 {
ts = line[1:idx]
msg = strings.TrimSpace(line[idx+1:])
}
}
msg = strings.TrimPrefix(msg, "Monitor ")
msg = strings.TrimPrefix(msg, "Push ")
// prefix: " HH:MM ● " = 9 visible chars, or " ● " = 3 without timestamp
prefixW := 3
if ts != "" {
prefixW = len(ts) + 4
}
msgW := maxW - prefixW
if msgW < 5 {
msgW = 5
}
msg = limitStr(msg, msgW)
if ts != "" {
return " " + m.st.subtleStyle.Render(ts) + " " + tag + " " + msg
}
return " " + tag + " " + msg
}
func (m Model) viewLogsSidebar(width int) string {
logs := m.engine.GetLogs()
if len(logs) == 0 {
return m.st.subtleStyle.Render(" No logs yet")
}
sidebarStyle := lipgloss.NewStyle().Width(width).MaxWidth(width)
var lines []string
for _, line := range logs {
if strings.TrimSpace(line) == "" {
continue
}
if m.logFilterImportant && !isImportantLog(classifyLog(line)) {
continue
}
lines = append(lines, m.renderCompactLogLine(line, width))
}
return "\n" + sidebarStyle.Render(strings.Join(lines, "\n"))
}
+58
View File
@@ -0,0 +1,58 @@
package tui
import (
"github.com/charmbracelet/lipgloss"
)
func (m Model) viewSettingsTab() string {
maxSections := 2
if m.isAdmin {
maxSections = 3
}
sections := []string{"Alerts", "Nodes"}
if m.isAdmin {
sections = append(sections, "Users")
}
_ = maxSections
var tabs []string
for i, name := range sections {
if i == m.settingsSection {
tabs = append(tabs, m.st.activeTab.Render(name))
} else {
tabs = append(tabs, m.st.inactiveTab.Render(name))
}
}
header := lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
var content string
switch m.settingsSection {
case sectionAlerts:
content = m.viewAlertsTab()
case sectionNodes:
content = m.viewNodesTab()
case sectionUsers:
if m.isAdmin {
content = m.viewUsersTab()
}
}
return header + "\n" + content
}
func (m *Model) switchSettingsSection(idx int) {
max := 1
if m.isAdmin {
max = 2
}
if idx > max {
idx = 0
}
if idx < 0 {
idx = max
}
m.settingsSection = idx
m.cursor = 0
m.tableOffset = 0
}
+6 -2
View File
@@ -82,8 +82,12 @@ func (m Model) computeLayout() tableLayout {
var widths []int var widths []int
var fixed int var fixed int
cw := m.contentWidth
if cw == 0 {
cw = m.termWidth
}
for _, c := range siteColumns { for _, c := range siteColumns {
if c.minTerm > 0 && m.termWidth < c.minTerm { if c.minTerm > 0 && cw < c.minTerm {
continue continue
} }
active = append(active, c.key) active = append(active, c.key)
@@ -104,7 +108,7 @@ func (m Model) computeLayout() tableLayout {
numCols := len(headers) numCols := len(headers)
borderOverhead := 2 + (numCols - 1) borderOverhead := 2 + (numCols - 1)
avail := m.termWidth - chromePadH - 2 - borderOverhead - fixed avail := cw - chromePadH - 2 - borderOverhead - fixed
if avail < 20 { if avail < 20 {
avail = 20 avail = 20
} }
+1 -1
View File
@@ -116,7 +116,7 @@ func (m *Model) submitUserForm() tea.Cmd {
st := m.store st := m.store
id := m.editID id := m.editID
username, key, role := d.Username, d.PublicKey, d.Role username, key, role := d.Username, d.PublicKey, d.Role
m.state = stateUsers m.state = stateDashboard
if id > 0 { if id > 0 {
return writeCmd("Update user", func() error { return writeCmd("Update user", func() error {
return st.UpdateUser(context.Background(), id, username, key, role) return st.UpdateUser(context.Background(), id, username, key, role)
+10 -2
View File
@@ -13,7 +13,11 @@ const (
) )
func (m Model) isWide() bool { func (m Model) isWide() bool {
return m.termWidth >= wideBreakpoint w := m.contentWidth
if w == 0 {
w = m.termWidth
}
return w >= wideBreakpoint
} }
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 {
@@ -35,7 +39,11 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
} }
borderOverhead := 2 + len(colWidths) - 1 borderOverhead := 2 + len(colWidths) - 1
tableWidth := colTotal + borderOverhead tableWidth := colTotal + borderOverhead
maxWidth := m.termWidth - chromePadH - 2 cw := m.contentWidth
if cw == 0 {
cw = m.termWidth
}
maxWidth := cw - chromePadH - 2
if tableWidth > maxWidth { if tableWidth > maxWidth {
tableWidth = maxWidth tableWidth = maxWidth
} }
+24 -10
View File
@@ -84,6 +84,18 @@ const (
detailSparkWidth = 40 detailSparkWidth = 40
) )
const (
tabMonitors = 0
tabMaint = 1
tabSettings = 2
)
const (
sectionAlerts = 0
sectionNodes = 1
sectionUsers = 2
)
type sessionState int type sessionState int
const ( const (
@@ -102,16 +114,18 @@ const (
) )
type Model struct { type Model struct {
state sessionState state sessionState
currentTab int currentTab int
cursor int settingsSection int
selectedID int cursor int
tableOffset int selectedID int
maxTableRows int tableOffset int
termWidth int maxTableRows int
termHeight int termWidth int
editID int termHeight int
editToken string contentWidth int
editID int
editToken string
huhForm *huh.Form huhForm *huh.Form
siteFormData *siteFormData siteFormData *siteFormData
+107 -130
View File
@@ -78,31 +78,28 @@ func (m *Model) handleConfirmDelete(msg tea.Msg) (tea.Model, tea.Cmd) {
id := m.deleteID id := m.deleteID
var cmd tea.Cmd var cmd tea.Cmd
switch m.deleteTab { switch m.deleteTab {
case 0: case tabMonitors:
cmd = writeCmd("Delete site", func() error { return st.DeleteSite(context.Background(), id) }) cmd = writeCmd("Delete site", func() error { return st.DeleteSite(context.Background(), id) })
m.engine.RemoveSite(id) m.engine.RemoveSite(id)
m.adjustCursor(len(m.sites) - 1) m.adjustCursor(len(m.sites) - 1)
case 1: case tabMaint:
cmd = writeCmd("Delete alert", func() error { return st.DeleteAlert(context.Background(), id) })
m.adjustCursor(len(m.alerts) - 1)
case 4:
cmd = writeCmd("Delete maintenance window", func() error { return st.DeleteMaintenanceWindow(context.Background(), id) }) cmd = writeCmd("Delete maintenance window", func() error { return st.DeleteMaintenanceWindow(context.Background(), id) })
m.adjustCursor(len(m.maintenanceWindows) - 1) m.adjustCursor(len(m.maintenanceWindows) - 1)
case 5: case tabSettings:
cmd = writeCmd("Delete user", func() error { return st.DeleteUser(context.Background(), id) }) switch m.settingsSection {
m.adjustCursor(len(m.users) - 1) case sectionAlerts:
cmd = writeCmd("Delete alert", func() error { return st.DeleteAlert(context.Background(), id) })
m.adjustCursor(len(m.alerts) - 1)
case sectionUsers:
cmd = writeCmd("Delete user", func() error { return st.DeleteUser(context.Background(), id) })
m.adjustCursor(len(m.users) - 1)
}
} }
m.refreshLive() m.refreshLive()
m.state = stateDashboard m.state = stateDashboard
if m.deleteTab == 5 {
m.state = stateUsers
}
return m, cmd return m, cmd
case "n", "N", "esc": case "n", "N", "esc":
m.state = stateDashboard m.state = stateDashboard
if m.deleteTab == 5 {
m.state = stateUsers
}
case "ctrl+c": case "ctrl+c":
return m, tea.Quit return m, tea.Quit
} }
@@ -117,9 +114,6 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
if keyMsg.String() == "esc" { if keyMsg.String() == "esc" {
m.huhForm = nil m.huhForm = nil
m.state = stateDashboard m.state = stateDashboard
if m.currentTab == 5 {
m.state = stateUsers
}
return m, nil return m, nil
} }
} }
@@ -265,7 +259,7 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
} }
return m, nil return m, nil
} }
if m.state != stateDashboard && m.state != stateLogs && m.state != stateUsers { if m.state != stateDashboard {
return m, nil return m, nil
} }
if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft { if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
@@ -275,15 +269,6 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
if m.state == stateLogs {
if msg.Button == tea.MouseButtonWheelUp {
m.logViewport.ScrollUp(3)
} else {
m.logViewport.ScrollDown(3)
}
return m, nil
}
listLen := m.currentListLen() listLen := m.currentListLen()
if msg.Button == tea.MouseButtonWheelUp { if msg.Button == tea.MouseButtonWheelUp {
if m.cursor > 0 { if m.cursor > 0 {
@@ -325,7 +310,7 @@ func (m *Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m.handleSLAKey(msg) return m.handleSLAKey(msg)
case stateAlertDetail: case stateAlertDetail:
return m.handleAlertDetailKey(msg) return m.handleAlertDetailKey(msg)
case stateDashboard, stateLogs, stateUsers: case stateDashboard:
return m.handleDashboardKey(msg) return m.handleDashboardKey(msg)
} }
return m, nil return m, nil
@@ -521,33 +506,27 @@ func (m *Model) handleAlertDetailKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg.String() { switch msg.String() {
case "q": case "q":
return m, tea.Quit return m, tea.Quit
case "/": case "/":
if m.currentTab == 0 { if m.currentTab == tabMonitors {
m.filterMode = true m.filterMode = true
m.recalcLayout() m.recalcLayout()
return m, nil return m, nil
} }
case "f":
if m.state == stateLogs {
m.logFilterImportant = !m.logFilterImportant
m.refreshLogContent()
return m, nil
}
case "tab": case "tab":
m.switchTab(m.currentTab + 1) m.switchTab(m.currentTab + 1)
case "pgup", "pgdown": case "left", "h":
if m.state == stateLogs { if m.currentTab == tabSettings {
m.logViewport, cmd = m.logViewport.Update(msg) m.switchSettingsSection(m.settingsSection - 1)
return m, cmd }
case "right", "l":
if m.currentTab == tabSettings {
m.switchSettingsSection(m.settingsSection + 1)
} }
case "up", "k": case "up", "k":
if m.state == stateLogs { if m.cursor > 0 {
m.logViewport.ScrollUp(1)
} else if m.cursor > 0 {
m.cursor-- m.cursor--
if m.cursor < m.tableOffset { if m.cursor < m.tableOffset {
m.tableOffset = m.cursor m.tableOffset = m.cursor
@@ -555,29 +534,25 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.syncSelectedID() m.syncSelectedID()
} }
case "down", "j": case "down", "j":
if m.state == stateLogs { max := m.currentListLen() - 1
m.logViewport.ScrollDown(1) if m.cursor < max {
} else { m.cursor++
max := m.currentListLen() - 1 if m.cursor >= m.tableOffset+m.maxTableRows {
if m.cursor < max { m.tableOffset++
m.cursor++
if m.cursor >= m.tableOffset+m.maxTableRows {
m.tableOffset++
}
m.syncSelectedID()
} }
m.syncSelectedID()
} }
case "n": case "n":
return m.handleNewItem() return m.handleNewItem()
case "e", "enter": case "e", "enter":
return m.handleEditItem() return m.handleEditItem()
case "t": case "t":
if m.currentTab == 1 && len(m.alerts) > 0 { if m.currentTab == tabSettings && m.settingsSection == sectionAlerts && len(m.alerts) > 0 {
a := m.alerts[m.cursor] a := m.alerts[m.cursor]
return m, m.testAlertCmd(a.ID, a.Name) return m, m.testAlertCmd(a.ID, a.Name)
} }
case " ": case " ":
if m.currentTab == 0 && len(m.sites) > 0 && m.sites[m.cursor].Type == "group" { if m.currentTab == tabMonitors && len(m.sites) > 0 && m.sites[m.cursor].Type == "group" {
gid := m.sites[m.cursor].ID gid := m.sites[m.cursor].ID
m.collapsed[gid] = !m.collapsed[gid] m.collapsed[gid] = !m.collapsed[gid]
payload := collapsedJSON(m.collapsed) payload := collapsedJSON(m.collapsed)
@@ -588,7 +563,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}) })
} }
case "p": case "p":
if m.currentTab == 0 && len(m.sites) > 0 { if m.currentTab == tabMonitors && len(m.sites) > 0 {
id := m.sites[m.cursor].ID id := m.sites[m.cursor].ID
paused := m.engine.ToggleSitePause(id) paused := m.engine.ToggleSitePause(id)
st := m.store st := m.store
@@ -598,14 +573,14 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}) })
} }
case "i": case "i":
if m.currentTab == 0 && len(m.sites) > 0 { if m.currentTab == tabMonitors && len(m.sites) > 0 {
m.state = stateDetail m.state = stateDetail
return m, m.loadDetailCmd(m.sites[m.cursor].ID) return m, m.loadDetailCmd(m.sites[m.cursor].ID)
} else if m.currentTab == 1 && len(m.alerts) > 0 { } else if m.currentTab == tabSettings && m.settingsSection == sectionAlerts && len(m.alerts) > 0 {
m.state = stateAlertDetail m.state = stateAlertDetail
} }
case "x": case "x":
if m.currentTab == 4 && len(m.maintenanceWindows) > 0 { if m.currentTab == tabMaint && len(m.maintenanceWindows) > 0 {
mw := m.maintenanceWindows[m.cursor] mw := m.maintenanceWindows[m.cursor]
now := time.Now() now := time.Now()
isActive := !mw.StartTime.After(now) && (mw.EndTime.IsZero() || mw.EndTime.After(now)) isActive := !mw.StartTime.After(now) && (mw.EndTime.IsZero() || mw.EndTime.After(now))
@@ -637,19 +612,22 @@ func (m *Model) handleNewItem() (tea.Model, tea.Cmd) {
m.editID = 0 m.editID = 0
m.editToken = "" m.editToken = ""
switch m.currentTab { switch m.currentTab {
case 0: case tabMonitors:
m.state = stateFormSite m.state = stateFormSite
return m, m.initSiteHuhForm() return m, m.initSiteHuhForm()
case 1: case tabMaint:
m.state = stateFormAlert
return m, m.initAlertHuhForm()
case 4:
m.state = stateFormMaint m.state = stateFormMaint
return m, m.initMaintHuhForm() return m, m.initMaintHuhForm()
case 5: case tabSettings:
if m.isAdmin { switch m.settingsSection {
m.state = stateFormUser case sectionAlerts:
return m, m.initUserHuhForm() m.state = stateFormAlert
return m, m.initAlertHuhForm()
case sectionUsers:
if m.isAdmin {
m.state = stateFormUser
return m, m.initUserHuhForm()
}
} }
} }
return m, nil return m, nil
@@ -657,24 +635,27 @@ func (m *Model) handleNewItem() (tea.Model, tea.Cmd) {
func (m *Model) handleEditItem() (tea.Model, tea.Cmd) { func (m *Model) handleEditItem() (tea.Model, tea.Cmd) {
switch m.currentTab { switch m.currentTab {
case 0: case tabMonitors:
if len(m.sites) > 0 { if len(m.sites) > 0 {
m.editID = m.sites[m.cursor].ID m.editID = m.sites[m.cursor].ID
m.editToken = m.sites[m.cursor].Token m.editToken = m.sites[m.cursor].Token
m.state = stateFormSite m.state = stateFormSite
return m, m.initSiteHuhForm() return m, m.initSiteHuhForm()
} }
case 1: case tabSettings:
if len(m.alerts) > 0 { switch m.settingsSection {
m.editID = m.alerts[m.cursor].ID case sectionAlerts:
m.state = stateFormAlert if len(m.alerts) > 0 {
return m, m.initAlertHuhForm() m.editID = m.alerts[m.cursor].ID
} m.state = stateFormAlert
case 5: return m, m.initAlertHuhForm()
if m.isAdmin && len(m.users) > 0 { }
m.editID = m.users[m.cursor].ID case sectionUsers:
m.state = stateFormUser if m.isAdmin && len(m.users) > 0 {
return m, m.initUserHuhForm() m.editID = m.users[m.cursor].ID
m.state = stateFormUser
return m, m.initUserHuhForm()
}
} }
} }
return m, nil return m, nil
@@ -682,43 +663,43 @@ func (m *Model) handleEditItem() (tea.Model, tea.Cmd) {
func (m *Model) handleDeleteItem() (tea.Model, tea.Cmd) { func (m *Model) handleDeleteItem() (tea.Model, tea.Cmd) {
switch m.currentTab { switch m.currentTab {
case 0: case tabMonitors:
if len(m.sites) > 0 { if len(m.sites) > 0 {
m.deleteID = m.sites[m.cursor].ID m.deleteID = m.sites[m.cursor].ID
m.deleteName = m.sites[m.cursor].Name m.deleteName = m.sites[m.cursor].Name
m.deleteTab = 0 m.deleteTab = tabMonitors
m.state = stateConfirmDelete m.state = stateConfirmDelete
} }
case 1: case tabMaint:
if len(m.alerts) > 0 {
m.deleteID = m.alerts[m.cursor].ID
m.deleteName = m.alerts[m.cursor].Name
m.deleteTab = 1
m.state = stateConfirmDelete
}
case 4:
if len(m.maintenanceWindows) > 0 { if len(m.maintenanceWindows) > 0 {
m.deleteID = m.maintenanceWindows[m.cursor].ID m.deleteID = m.maintenanceWindows[m.cursor].ID
m.deleteName = m.maintenanceWindows[m.cursor].Title m.deleteName = m.maintenanceWindows[m.cursor].Title
m.deleteTab = 4 m.deleteTab = tabMaint
m.state = stateConfirmDelete m.state = stateConfirmDelete
} }
case 5: case tabSettings:
if m.isAdmin && len(m.users) > 0 { switch m.settingsSection {
m.deleteID = m.users[m.cursor].ID case sectionAlerts:
m.deleteName = m.users[m.cursor].Username if len(m.alerts) > 0 {
m.deleteTab = 5 m.deleteID = m.alerts[m.cursor].ID
m.state = stateConfirmDelete m.deleteName = m.alerts[m.cursor].Name
m.deleteTab = tabSettings
m.state = stateConfirmDelete
}
case sectionUsers:
if m.isAdmin && len(m.users) > 0 {
m.deleteID = m.users[m.cursor].ID
m.deleteName = m.users[m.cursor].Username
m.deleteTab = tabSettings
m.state = stateConfirmDelete
}
} }
} }
return m, nil return m, nil
} }
func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) { func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
tabCount := 5 tabCount := tabSettings + 1
if m.isAdmin {
tabCount = 6
}
for i := 0; i < tabCount; i++ { for i := 0; i < tabCount; i++ {
if m.zones.Get(fmt.Sprintf("tab-%d", i)).InBounds(msg) { if m.zones.Get(fmt.Sprintf("tab-%d", i)).InBounds(msg) {
m.switchTab(i) m.switchTab(i)
@@ -743,24 +724,14 @@ func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
} }
func (m *Model) switchTab(idx int) { func (m *Model) switchTab(idx int) {
maxTabs := 4 maxTabs := tabSettings
if m.isAdmin {
maxTabs = 5
}
if idx > maxTabs { if idx > maxTabs {
idx = 0 idx = 0
} }
m.currentTab = idx m.currentTab = idx
m.cursor = 0 m.cursor = 0
m.tableOffset = 0 m.tableOffset = 0
switch idx { m.state = stateDashboard
case 2:
m.state = stateLogs
case 5:
m.state = stateUsers
default:
m.state = stateDashboard
}
} }
func (m *Model) adjustCursor(_ int) { func (m *Model) adjustCursor(_ int) {
@@ -791,30 +762,36 @@ func (m *Model) submitForm() tea.Cmd {
func (m Model) currentListLen() int { func (m Model) currentListLen() int {
switch m.currentTab { switch m.currentTab {
case 1: case tabMonitors:
return len(m.alerts)
case 3:
return len(m.nodes)
case 4:
return len(m.maintenanceWindows)
case 5:
return len(m.users)
default:
return len(m.sites) return len(m.sites)
case tabMaint:
return len(m.maintenanceWindows)
case tabSettings:
switch m.settingsSection {
case sectionAlerts:
return len(m.alerts)
case sectionNodes:
return len(m.nodes)
case sectionUsers:
return len(m.users)
}
} }
return 0
} }
func (m Model) currentZonePrefix() (string, int) { func (m Model) currentZonePrefix() (string, int) {
switch m.currentTab { switch m.currentTab {
case 0: case tabMonitors:
return "site", len(m.sites) return "site", len(m.sites)
case 1: case tabMaint:
return "alert", len(m.alerts)
case 4:
return "maint", len(m.maintenanceWindows) return "maint", len(m.maintenanceWindows)
case 5: case tabSettings:
return "user", len(m.users) switch m.settingsSection {
default: case sectionAlerts:
return "site", 0 return "alert", len(m.alerts)
case sectionUsers:
return "user", len(m.users)
}
} }
return "site", 0
} }
+39 -28
View File
@@ -151,20 +151,28 @@ func (m Model) viewDashboard() string {
var content string var content string
switch m.currentTab { switch m.currentTab {
case 0: case tabMonitors:
content = m.viewSitesTab() showSidebar := m.termWidth >= wideBreakpoint
case 1: if showSidebar {
content = m.viewAlertsTab() availW := m.termWidth - chromePadH
case 2: leftW := availW * 70 / 100
content = m.viewLogsTab() rightW := availW - leftW
case 3: m.contentWidth = leftW
content = m.viewNodesTab() monitors := m.viewSitesTab()
case 4: left := lipgloss.NewStyle().Width(leftW).Render(monitors)
content = m.viewMaintTab() sidebar := m.viewLogsSidebar(rightW)
case 5: right := lipgloss.NewStyle().Width(rightW).Render(sidebar)
if m.isAdmin { content = lipgloss.JoinHorizontal(lipgloss.Top, left, right)
content = m.viewUsersTab() } else {
m.contentWidth = m.termWidth
content = m.viewSitesTab()
} }
case tabMaint:
m.contentWidth = m.termWidth
content = m.viewMaintTab()
case tabSettings:
m.contentWidth = m.termWidth
content = m.viewSettingsTab()
} }
content = strings.TrimSpace(content) content = strings.TrimSpace(content)
@@ -199,15 +207,15 @@ type tabEntry struct {
} }
func (m Model) renderTabBar(stats dashboardStats) string { func (m Model) renderTabBar(stats dashboardStats) string {
settingsCount := len(m.alerts) + len(m.nodes)
settingsWarn := stats.offlineNodes
if m.isAdmin {
settingsCount += len(m.users)
}
tabs := []tabEntry{ tabs := []tabEntry{
{"Monitors", stats.totalMonitors, stats.downCount + stats.lateCount}, {"Monitors", stats.totalMonitors, stats.downCount + stats.lateCount},
{"Alerts", len(m.alerts), 0},
{"Logs", 0, 0},
{"Nodes", len(m.nodes), stats.offlineNodes},
{"Maint", len(m.maintenanceWindows), stats.activeMaint}, {"Maint", len(m.maintenanceWindows), stats.activeMaint},
} {"Settings", settingsCount, settingsWarn},
if m.isAdmin {
tabs = append(tabs, tabEntry{"Users", len(m.users), 0})
} }
countStyle := lipgloss.NewStyle().Foreground(m.theme.Muted) countStyle := lipgloss.NewStyle().Foreground(m.theme.Muted)
@@ -270,23 +278,26 @@ func (m Model) renderFooter(stats dashboardStats) string {
var keys string var keys string
switch m.currentTab { switch m.currentTab {
case 0: case tabMonitors:
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [Space]Collapse [T]Theme [Tab]Switch [q]Quit" keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [Space]Collapse [T]Theme [Tab]Switch [q]Quit"
case 1: case tabMaint:
keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit"
case 2:
keys = "[↑/↓]Scroll [PgUp/PgDn]Page [f]Filter [T]Theme [Tab]Switch [q]Quit"
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 tabSettings:
keys = "[n]Add [d]Revoke [T]Theme [Tab]Switch [q]Quit" switch m.settingsSection {
case sectionAlerts:
keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [←/→]Section [T]Theme [Tab]Switch [q]Quit"
case sectionUsers:
keys = "[n]Add [d]Revoke [←/→]Section [T]Theme [Tab]Switch [q]Quit"
default:
keys = "[←/→]Section [T]Theme [Tab]Switch [q]Quit"
}
default: default:
keys = "[T]Theme [Tab]Switch [q]Quit" keys = "[T]Theme [Tab]Switch [q]Quit"
} }
ver := m.st.subtleStyle.Render("v" + m.version) ver := m.st.subtleStyle.Render("v" + m.version)
line := statusLine + " " + m.st.subtleStyle.Render(keys) + " " + ver line := statusLine + " " + m.st.subtleStyle.Render(keys) + " " + ver
if m.filterText != "" && m.currentTab == 0 { if m.filterText != "" && m.currentTab == tabMonitors {
line = m.st.subtleStyle.Render(fmt.Sprintf("filter: %s", m.filterText)) + " " + statusLine + " " + m.st.subtleStyle.Render(keys) + " " + ver line = m.st.subtleStyle.Render(fmt.Sprintf("filter: %s", m.filterText)) + " " + statusLine + " " + m.st.subtleStyle.Render(keys) + " " + ver
} }