diff --git a/internal/tui/tab_alerts.go b/internal/tui/tab_alerts.go index 0d203f1..60b1890 100644 --- a/internal/tui/tab_alerts.go +++ b/internal/tui/tab_alerts.go @@ -7,25 +7,6 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/table" -) - -var ( - alertHeaderStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#7D56F4")). - Bold(true). - Padding(0, 1) - - alertCellStyle = lipgloss.NewStyle().Padding(0, 1) - - alertSelectedStyle = lipgloss.NewStyle(). - Padding(0, 1). - Bold(true). - Foreground(lipgloss.Color("#ffffff")). - Background(lipgloss.Color("#3b3b5c")) - - alertBorderStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#444")) ) type alertFormData struct { @@ -97,49 +78,27 @@ func (m Model) viewAlertsTab() string { return "\n No alert channels configured. Press [n] to add one." } - end := m.tableOffset + m.maxTableRows - if end > len(m.alerts) { - end = len(m.alerts) - } - - selectedVisual := m.cursor - m.tableOffset - - var rows [][]string - for i := m.tableOffset; i < end; i++ { - alert := m.alerts[i] - rows = append(rows, []string{ - fmt.Sprintf("%d", i+1), - m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(alert.Name, 15)), - fmtAlertType(alert.Type), - fmtAlertConfig(struct { - Type string - Settings map[string]string - }{alert.Type, alert.Settings}), - }) - } - - tableWidth := m.termWidth - 6 - if tableWidth < 40 { - tableWidth = 40 - } - - t := table.New(). - Border(lipgloss.RoundedBorder()). - BorderStyle(alertBorderStyle). - Width(tableWidth). - Headers("#", "NAME", "TYPE", "CONFIG"). - Rows(rows...). - StyleFunc(func(row, col int) lipgloss.Style { - if row == table.HeaderRow { - return alertHeaderStyle + return m.renderTable( + []string{"#", "NAME", "TYPE", "CONFIG"}, + len(m.alerts), + func(start, end int) [][]string { + var rows [][]string + for i := start; i < end; i++ { + a := m.alerts[i] + rows = append(rows, []string{ + fmt.Sprintf("%d", i+1), + m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, 15)), + fmtAlertType(a.Type), + fmtAlertConfig(struct { + Type string + Settings map[string]string + }{a.Type, a.Settings}), + }) } - if row == selectedVisual { - return alertSelectedStyle - } - return alertCellStyle - }) - - return "\n" + t.Render() + return rows + }, + nil, nil, + ) } func (m *Model) initAlertHuhForm() tea.Cmd { diff --git a/internal/tui/tab_sites.go b/internal/tui/tab_sites.go index 1644a35..e5ebbfe 100644 --- a/internal/tui/tab_sites.go +++ b/internal/tui/tab_sites.go @@ -12,33 +12,14 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/table" ) var sparkChars = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} -var ( - siteHeaderStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#7D56F4")). - Bold(true). - Padding(0, 1) - - siteCellStyle = lipgloss.NewStyle().Padding(0, 1) - - siteSelectedStyle = lipgloss.NewStyle(). - Padding(0, 1). - Bold(true). - Foreground(lipgloss.Color("#ffffff")). - Background(lipgloss.Color("#3b3b5c")) - - siteBorderStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#444")) - - siteGroupStyle = lipgloss.NewStyle(). - Padding(0, 1). - Bold(true). - Foreground(lipgloss.Color("#7D56F4")) -) +var siteGroupStyle = lipgloss.NewStyle(). + Padding(0, 1). + Bold(true). + Foreground(lipgloss.Color("#7D56F4")) type siteFormData struct { Name string @@ -219,111 +200,80 @@ func (m Model) viewSitesTab() string { return "\n No sites configured. Press [n] to add one." } - end := m.tableOffset + m.maxTableRows - if end > len(m.sites) { - end = len(m.sites) - } - - selectedVisual := m.cursor - m.tableOffset - - var rows [][]string - var groupRows []int - for i := m.tableOffset; i < end; i++ { - site := m.sites[i] - - if site.Type == "group" { - groupRows = append(groupRows, i-m.tableOffset) - arrow := "▾" - if m.collapsed[site.ID] { - arrow = "▸" - } - rows = append(rows, []string{ - strconv.Itoa(i + 1), - m.zones.Mark(fmt.Sprintf("site-%d", i), arrow+" "+limitStr(site.Name, 11)), - "group", - fmtStatus(site.Status, site.Paused), - subtleStyle.Render("—"), - subtleStyle.Render("—"), - subtleStyle.Render(strings.Repeat("·", sparkWidth)), - subtleStyle.Render("-"), - subtleStyle.Render("—"), - }) - continue - } - - name := site.Name - if site.ParentID > 0 { - prefix := "├" - if i+1 >= len(m.sites) || m.sites[i+1].ParentID != site.ParentID { - prefix = "└" - } - name = prefix + " " + limitStr(name, 11) - } else { - name = limitStr(name, 13) - } - - hist, _ := monitor.GetHistory(site.ID) - var spark string - if site.Type == "push" { - spark = heartbeatSparkline(hist.Statuses, sparkWidth) - } else { - spark = latencySparkline(hist.Latencies, sparkWidth) - } - - rows = append(rows, []string{ - strconv.Itoa(i + 1), - m.zones.Mark(fmt.Sprintf("site-%d", i), name), - site.Type, - fmtStatus(site.Status, site.Paused), - fmtLatency(site.Latency), - fmtUptime(hist.TotalChecks, hist.UpChecks), - spark, - fmtSSL(site), - fmtRetries(site), - }) - } - - isGroupRow := func(row int) bool { - for _, g := range groupRows { - if g == row { - return true - } - } - return false - } - - tableWidth := m.termWidth - 6 - if tableWidth < 40 { - tableWidth = 40 - } - - // column widths: #=6, name=flex, type=10, status=10, latency=8, uptime=8, history=sparkWidth+4, ssl=7, retry=9 colWidths := []int{6, 0, 10, 10, 8, 8, sparkWidth + 4, 7, 9} - t := table.New(). - Border(lipgloss.RoundedBorder()). - BorderStyle(siteBorderStyle). - Width(tableWidth). - Headers("#", "NAME", "TYPE", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRY"). - Rows(rows...). - StyleFunc(func(row, col int) lipgloss.Style { - var base lipgloss.Style - if row == table.HeaderRow { - base = siteHeaderStyle - } else if row == selectedVisual { - base = siteSelectedStyle - } else if isGroupRow(row) { - base = siteGroupStyle - } else { - base = siteCellStyle - } - if col < len(colWidths) && colWidths[col] > 0 { - base = base.Width(colWidths[col]) - } - return base - }) + var groupRows map[int]bool + return m.renderTable( + []string{"#", "NAME", "TYPE", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRY"}, + len(m.sites), + func(start, end int) [][]string { + groupRows = make(map[int]bool) + var rows [][]string + for i := start; i < end; i++ { + site := m.sites[i] - return "\n" + t.Render() + if site.Type == "group" { + groupRows[i-start] = true + arrow := "▾" + if m.collapsed[site.ID] { + arrow = "▸" + } + rows = append(rows, []string{ + strconv.Itoa(i + 1), + m.zones.Mark(fmt.Sprintf("site-%d", i), arrow+" "+limitStr(site.Name, 11)), + "group", + fmtStatus(site.Status, site.Paused), + subtleStyle.Render("—"), + subtleStyle.Render("—"), + subtleStyle.Render(strings.Repeat("·", sparkWidth)), + subtleStyle.Render("-"), + subtleStyle.Render("—"), + }) + continue + } + + name := site.Name + if site.ParentID > 0 { + prefix := "├" + if i+1 >= len(m.sites) || m.sites[i+1].ParentID != site.ParentID { + prefix = "└" + } + name = prefix + " " + limitStr(name, 11) + } else { + name = limitStr(name, 13) + } + + hist, _ := monitor.GetHistory(site.ID) + var spark string + if site.Type == "push" { + spark = heartbeatSparkline(hist.Statuses, sparkWidth) + } else { + spark = latencySparkline(hist.Latencies, sparkWidth) + } + + rows = append(rows, []string{ + strconv.Itoa(i + 1), + m.zones.Mark(fmt.Sprintf("site-%d", i), name), + site.Type, + fmtStatus(site.Status, site.Paused), + fmtLatency(site.Latency), + fmtUptime(hist.TotalChecks, hist.UpChecks), + spark, + fmtSSL(site), + fmtRetries(site), + }) + } + return rows + }, + colWidths, + func(row, col int) *lipgloss.Style { + if groupRows[row] { + s := siteGroupStyle + return &s + } + return nil + }, + ) } func (m *Model) initSiteHuhForm() tea.Cmd { diff --git a/internal/tui/tab_users.go b/internal/tui/tab_users.go index d82e5fb..46c5679 100644 --- a/internal/tui/tab_users.go +++ b/internal/tui/tab_users.go @@ -6,26 +6,6 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/table" -) - -var ( - userHeaderStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#7D56F4")). - Bold(true). - Padding(0, 1) - - userCellStyle = lipgloss.NewStyle().Padding(0, 1) - - userSelectedStyle = lipgloss.NewStyle(). - Padding(0, 1). - Bold(true). - Foreground(lipgloss.Color("#ffffff")). - Background(lipgloss.Color("#3b3b5c")) - - userBorderStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#444")) ) type userFormData struct { @@ -53,46 +33,24 @@ func (m Model) viewUsersTab() string { return "\n No users configured. Press [n] to add one." } - end := m.tableOffset + m.maxTableRows - if end > len(m.users) { - end = len(m.users) - } - - selectedVisual := m.cursor - m.tableOffset - - var rows [][]string - for i := m.tableOffset; i < end; i++ { - 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)), - fmtRole(u.Role), - fmtKey(u.PublicKey), - }) - } - - tableWidth := m.termWidth - 6 - if tableWidth < 40 { - tableWidth = 40 - } - - t := table.New(). - Border(lipgloss.RoundedBorder()). - BorderStyle(userBorderStyle). - Width(tableWidth). - Headers("#", "USERNAME", "ROLE", "PUBLIC KEY"). - Rows(rows...). - StyleFunc(func(row, col int) lipgloss.Style { - if row == table.HeaderRow { - return userHeaderStyle + return m.renderTable( + []string{"#", "USERNAME", "ROLE", "PUBLIC KEY"}, + len(m.users), + func(start, end int) [][]string { + var rows [][]string + for i := start; i < end; i++ { + 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)), + fmtRole(u.Role), + fmtKey(u.PublicKey), + }) } - if row == selectedVisual { - return userSelectedStyle - } - return userCellStyle - }) - - return "\n" + t.Render() + return rows + }, + nil, nil, + ) } func (m *Model) initUserHuhForm() tea.Cmd { diff --git a/internal/tui/table_helpers.go b/internal/tui/table_helpers.go new file mode 100644 index 0000000..be8719e --- /dev/null +++ b/internal/tui/table_helpers.go @@ -0,0 +1,75 @@ +package tui + +import ( + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/table" +) + +var ( + tableHeaderStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#7D56F4")). + Bold(true). + Padding(0, 1) + + tableCellStyle = lipgloss.NewStyle().Padding(0, 1) + + tableSelectedStyle = lipgloss.NewStyle(). + Padding(0, 1). + Bold(true). + Foreground(lipgloss.Color("#ffffff")). + Background(lipgloss.Color("#3b3b5c")) + + tableBorderStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#444")) +) + +type StyleOverride func(row, col int) *lipgloss.Style + +func (m Model) renderTable(headers []string, items int, buildRows func(start, end int) [][]string, colWidths []int, styleOverride StyleOverride) string { + if items == 0 { + return "" + } + + end := m.tableOffset + m.maxTableRows + if end > items { + end = items + } + + selectedVisual := m.cursor - m.tableOffset + rows := buildRows(m.tableOffset, end) + + tableWidth := m.termWidth - 6 + if tableWidth < 40 { + tableWidth = 40 + } + + t := table.New(). + Border(lipgloss.RoundedBorder()). + BorderStyle(tableBorderStyle). + Width(tableWidth). + Headers(headers...). + Rows(rows...). + StyleFunc(func(row, col int) lipgloss.Style { + if row == table.HeaderRow { + return tableHeaderStyle + } + if styleOverride != nil { + if s := styleOverride(row, col); s != nil { + if col < len(colWidths) && colWidths[col] > 0 { + return s.Width(colWidths[col]) + } + return *s + } + } + base := tableCellStyle + if row == selectedVisual { + base = tableSelectedStyle + } + if col < len(colWidths) && colWidths[col] > 0 { + base = base.Width(colWidths[col]) + } + return base + }) + + return "\n" + t.Render() +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 4324c65..5e0e7b6 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -346,11 +346,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) { - maxTabs := 3 - if !m.isAdmin { - maxTabs = 2 + tabCount := 3 + if m.isAdmin { + tabCount = 4 } - for i := 0; i <= maxTabs; i++ { + for i := 0; i < tabCount; i++ { if m.zones.Get(fmt.Sprintf("tab-%d", i)).InBounds(msg) { m.switchTab(i) return m, nil @@ -477,6 +477,19 @@ func (m *Model) refreshData() { } } m.logViewport.SetContent(strings.Join(monitor.GetLogs(), "\n")) + + listLen := len(m.sites) + if m.currentTab == 1 { + listLen = len(m.alerts) + } else if m.currentTab == 3 { + listLen = len(m.users) + } + if listLen > 0 && m.cursor >= listLen { + m.cursor = listLen - 1 + } + if m.cursor < m.tableOffset { + m.tableOffset = m.cursor + } } func (m *Model) submitForm() {