feat(tui): single-dashboard layout with sidebar and overlay
Replace 3-tab navigation with a single monitoring dashboard. - Replace tab bar with minimal status line (pulse + counts + version) - Maintenance becomes a toggleable left sidebar (m key) - Compact list: active (●) and scheduled (○) windows - Enter opens full-screen detail with end/delete actions - Settings becomes a centered floating overlay (S key) - Alerts/Nodes/Users sub-sections with left/right navigation - Full CRUD support inside overlay with returnState tracking - Remove currentTab, switchTab, tab constants, tab click zones - Replace deleteTab with deleteKind string for cleaner dispatch - Separate settingsCursor/settingsOffset from main cursor - Panel focus: maint → monitors → logs → detail - Context-sensitive footer key hints per focused panel
This commit was merged in pull request #162.
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) viewMaintDetailPanel() string {
|
||||
mw := m.findMaintWindow(m.maintDetailID)
|
||||
if mw == nil {
|
||||
return lipgloss.NewStyle().Padding(1, 2).Render(m.st.subtleStyle.Render("Maintenance window not found"))
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
header := " " + m.st.subtleStyle.Render("Maintenance >") + " " + m.st.titleStyle.Render(mw.Title)
|
||||
b.WriteString(header + "\n")
|
||||
b.WriteString(m.divider() + "\n")
|
||||
|
||||
row := func(label, value string) {
|
||||
fmt.Fprintf(&b, " %-16s %s\n", m.st.subtleStyle.Render(label), value)
|
||||
}
|
||||
|
||||
maintType := m.st.maintStyle.Render("maintenance")
|
||||
if mw.Type == "incident" {
|
||||
maintType = m.st.dangerStyle.Render("incident")
|
||||
}
|
||||
row("Type", maintType)
|
||||
|
||||
now := time.Now()
|
||||
if mw.StartTime.After(now) {
|
||||
row("Status", m.st.warnStyle.Render("SCHEDULED"))
|
||||
} else if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
||||
row("Status", m.st.subtleStyle.Render("ENDED"))
|
||||
} else {
|
||||
row("Status", m.st.specialStyle.Render("ACTIVE"))
|
||||
}
|
||||
|
||||
if mw.MonitorID == 0 {
|
||||
row("Monitors", "All")
|
||||
} else {
|
||||
name := fmt.Sprintf("#%d", mw.MonitorID)
|
||||
allSites := m.engine.GetAllSites()
|
||||
for _, s := range allSites {
|
||||
if s.ID == mw.MonitorID {
|
||||
name = s.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
row("Monitors", name)
|
||||
}
|
||||
|
||||
row("Started", mw.StartTime.Format("2006-01-02 15:04"))
|
||||
if mw.EndTime.IsZero() {
|
||||
row("Ends", m.st.subtleStyle.Render("indefinite"))
|
||||
} else {
|
||||
row("Ends", mw.EndTime.Format("2006-01-02 15:04"))
|
||||
if mw.EndTime.After(now) && !mw.StartTime.After(now) {
|
||||
remaining := time.Until(mw.EndTime)
|
||||
row("Remaining", fmtDuration(remaining))
|
||||
}
|
||||
dur := mw.EndTime.Sub(mw.StartTime)
|
||||
row("Duration", fmtDuration(dur))
|
||||
}
|
||||
|
||||
if mw.Description != "" {
|
||||
b.WriteString("\n")
|
||||
row("Description", mw.Description)
|
||||
}
|
||||
|
||||
b.WriteString("\n" + m.divider() + "\n")
|
||||
|
||||
var keys []string
|
||||
isActive := !mw.StartTime.After(now) && (mw.EndTime.IsZero() || mw.EndTime.After(now))
|
||||
if isActive {
|
||||
keys = append(keys, "[x] End")
|
||||
}
|
||||
keys = append(keys, "[d] Delete", "[q/Esc] Back")
|
||||
b.WriteString(" " + m.st.subtleStyle.Render(strings.Join(keys, " ")))
|
||||
|
||||
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
||||
}
|
||||
|
||||
func (m Model) activeMaintWindows() []models.MaintenanceWindow {
|
||||
now := time.Now()
|
||||
var out []models.MaintenanceWindow
|
||||
for _, mw := range m.maintenanceWindows {
|
||||
if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
||||
continue
|
||||
}
|
||||
out = append(out, mw)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m Model) viewMaintSidebar(width, maxLines int) string {
|
||||
windows := m.activeMaintWindows()
|
||||
if len(windows) == 0 {
|
||||
return m.st.subtleStyle.Render(" No active maintenance")
|
||||
}
|
||||
|
||||
contentW := width - 2
|
||||
if contentW < 10 {
|
||||
contentW = 10
|
||||
}
|
||||
|
||||
start := m.maintOffset
|
||||
if start > len(windows) {
|
||||
start = len(windows)
|
||||
}
|
||||
linesUsed := 0
|
||||
end := start
|
||||
for end < len(windows) && linesUsed+2 <= maxLines {
|
||||
linesUsed += 2
|
||||
end++
|
||||
}
|
||||
|
||||
var lines []string
|
||||
now := time.Now()
|
||||
for i := start; i < end; i++ {
|
||||
mw := windows[i]
|
||||
selected := m.focusedPanel == panelMaint && i == m.maintCursor
|
||||
|
||||
isActive := !mw.StartTime.After(now) && (mw.EndTime.IsZero() || mw.EndTime.After(now))
|
||||
|
||||
var icon string
|
||||
if isActive {
|
||||
icon = m.st.specialStyle.Render("●")
|
||||
} else {
|
||||
icon = m.st.warnStyle.Render("○")
|
||||
}
|
||||
|
||||
title := limitStr(mw.Title, contentW-3)
|
||||
titleLine := " " + icon + " " + title
|
||||
|
||||
var detail string
|
||||
if isActive {
|
||||
if mw.EndTime.IsZero() {
|
||||
detail = m.st.subtleStyle.Render("active · indefinite")
|
||||
} else {
|
||||
remaining := time.Until(mw.EndTime)
|
||||
detail = m.st.subtleStyle.Render("active · " + fmtDuration(remaining))
|
||||
}
|
||||
} else {
|
||||
detail = m.st.subtleStyle.Render(mw.StartTime.Format("Jan 02") + " · " + fmtDuration(mw.EndTime.Sub(mw.StartTime)))
|
||||
}
|
||||
detailLine := " " + limitStr(detail, contentW-3)
|
||||
|
||||
if selected {
|
||||
sel := m.st.tableSelectedStyle
|
||||
titleLine = sel.Render(lipgloss.NewStyle().Width(contentW).Render(titleLine))
|
||||
detailLine = sel.Render(lipgloss.NewStyle().Width(contentW).Render(detailLine))
|
||||
}
|
||||
|
||||
lines = append(lines, titleLine, detailLine)
|
||||
}
|
||||
|
||||
if len(windows) > end-start {
|
||||
more := fmt.Sprintf(" %d more", len(windows)-(end-start))
|
||||
lines = append(lines, m.st.subtleStyle.Render(more))
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (m *Model) scrollMaintCursor(delta int) {
|
||||
windows := m.activeMaintWindows()
|
||||
total := len(windows)
|
||||
if total == 0 {
|
||||
return
|
||||
}
|
||||
m.maintCursor += delta
|
||||
if m.maintCursor < 0 {
|
||||
m.maintCursor = 0
|
||||
}
|
||||
if m.maintCursor >= total {
|
||||
m.maintCursor = total - 1
|
||||
}
|
||||
if m.maintCursor < m.maintOffset {
|
||||
m.maintOffset = m.maintCursor
|
||||
}
|
||||
visible := m.maxTableRows / 2
|
||||
if visible < 1 {
|
||||
visible = 1
|
||||
}
|
||||
if m.maintCursor >= m.maintOffset+visible {
|
||||
m.maintOffset = m.maintCursor - visible + 1
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user