refactor(tui): extract shared table rendering, fix cursor bounds
- New table_helpers.go with renderTable() and shared styles - Remove 4 duplicated style blocks (header/cell/selected/border) from tab_alerts.go and tab_users.go - All 3 tab views now use renderTable() for offset/end calc, selected row highlighting, and table construction - Sites tab keeps siteGroupStyle via StyleOverride callback - Clamp cursor to list length at end of refreshData() to prevent index-out-of-bounds after concurrent list changes - Fix off-by-one in tab click handler (i <= maxTabs → i < tabCount)
This commit is contained in:
+13
-54
@@ -7,25 +7,6 @@ import (
|
|||||||
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"
|
||||||
"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 {
|
type alertFormData struct {
|
||||||
@@ -97,49 +78,27 @@ 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."
|
||||||
}
|
}
|
||||||
|
|
||||||
end := m.tableOffset + m.maxTableRows
|
return m.renderTable(
|
||||||
if end > len(m.alerts) {
|
[]string{"#", "NAME", "TYPE", "CONFIG"},
|
||||||
end = len(m.alerts)
|
len(m.alerts),
|
||||||
}
|
func(start, end int) [][]string {
|
||||||
|
|
||||||
selectedVisual := m.cursor - m.tableOffset
|
|
||||||
|
|
||||||
var rows [][]string
|
var rows [][]string
|
||||||
for i := m.tableOffset; i < end; i++ {
|
for i := start; i < end; i++ {
|
||||||
alert := m.alerts[i]
|
a := m.alerts[i]
|
||||||
rows = append(rows, []string{
|
rows = append(rows, []string{
|
||||||
fmt.Sprintf("%d", i+1),
|
fmt.Sprintf("%d", i+1),
|
||||||
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(alert.Name, 15)),
|
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, 15)),
|
||||||
fmtAlertType(alert.Type),
|
fmtAlertType(a.Type),
|
||||||
fmtAlertConfig(struct {
|
fmtAlertConfig(struct {
|
||||||
Type string
|
Type string
|
||||||
Settings map[string]string
|
Settings map[string]string
|
||||||
}{alert.Type, alert.Settings}),
|
}{a.Type, a.Settings}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return rows
|
||||||
tableWidth := m.termWidth - 6
|
},
|
||||||
if tableWidth < 40 {
|
nil, nil,
|
||||||
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
|
|
||||||
}
|
|
||||||
if row == selectedVisual {
|
|
||||||
return alertSelectedStyle
|
|
||||||
}
|
|
||||||
return alertCellStyle
|
|
||||||
})
|
|
||||||
|
|
||||||
return "\n" + t.Render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) initAlertHuhForm() tea.Cmd {
|
func (m *Model) initAlertHuhForm() tea.Cmd {
|
||||||
|
|||||||
+20
-70
@@ -12,33 +12,14 @@ import (
|
|||||||
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"
|
||||||
"github.com/charmbracelet/lipgloss/table"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var sparkChars = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
var sparkChars = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||||
|
|
||||||
var (
|
var siteGroupStyle = lipgloss.NewStyle().
|
||||||
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).
|
Padding(0, 1).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#7D56F4"))
|
Foreground(lipgloss.Color("#7D56F4"))
|
||||||
)
|
|
||||||
|
|
||||||
type siteFormData struct {
|
type siteFormData struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -219,20 +200,20 @@ func (m Model) viewSitesTab() string {
|
|||||||
return "\n No sites configured. Press [n] to add one."
|
return "\n No sites configured. Press [n] to add one."
|
||||||
}
|
}
|
||||||
|
|
||||||
end := m.tableOffset + m.maxTableRows
|
colWidths := []int{6, 0, 10, 10, 8, 8, sparkWidth + 4, 7, 9}
|
||||||
if end > len(m.sites) {
|
|
||||||
end = len(m.sites)
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedVisual := m.cursor - m.tableOffset
|
|
||||||
|
|
||||||
|
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
|
var rows [][]string
|
||||||
var groupRows []int
|
for i := start; i < end; i++ {
|
||||||
for i := m.tableOffset; i < end; i++ {
|
|
||||||
site := m.sites[i]
|
site := m.sites[i]
|
||||||
|
|
||||||
if site.Type == "group" {
|
if site.Type == "group" {
|
||||||
groupRows = append(groupRows, i-m.tableOffset)
|
groupRows[i-start] = true
|
||||||
arrow := "▾"
|
arrow := "▾"
|
||||||
if m.collapsed[site.ID] {
|
if m.collapsed[site.ID] {
|
||||||
arrow = "▸"
|
arrow = "▸"
|
||||||
@@ -282,48 +263,17 @@ func (m Model) viewSitesTab() string {
|
|||||||
fmtRetries(site),
|
fmtRetries(site),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return rows
|
||||||
isGroupRow := func(row int) bool {
|
},
|
||||||
for _, g := range groupRows {
|
colWidths,
|
||||||
if g == row {
|
func(row, col int) *lipgloss.Style {
|
||||||
return true
|
if groupRows[row] {
|
||||||
|
s := siteGroupStyle
|
||||||
|
return &s
|
||||||
}
|
}
|
||||||
}
|
return nil
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
return "\n" + t.Render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) initSiteHuhForm() tea.Cmd {
|
func (m *Model) initSiteHuhForm() tea.Cmd {
|
||||||
|
|||||||
@@ -6,26 +6,6 @@ import (
|
|||||||
|
|
||||||
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/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 {
|
type userFormData struct {
|
||||||
@@ -53,15 +33,12 @@ 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."
|
||||||
}
|
}
|
||||||
|
|
||||||
end := m.tableOffset + m.maxTableRows
|
return m.renderTable(
|
||||||
if end > len(m.users) {
|
[]string{"#", "USERNAME", "ROLE", "PUBLIC KEY"},
|
||||||
end = len(m.users)
|
len(m.users),
|
||||||
}
|
func(start, end int) [][]string {
|
||||||
|
|
||||||
selectedVisual := m.cursor - m.tableOffset
|
|
||||||
|
|
||||||
var rows [][]string
|
var rows [][]string
|
||||||
for i := m.tableOffset; i < end; i++ {
|
for i := start; i < end; i++ {
|
||||||
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),
|
||||||
@@ -70,29 +47,10 @@ func (m Model) viewUsersTab() string {
|
|||||||
fmtKey(u.PublicKey),
|
fmtKey(u.PublicKey),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return rows
|
||||||
tableWidth := m.termWidth - 6
|
},
|
||||||
if tableWidth < 40 {
|
nil, nil,
|
||||||
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
|
|
||||||
}
|
|
||||||
if row == selectedVisual {
|
|
||||||
return userSelectedStyle
|
|
||||||
}
|
|
||||||
return userCellStyle
|
|
||||||
})
|
|
||||||
|
|
||||||
return "\n" + t.Render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) initUserHuhForm() tea.Cmd {
|
func (m *Model) initUserHuhForm() tea.Cmd {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
+17
-4
@@ -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) {
|
func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||||
maxTabs := 3
|
tabCount := 3
|
||||||
if !m.isAdmin {
|
if m.isAdmin {
|
||||||
maxTabs = 2
|
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) {
|
if m.zones.Get(fmt.Sprintf("tab-%d", i)).InBounds(msg) {
|
||||||
m.switchTab(i)
|
m.switchTab(i)
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -477,6 +477,19 @@ func (m *Model) refreshData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.logViewport.SetContent(strings.Join(monitor.GetLogs(), "\n"))
|
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() {
|
func (m *Model) submitForm() {
|
||||||
|
|||||||
Reference in New Issue
Block a user