5d362fdbe6
tui.go (1032→164) and tab_sites.go (993→482) violated "small functions" and "testable in isolation" standards. Extracted 6 new files by concern: - format.go: pure formatting functions (fmtLatency, fmtUptime, etc.) - sparkline.go: sparkline rendering (latency, heartbeat, group) - update.go: Update method decomposed into 15 named handlers - view_dashboard.go: View, dashboard composition, tab bar, footer - view_detail.go: site detail panel - data.go: data refresh with extracted sortSitesForDisplay/filterSites Added 17 unit tests for the newly-testable pure functions covering format, sparkline, sort ordering, and filter logic. No behavioral changes — strict move-and-extract refactor.
125 lines
3.0 KiB
Go
125 lines
3.0 KiB
Go
package tui
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store"
|
|
)
|
|
|
|
func loadCollapsed(s store.Store) map[int]bool {
|
|
m := make(map[int]bool)
|
|
raw, err := s.GetPreference("collapsed_groups")
|
|
if err != nil || raw == "" {
|
|
return m
|
|
}
|
|
var ids []int
|
|
if err := json.Unmarshal([]byte(raw), &ids); err != nil {
|
|
return m
|
|
}
|
|
for _, id := range ids {
|
|
m[id] = true
|
|
}
|
|
return m
|
|
}
|
|
|
|
func saveCollapsed(s store.Store, collapsed map[int]bool) {
|
|
var ids []int
|
|
for id, v := range collapsed {
|
|
if v {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
data, _ := json.Marshal(ids)
|
|
_ = s.SetPreference("collapsed_groups", string(data))
|
|
}
|
|
|
|
func sortSitesForDisplay(allSites []models.Site, collapsed map[int]bool) []models.Site {
|
|
var groups, ungrouped []models.Site
|
|
children := make(map[int][]models.Site)
|
|
for _, s := range allSites {
|
|
if s.Type == "group" {
|
|
groups = append(groups, s)
|
|
} else if s.ParentID > 0 {
|
|
children[s.ParentID] = append(children[s.ParentID], s)
|
|
} else {
|
|
ungrouped = append(ungrouped, s)
|
|
}
|
|
}
|
|
sort.Slice(groups, func(i, j int) bool { return groups[i].ID < groups[j].ID })
|
|
for pid := range children {
|
|
c := children[pid]
|
|
sort.Slice(c, func(i, j int) bool { return c[i].ID < c[j].ID })
|
|
sort.SliceStable(c, func(i, j int) bool { return siteOrder(c[i]) < siteOrder(c[j]) })
|
|
children[pid] = c
|
|
}
|
|
sort.Slice(ungrouped, func(i, j int) bool { return ungrouped[i].ID < ungrouped[j].ID })
|
|
sort.SliceStable(ungrouped, func(i, j int) bool { return siteOrder(ungrouped[i]) < siteOrder(ungrouped[j]) })
|
|
|
|
var ordered []models.Site
|
|
for _, g := range groups {
|
|
ordered = append(ordered, g)
|
|
if !collapsed[g.ID] {
|
|
ordered = append(ordered, children[g.ID]...)
|
|
}
|
|
}
|
|
ordered = append(ordered, ungrouped...)
|
|
return ordered
|
|
}
|
|
|
|
func filterSites(sites []models.Site, needle string) []models.Site {
|
|
lower := strings.ToLower(needle)
|
|
var filtered []models.Site
|
|
for _, s := range sites {
|
|
if strings.Contains(strings.ToLower(s.Name), lower) {
|
|
filtered = append(filtered, s)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func (m *Model) refreshData() {
|
|
allSites := m.engine.GetAllSites()
|
|
ordered := sortSitesForDisplay(allSites, m.collapsed)
|
|
if m.filterText != "" {
|
|
ordered = filterSites(ordered, m.filterText)
|
|
}
|
|
m.sites = ordered
|
|
|
|
if alerts, err := m.store.GetAllAlerts(); err == nil {
|
|
m.alerts = alerts
|
|
}
|
|
if m.isAdmin {
|
|
if users, err := m.store.GetAllUsers(); err == nil {
|
|
m.users = users
|
|
}
|
|
}
|
|
if nodes, err := m.store.GetAllNodes(); err == nil {
|
|
m.nodes = nodes
|
|
}
|
|
if windows, err := m.store.GetAllMaintenanceWindows(100); err == nil {
|
|
m.maintenanceWindows = windows
|
|
}
|
|
m.logViewport.SetContent(strings.Join(m.engine.GetLogs(), "\n"))
|
|
|
|
listLen := len(m.sites)
|
|
switch m.currentTab {
|
|
case 1:
|
|
listLen = len(m.alerts)
|
|
case 3:
|
|
listLen = len(m.nodes)
|
|
case 4:
|
|
listLen = len(m.maintenanceWindows)
|
|
case 5:
|
|
listLen = len(m.users)
|
|
}
|
|
if listLen > 0 && m.cursor >= listLen {
|
|
m.cursor = listLen - 1
|
|
}
|
|
if m.cursor < m.tableOffset {
|
|
m.tableOffset = m.cursor
|
|
}
|
|
}
|