refactor(tui): move maintenance from sidebar to bottom strip
Replace the cramped 22-char maint sidebar with a full-width bottom strip in the same position as logs. Bottom panel is modal: l for logs, m for maint, same key again to close. Maint strip shows one row per window with icon, title, affected monitors, and status/time remaining. Removes maintSidebarW constant and maintOffset field. Introduces bottomPanel enum (bottomNone/bottomLogs/bottomMaint) replacing logsOpen and maintOpen booleans. m key no longer gated behind wideBreakpoint.
This commit is contained in:
+10
-4
@@ -95,6 +95,14 @@ const (
|
|||||||
panelMaint = 3
|
panelMaint = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type bottomPanel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
bottomNone bottomPanel = iota
|
||||||
|
bottomLogs
|
||||||
|
bottomMaint
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
detailDefault = 0
|
detailDefault = 0
|
||||||
detailSLA = 1
|
detailSLA = 1
|
||||||
@@ -198,11 +206,9 @@ type Model struct {
|
|||||||
lastTabLoad time.Time // last dispatch of loadTabDataCmd (throttle)
|
lastTabLoad time.Time // last dispatch of loadTabDataCmd (throttle)
|
||||||
tabSeq int // seq of the newest issued tab-data load
|
tabSeq int // seq of the newest issued tab-data load
|
||||||
|
|
||||||
logsOpen bool
|
bottomPanel bottomPanel
|
||||||
detailOpen bool
|
detailOpen bool
|
||||||
maintOpen bool
|
|
||||||
maintCursor int
|
maintCursor int
|
||||||
maintOffset int
|
|
||||||
detailChanges []models.StateChange
|
detailChanges []models.StateChange
|
||||||
detailChangesSiteID int
|
detailChangesSiteID int
|
||||||
detailDailyDays []monitor.DayReport
|
detailDailyDays []monitor.DayReport
|
||||||
@@ -249,7 +255,7 @@ func InitialModel(ctx context.Context, isAdmin bool, s store.Store, eng *monitor
|
|||||||
theme: theme,
|
theme: theme,
|
||||||
themeIndex: themeIdx,
|
themeIndex: themeIdx,
|
||||||
st: newStyles(theme),
|
st: newStyles(theme),
|
||||||
logsOpen: true,
|
bottomPanel: bottomLogs,
|
||||||
detailOpen: detailPref == "true",
|
detailOpen: detailPref == "true",
|
||||||
demoMode: os.Getenv("UPTOP_DEMO") == "1",
|
demoMode: os.Getenv("UPTOP_DEMO") == "1",
|
||||||
version: version,
|
version: version,
|
||||||
|
|||||||
+14
-16
@@ -157,7 +157,7 @@ func (m *Model) recalcLayout() {
|
|||||||
if m.filterMode || m.filterText != "" {
|
if m.filterMode || m.filterText != "" {
|
||||||
chrome++
|
chrome++
|
||||||
}
|
}
|
||||||
if m.logsOpen {
|
if m.bottomPanel != bottomNone {
|
||||||
chrome += logsStripHeight
|
chrome += logsStripHeight
|
||||||
}
|
}
|
||||||
m.maxTableRows = m.termHeight - chrome
|
m.maxTableRows = m.termHeight - chrome
|
||||||
@@ -580,22 +580,20 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.sortAsc = !m.sortAsc
|
m.sortAsc = !m.sortAsc
|
||||||
m.refreshLive()
|
m.refreshLive()
|
||||||
case "m":
|
case "m":
|
||||||
if m.termWidth >= wideBreakpoint {
|
if m.bottomPanel == bottomMaint {
|
||||||
if m.focusedPanel == panelMaint {
|
m.bottomPanel = bottomNone
|
||||||
m.maintOpen = false
|
|
||||||
m.focusedPanel = panelMonitors
|
|
||||||
} else {
|
|
||||||
m.maintOpen = true
|
|
||||||
m.focusedPanel = panelMaint
|
|
||||||
}
|
|
||||||
m.recalcLayout()
|
|
||||||
}
|
|
||||||
case "l":
|
|
||||||
if m.logsOpen {
|
|
||||||
m.logsOpen = false
|
|
||||||
m.focusedPanel = panelMonitors
|
m.focusedPanel = panelMonitors
|
||||||
} else {
|
} else {
|
||||||
m.logsOpen = true
|
m.bottomPanel = bottomMaint
|
||||||
|
m.focusedPanel = panelMaint
|
||||||
|
}
|
||||||
|
m.recalcLayout()
|
||||||
|
case "l":
|
||||||
|
if m.bottomPanel == bottomLogs {
|
||||||
|
m.bottomPanel = bottomNone
|
||||||
|
m.focusedPanel = panelMonitors
|
||||||
|
} else {
|
||||||
|
m.bottomPanel = bottomLogs
|
||||||
m.focusedPanel = panelLogs
|
m.focusedPanel = panelLogs
|
||||||
}
|
}
|
||||||
m.recalcLayout()
|
m.recalcLayout()
|
||||||
@@ -868,7 +866,7 @@ func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.maintOpen && m.zones.Get("panel-maint").InBounds(msg) {
|
if m.bottomPanel == bottomMaint && m.zones.Get("panel-maint").InBounds(msg) {
|
||||||
m.focusedPanel = panelMaint
|
m.focusedPanel = panelMaint
|
||||||
return m, nil
|
return m, nil
|
||||||
} else if m.zones.Get("panel-monitors").InBounds(msg) {
|
} else if m.zones.Get("panel-monitors").InBounds(msg) {
|
||||||
|
|||||||
@@ -143,28 +143,18 @@ func (m Model) computeStats() dashboardStats {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
const maintSidebarW = 22
|
|
||||||
|
|
||||||
func (m Model) viewMonitorsLayout() string {
|
func (m Model) viewMonitorsLayout() string {
|
||||||
availW := m.termWidth - chromePadH
|
availW := m.termWidth - chromePadH
|
||||||
wide := m.termWidth >= wideBreakpoint
|
wide := m.termWidth >= wideBreakpoint
|
||||||
|
|
||||||
showMaint := m.maintOpen && wide
|
|
||||||
showDetail := m.detailOpen && wide
|
showDetail := m.detailOpen && wide
|
||||||
|
|
||||||
var maintW, detailW, monW int
|
var detailW, monW int
|
||||||
if showMaint {
|
|
||||||
maintW = maintSidebarW
|
|
||||||
if maintW > availW/4 {
|
|
||||||
maintW = availW / 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remaining := availW - maintW
|
|
||||||
if showDetail {
|
if showDetail {
|
||||||
monW = remaining * 60 / 100
|
monW = availW * 60 / 100
|
||||||
detailW = remaining - monW
|
detailW = availW - monW
|
||||||
} else {
|
} else {
|
||||||
monW = remaining
|
monW = availW
|
||||||
}
|
}
|
||||||
|
|
||||||
m.contentWidth = monW - 2
|
m.contentWidth = monW - 2
|
||||||
@@ -173,11 +163,6 @@ func (m Model) viewMonitorsLayout() string {
|
|||||||
monPanel := m.zones.Mark("panel-monitors", m.titledPanel("Monitors", monitors, monW, m.focusedPanel == panelMonitors))
|
monPanel := m.zones.Mark("panel-monitors", m.titledPanel("Monitors", monitors, monW, m.focusedPanel == panelMonitors))
|
||||||
|
|
||||||
var topParts []string
|
var topParts []string
|
||||||
if showMaint {
|
|
||||||
sidebar := m.viewMaintSidebar(maintW-2, m.maxTableRows)
|
|
||||||
maintPanel := m.zones.Mark("panel-maint", m.titledPanel("Maint", sidebar, maintW, m.focusedPanel == panelMaint))
|
|
||||||
topParts = append(topParts, maintPanel)
|
|
||||||
}
|
|
||||||
topParts = append(topParts, monPanel)
|
topParts = append(topParts, monPanel)
|
||||||
if showDetail {
|
if showDetail {
|
||||||
title := ""
|
title := ""
|
||||||
@@ -199,10 +184,15 @@ func (m Model) viewMonitorsLayout() string {
|
|||||||
|
|
||||||
top := lipgloss.JoinHorizontal(lipgloss.Top, topParts...)
|
top := lipgloss.JoinHorizontal(lipgloss.Top, topParts...)
|
||||||
|
|
||||||
if m.logsOpen {
|
switch m.bottomPanel {
|
||||||
|
case bottomLogs:
|
||||||
logContent := m.viewLogsStrip(availW-2, logsStripHeight-2)
|
logContent := m.viewLogsStrip(availW-2, logsStripHeight-2)
|
||||||
logPanel := m.zones.Mark("panel-logs", m.titledPanel("Logs", logContent, availW, m.focusedPanel == panelLogs))
|
logPanel := m.zones.Mark("panel-logs", m.titledPanel("Logs", logContent, availW, m.focusedPanel == panelLogs))
|
||||||
return top + "\n" + logPanel
|
return top + "\n" + logPanel
|
||||||
|
case bottomMaint:
|
||||||
|
maintContent := m.viewMaintStrip(availW-2, logsStripHeight-2)
|
||||||
|
maintPanel := m.zones.Mark("panel-maint", m.titledPanel("Maint", maintContent, availW, m.focusedPanel == panelMaint))
|
||||||
|
return top + "\n" + maintPanel
|
||||||
}
|
}
|
||||||
return top
|
return top
|
||||||
}
|
}
|
||||||
@@ -296,7 +286,7 @@ func (m Model) renderFooter(_ dashboardStats) string {
|
|||||||
|
|
||||||
var parts []string
|
var parts []string
|
||||||
if m.focusedPanel == panelMaint {
|
if m.focusedPanel == panelMaint {
|
||||||
parts = []string{m.hotkey("n", "New"), m.hotkey("x", "End"), m.hotkey("d", "Del"), m.hotkey("Esc", "Back")}
|
parts = []string{m.hotkey("n", "New"), m.hotkey("Enter", "Detail"), m.hotkey("x", "End"), m.hotkey("d", "Del"), m.hotkey("m/Esc", "Back")}
|
||||||
} else if m.focusedPanel == panelLogs {
|
} else if m.focusedPanel == panelLogs {
|
||||||
parts = []string{m.hotkey("↑/↓", "Scroll"), m.hotkey("Enter", "Expand"), m.hotkey("l/Esc", "Back")}
|
parts = []string{m.hotkey("↑/↓", "Scroll"), m.hotkey("Enter", "Expand"), m.hotkey("l/Esc", "Back")}
|
||||||
} else if m.detailOpen && m.detailMode == detailSLA {
|
} else if m.detailOpen && m.detailMode == detailSLA {
|
||||||
@@ -304,7 +294,7 @@ func (m Model) renderFooter(_ dashboardStats) string {
|
|||||||
} else if m.detailOpen && m.detailMode == detailHistory {
|
} else if m.detailOpen && m.detailMode == detailHistory {
|
||||||
parts = []string{m.hotkey("Esc", "Back")}
|
parts = []string{m.hotkey("Esc", "Back")}
|
||||||
} else if m.detailOpen {
|
} else if m.detailOpen {
|
||||||
parts = []string{m.hotkey("Enter", "Close"), m.hotkey("h", "History"), m.hotkey("s", "SLA"), m.hotkey("e", "Edit"), m.hotkey("m", "Maint")}
|
parts = []string{m.hotkey("Enter", "Close"), m.hotkey("h", "History"), m.hotkey("s", "SLA"), m.hotkey("e", "Edit")}
|
||||||
} else {
|
} else {
|
||||||
parts = []string{m.hotkey("/", "Filter"), m.hotkey("Enter", "Detail")}
|
parts = []string{m.hotkey("/", "Filter"), m.hotkey("Enter", "Detail")}
|
||||||
if m.cursor < len(m.sites) && m.sites[m.cursor].Type == "group" {
|
if m.cursor < len(m.sites) && m.sites[m.cursor].Type == "group" {
|
||||||
@@ -314,9 +304,9 @@ func (m Model) renderFooter(_ dashboardStats) string {
|
|||||||
parts = append(parts, m.hotkey("Space", "Collapse"))
|
parts = append(parts, m.hotkey("Space", "Collapse"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parts = append(parts, m.hotkey("n", "New"), m.hotkey("e", "Edit"), m.hotkey("d", "Del"), m.hotkey("m", "Maint"))
|
parts = append(parts, m.hotkey("n", "New"), m.hotkey("e", "Edit"), m.hotkey("d", "Del"))
|
||||||
}
|
}
|
||||||
parts = append(parts, m.hotkey("l", "Logs"), m.hotkey("S", "Settings"), m.hotkey("T", "Theme"), m.hotkey("q", "Quit"))
|
parts = append(parts, m.hotkey("m", "Maint"), m.hotkey("l", "Logs"), m.hotkey("S", "Settings"), m.hotkey("T", "Theme"), m.hotkey("q", "Quit"))
|
||||||
|
|
||||||
line := strings.Join(parts, dot)
|
line := strings.Join(parts, dot)
|
||||||
if m.filterText != "" {
|
if m.filterText != "" {
|
||||||
|
|||||||
@@ -85,6 +85,15 @@ func (m Model) viewMaintDetailPanel() string {
|
|||||||
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) monitorNameByID(id int) string {
|
||||||
|
for _, s := range m.engine.GetAllSites() {
|
||||||
|
if s.ID == id {
|
||||||
|
return s.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("#%d", id)
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) activeMaintWindows() []models.MaintenanceWindow {
|
func (m Model) activeMaintWindows() []models.MaintenanceWindow {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
var out []models.MaintenanceWindow
|
var out []models.MaintenanceWindow
|
||||||
@@ -97,31 +106,28 @@ func (m Model) activeMaintWindows() []models.MaintenanceWindow {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) viewMaintSidebar(width, maxLines int) string {
|
func (m Model) viewMaintStrip(width, maxLines int) string {
|
||||||
windows := m.activeMaintWindows()
|
windows := m.activeMaintWindows()
|
||||||
if len(windows) == 0 {
|
if len(windows) == 0 {
|
||||||
return m.st.subtleStyle.Render(" No active maintenance")
|
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()
|
now := time.Now()
|
||||||
for i := start; i < end; i++ {
|
titleW := 30
|
||||||
|
monW := 20
|
||||||
|
if width < 80 {
|
||||||
|
titleW = width / 3
|
||||||
|
monW = width / 5
|
||||||
|
}
|
||||||
|
|
||||||
|
end := maxLines
|
||||||
|
if end > len(windows) {
|
||||||
|
end = len(windows)
|
||||||
|
}
|
||||||
|
|
||||||
|
style := lipgloss.NewStyle().Width(width).MaxWidth(width)
|
||||||
|
var lines []string
|
||||||
|
for i := 0; i < end; i++ {
|
||||||
mw := windows[i]
|
mw := windows[i]
|
||||||
selected := m.focusedPanel == panelMaint && i == m.maintCursor
|
selected := m.focusedPanel == panelMaint && i == m.maintCursor
|
||||||
|
|
||||||
@@ -134,37 +140,40 @@ func (m Model) viewMaintSidebar(width, maxLines int) string {
|
|||||||
icon = m.st.warnStyle.Render("○")
|
icon = m.st.warnStyle.Render("○")
|
||||||
}
|
}
|
||||||
|
|
||||||
title := limitStr(mw.Title, contentW-3)
|
title := limitStr(mw.Title, titleW)
|
||||||
titleLine := " " + icon + " " + title
|
|
||||||
|
|
||||||
var detail string
|
monName := "All Monitors"
|
||||||
|
if mw.MonitorID > 0 {
|
||||||
|
monName = m.monitorNameByID(mw.MonitorID)
|
||||||
|
}
|
||||||
|
monName = limitStr(monName, monW)
|
||||||
|
|
||||||
|
var status string
|
||||||
if isActive {
|
if isActive {
|
||||||
if mw.EndTime.IsZero() {
|
if mw.EndTime.IsZero() {
|
||||||
detail = m.st.subtleStyle.Render("active · indefinite")
|
status = m.st.subtleStyle.Render("active · indefinite")
|
||||||
} else {
|
} else {
|
||||||
remaining := time.Until(mw.EndTime)
|
status = m.st.subtleStyle.Render("active · " + fmtDuration(time.Until(mw.EndTime)) + " left")
|
||||||
detail = m.st.subtleStyle.Render("active · " + fmtDuration(remaining))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
detail = m.st.subtleStyle.Render(mw.StartTime.Format("Jan 02") + " · " + fmtDuration(mw.EndTime.Sub(mw.StartTime)))
|
status = m.st.subtleStyle.Render("scheduled · " + mw.StartTime.Format("Jan 02 15:04"))
|
||||||
}
|
}
|
||||||
detailLine := " " + limitStr(detail, contentW-3)
|
|
||||||
|
line := fmt.Sprintf(" %s %-*s %-*s %s", icon, titleW, title, monW, m.st.subtleStyle.Render(monName), status)
|
||||||
|
|
||||||
if selected {
|
if selected {
|
||||||
sel := m.st.tableSelectedStyle
|
line = m.st.tableSelectedStyle.Render(lipgloss.NewStyle().Width(width).Render(line))
|
||||||
titleLine = sel.Render(lipgloss.NewStyle().Width(contentW).Render(titleLine))
|
|
||||||
detailLine = sel.Render(lipgloss.NewStyle().Width(contentW).Render(detailLine))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines = append(lines, titleLine, detailLine)
|
lines = append(lines, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(windows) > end-start {
|
if len(windows) > end {
|
||||||
more := fmt.Sprintf(" %d more", len(windows)-(end-start))
|
more := fmt.Sprintf(" %d more", len(windows)-end)
|
||||||
lines = append(lines, m.st.subtleStyle.Render(more))
|
lines = append(lines, m.st.subtleStyle.Render(more))
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(lines, "\n")
|
return style.Render(strings.Join(lines, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) scrollMaintCursor(delta int) {
|
func (m *Model) scrollMaintCursor(delta int) {
|
||||||
@@ -180,14 +189,4 @@ func (m *Model) scrollMaintCursor(delta int) {
|
|||||||
if m.maintCursor >= total {
|
if m.maintCursor >= total {
|
||||||
m.maintCursor = total - 1
|
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