mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 14:36:58 +00:00
227 lines
5.0 KiB
Go
227 lines
5.0 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/bubbles/viewport"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"github.com/DeBrosOfficial/network/pkg/cli/monitor"
|
|
)
|
|
|
|
const (
|
|
tabOverview = iota
|
|
tabNodes
|
|
tabServices
|
|
tabMesh
|
|
tabDNS
|
|
tabNamespaces
|
|
tabAlerts
|
|
tabCount
|
|
)
|
|
|
|
var tabNames = []string{"Overview", "Nodes", "Services", "WG Mesh", "DNS", "Namespaces", "Alerts"}
|
|
|
|
// snapshotMsg carries the result of a background collection.
|
|
type snapshotMsg struct {
|
|
snap *monitor.ClusterSnapshot
|
|
err error
|
|
}
|
|
|
|
// tickMsg fires on each refresh interval.
|
|
type tickMsg time.Time
|
|
|
|
// model is the root Bubbletea model for the Orama monitor TUI.
|
|
type model struct {
|
|
cfg monitor.CollectorConfig
|
|
interval time.Duration
|
|
activeTab int
|
|
viewport viewport.Model
|
|
width int
|
|
height int
|
|
snapshot *monitor.ClusterSnapshot
|
|
loading bool
|
|
lastError error
|
|
lastUpdate time.Time
|
|
quitting bool
|
|
}
|
|
|
|
// newModel creates a fresh model with default viewport dimensions.
|
|
func newModel(cfg monitor.CollectorConfig, interval time.Duration) model {
|
|
vp := viewport.New(80, 24)
|
|
return model{
|
|
cfg: cfg,
|
|
interval: interval,
|
|
viewport: vp,
|
|
loading: true,
|
|
}
|
|
}
|
|
|
|
func (m model) Init() tea.Cmd {
|
|
return tea.Batch(doCollect(m.cfg), tickCmd(m.interval))
|
|
}
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
var cmds []tea.Cmd
|
|
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
switch {
|
|
case msg.String() == "q" || msg.String() == "ctrl+c":
|
|
m.quitting = true
|
|
return m, tea.Quit
|
|
|
|
case msg.String() == "tab" || msg.String() == "l":
|
|
m.activeTab = (m.activeTab + 1) % tabCount
|
|
m.updateContent()
|
|
m.viewport.GotoTop()
|
|
return m, nil
|
|
|
|
case msg.String() == "shift+tab" || msg.String() == "h":
|
|
m.activeTab = (m.activeTab - 1 + tabCount) % tabCount
|
|
m.updateContent()
|
|
m.viewport.GotoTop()
|
|
return m, nil
|
|
|
|
case msg.String() == "r":
|
|
if !m.loading {
|
|
m.loading = true
|
|
return m, doCollect(m.cfg)
|
|
}
|
|
return m, nil
|
|
|
|
default:
|
|
// Delegate scrolling to viewport
|
|
var cmd tea.Cmd
|
|
m.viewport, cmd = m.viewport.Update(msg)
|
|
return m, cmd
|
|
}
|
|
|
|
case tea.WindowSizeMsg:
|
|
m.width = msg.Width
|
|
m.height = msg.Height
|
|
// Reserve 4 lines: header, tab bar, blank separator, footer
|
|
vpHeight := msg.Height - 4
|
|
if vpHeight < 1 {
|
|
vpHeight = 1
|
|
}
|
|
m.viewport.Width = msg.Width
|
|
m.viewport.Height = vpHeight
|
|
m.updateContent()
|
|
return m, nil
|
|
|
|
case snapshotMsg:
|
|
m.loading = false
|
|
if msg.err != nil {
|
|
m.lastError = msg.err
|
|
} else {
|
|
m.snapshot = msg.snap
|
|
m.lastError = nil
|
|
m.lastUpdate = time.Now()
|
|
}
|
|
m.updateContent()
|
|
return m, nil
|
|
|
|
case tickMsg:
|
|
if !m.loading {
|
|
m.loading = true
|
|
cmds = append(cmds, doCollect(m.cfg))
|
|
}
|
|
cmds = append(cmds, tickCmd(m.interval))
|
|
return m, tea.Batch(cmds...)
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (m model) View() string {
|
|
if m.quitting {
|
|
return ""
|
|
}
|
|
|
|
// Header
|
|
var header string
|
|
if m.snapshot != nil {
|
|
ago := time.Since(m.lastUpdate).Truncate(time.Second)
|
|
header = headerStyle.Render(fmt.Sprintf(
|
|
"Orama Monitor — %s — Last: %s (%s ago)",
|
|
m.snapshot.Environment,
|
|
m.lastUpdate.Format("15:04:05"),
|
|
ago,
|
|
))
|
|
} else if m.loading {
|
|
header = headerStyle.Render("Orama Monitor — collecting...")
|
|
} else if m.lastError != nil {
|
|
header = headerStyle.Render(fmt.Sprintf("Orama Monitor — error: %v", m.lastError))
|
|
} else {
|
|
header = headerStyle.Render("Orama Monitor")
|
|
}
|
|
|
|
if m.loading && m.snapshot != nil {
|
|
header += styleMuted.Render(" (refreshing...)")
|
|
}
|
|
|
|
// Tab bar
|
|
tabs := renderTabBar(m.activeTab, m.width)
|
|
|
|
// Footer
|
|
footer := footerStyle.Render("tab: switch | j/k: scroll | r: refresh | q: quit")
|
|
|
|
return header + "\n" + tabs + "\n" + m.viewport.View() + "\n" + footer
|
|
}
|
|
|
|
// updateContent renders the active tab and sets it on the viewport.
|
|
func (m *model) updateContent() {
|
|
w := m.width
|
|
if w == 0 {
|
|
w = 80
|
|
}
|
|
|
|
var content string
|
|
switch m.activeTab {
|
|
case tabOverview:
|
|
content = renderOverview(m.snapshot, w)
|
|
case tabNodes:
|
|
content = renderNodes(m.snapshot, w)
|
|
case tabServices:
|
|
content = renderServicesTab(m.snapshot, w)
|
|
case tabMesh:
|
|
content = renderWGMesh(m.snapshot, w)
|
|
case tabDNS:
|
|
content = renderDNSTab(m.snapshot, w)
|
|
case tabNamespaces:
|
|
content = renderNamespacesTab(m.snapshot, w)
|
|
case tabAlerts:
|
|
content = renderAlertsTab(m.snapshot, w)
|
|
}
|
|
|
|
m.viewport.SetContent(content)
|
|
}
|
|
|
|
// doCollect returns a tea.Cmd that runs monitor.CollectOnce in a goroutine.
|
|
func doCollect(cfg monitor.CollectorConfig) tea.Cmd {
|
|
return func() tea.Msg {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
snap, err := monitor.CollectOnce(ctx, cfg)
|
|
return snapshotMsg{snap: snap, err: err}
|
|
}
|
|
}
|
|
|
|
// tickCmd returns a tea.Cmd that fires a tickMsg after the given interval.
|
|
func tickCmd(d time.Duration) tea.Cmd {
|
|
return tea.Tick(d, func(t time.Time) tea.Msg {
|
|
return tickMsg(t)
|
|
})
|
|
}
|
|
|
|
// Run starts the TUI program with the given collector config.
|
|
func Run(cfg monitor.CollectorConfig) error {
|
|
m := newModel(cfg, 30*time.Second)
|
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
|
_, err := p.Run()
|
|
return err
|
|
}
|