feat(tui): stable detail panel height with scroll support
Detail sidebar now maintains consistent height matching the monitors panel across all modes (default, SLA, history). Content scrolls within the fixed frame via mouse wheel or j/k when the detail panel is focused. Scroll offset resets on mode/monitor/period changes. Logs strip stays visible in all states.
This commit is contained in:
+60
-7
@@ -6,16 +6,69 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) titledPanelH(title, content string, width, height int, focused bool) string {
|
||||
s := m.titledPanel(title, content, width, focused)
|
||||
func (m Model) titledPanelH(title, content string, width, height, scrollOffset int, focused bool) string {
|
||||
if height <= 0 {
|
||||
return s
|
||||
return m.titledPanel(title, content, width, focused)
|
||||
}
|
||||
lines := strings.Split(s, "\n")
|
||||
if len(lines) <= height {
|
||||
return s
|
||||
|
||||
borderColor := m.theme.Border
|
||||
titleColor := m.theme.Muted
|
||||
if focused {
|
||||
borderColor = m.theme.Accent
|
||||
titleColor = m.theme.Accent
|
||||
}
|
||||
return strings.Join(lines[:height-1], "\n") + "\n" + lines[len(lines)-1]
|
||||
|
||||
bc := lipgloss.NewStyle().Foreground(borderColor)
|
||||
tc := lipgloss.NewStyle().Foreground(titleColor).Bold(true)
|
||||
|
||||
innerW := width - 2
|
||||
if innerW < 10 {
|
||||
innerW = 10
|
||||
}
|
||||
|
||||
titleRendered := tc.Render(" " + title + " ")
|
||||
titleLen := len([]rune(title)) + 2
|
||||
fillLen := innerW - titleLen - 1
|
||||
if fillLen < 0 {
|
||||
fillLen = 0
|
||||
}
|
||||
|
||||
top := bc.Render("╭─") + titleRendered + bc.Render(strings.Repeat("─", fillLen)+"╮")
|
||||
bottom := bc.Render("╰" + strings.Repeat("─", innerW) + "╯")
|
||||
|
||||
contentStyle := lipgloss.NewStyle().Width(innerW).MaxWidth(innerW)
|
||||
inner := contentStyle.Render(content)
|
||||
contentLines := strings.Split(inner, "\n")
|
||||
|
||||
bodyH := height - 2
|
||||
if bodyH < 1 {
|
||||
bodyH = 1
|
||||
}
|
||||
|
||||
if scrollOffset > len(contentLines)-bodyH {
|
||||
scrollOffset = len(contentLines) - bodyH
|
||||
}
|
||||
if scrollOffset < 0 {
|
||||
scrollOffset = 0
|
||||
}
|
||||
|
||||
end := scrollOffset + bodyH
|
||||
if end > len(contentLines) {
|
||||
end = len(contentLines)
|
||||
}
|
||||
visible := contentLines[scrollOffset:end]
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, top)
|
||||
for _, line := range visible {
|
||||
lines = append(lines, bc.Render("│")+line+strings.Repeat(" ", max(0, innerW-lipgloss.Width(line)))+bc.Render("│"))
|
||||
}
|
||||
for len(lines) < height-1 {
|
||||
lines = append(lines, bc.Render("│")+strings.Repeat(" ", innerW)+bc.Render("│"))
|
||||
}
|
||||
lines = append(lines, bottom)
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (m Model) titledPanel(title, content string, width int, focused bool) string {
|
||||
|
||||
+6
-5
@@ -160,11 +160,12 @@ type Model struct {
|
||||
historySiteName string
|
||||
historySiteID int
|
||||
|
||||
slaReport monitor.SLAReport
|
||||
slaDailyBreakdown []monitor.DayReport
|
||||
slaSiteName string
|
||||
slaSiteID int
|
||||
slaPeriodIdx int
|
||||
slaReport monitor.SLAReport
|
||||
slaDailyBreakdown []monitor.DayReport
|
||||
slaSiteName string
|
||||
slaSiteID int
|
||||
slaPeriodIdx int
|
||||
detailScrollOffset int
|
||||
|
||||
isAdmin bool
|
||||
zones *zone.Manager
|
||||
|
||||
@@ -298,6 +298,18 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if m.focusedPanel == panelDetail && m.detailOpen {
|
||||
if msg.Button == tea.MouseButtonWheelUp {
|
||||
m.detailScrollOffset -= 3
|
||||
} else {
|
||||
m.detailScrollOffset += 3
|
||||
}
|
||||
if m.detailScrollOffset < 0 {
|
||||
m.detailScrollOffset = 0
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
listLen := m.currentListLen()
|
||||
if msg.Button == tea.MouseButtonWheelUp {
|
||||
if m.cursor > 0 {
|
||||
@@ -588,6 +600,13 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
m.recalcLayout()
|
||||
case "up", "k":
|
||||
if m.focusedPanel == panelDetail && m.detailOpen {
|
||||
m.detailScrollOffset--
|
||||
if m.detailScrollOffset < 0 {
|
||||
m.detailScrollOffset = 0
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if m.focusedPanel == panelMaint {
|
||||
m.scrollMaintCursor(-1)
|
||||
return m, nil
|
||||
@@ -604,10 +623,15 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.syncSelectedID()
|
||||
if m.detailOpen && m.cursor < len(m.sites) {
|
||||
m.detailMode = detailDefault
|
||||
m.detailScrollOffset = 0
|
||||
return m, m.loadDetailCmd(m.sites[m.cursor].ID)
|
||||
}
|
||||
}
|
||||
case "down", "j":
|
||||
if m.focusedPanel == panelDetail && m.detailOpen {
|
||||
m.detailScrollOffset++
|
||||
return m, nil
|
||||
}
|
||||
if m.focusedPanel == panelMaint {
|
||||
m.scrollMaintCursor(1)
|
||||
return m, nil
|
||||
@@ -625,6 +649,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.syncSelectedID()
|
||||
if m.detailOpen && m.cursor < len(m.sites) {
|
||||
m.detailMode = detailDefault
|
||||
m.detailScrollOffset = 0
|
||||
return m, m.loadDetailCmd(m.sites[m.cursor].ID)
|
||||
}
|
||||
}
|
||||
@@ -652,6 +677,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if len(m.sites) > 0 {
|
||||
m.detailOpen = !m.detailOpen
|
||||
m.detailMode = detailDefault
|
||||
m.detailScrollOffset = 0
|
||||
m.recalcLayout()
|
||||
st := m.store
|
||||
ctx := m.ctx
|
||||
@@ -702,9 +728,11 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.focusedPanel = panelMonitors
|
||||
} else if m.detailOpen && m.detailMode != detailDefault {
|
||||
m.detailMode = detailDefault
|
||||
m.detailScrollOffset = 0
|
||||
} else if m.detailOpen {
|
||||
m.detailOpen = false
|
||||
m.detailMode = detailDefault
|
||||
m.detailScrollOffset = 0
|
||||
m.recalcLayout()
|
||||
st := m.store
|
||||
ctx := m.ctx
|
||||
@@ -719,6 +747,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.historySiteID = site.ID
|
||||
m.historyChanges = nil
|
||||
m.detailMode = detailHistory
|
||||
m.detailScrollOffset = 0
|
||||
return m, m.loadHistoryCmd(site.ID)
|
||||
}
|
||||
case "s":
|
||||
@@ -728,6 +757,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.slaSiteID = site.ID
|
||||
m.slaPeriodIdx = 2
|
||||
m.detailMode = detailSLA
|
||||
m.detailScrollOffset = 0
|
||||
return m, m.loadSLACmd(site.ID, m.slaPeriodIdx)
|
||||
}
|
||||
case "1", "2", "3", "4":
|
||||
@@ -735,6 +765,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
idx := int(msg.String()[0]-'0') - 1
|
||||
if idx >= 0 && idx < len(slaPeriods) {
|
||||
m.slaPeriodIdx = idx
|
||||
m.detailScrollOffset = 0
|
||||
return m, m.loadSLACmd(m.slaSiteID, idx)
|
||||
}
|
||||
}
|
||||
@@ -860,6 +891,7 @@ func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||
m.syncSelectedID()
|
||||
m.focusedPanel = panelMonitors
|
||||
if m.detailOpen {
|
||||
m.detailScrollOffset = 0
|
||||
return m, m.loadDetailCmd(m.sites[m.cursor].ID)
|
||||
}
|
||||
return m, nil
|
||||
|
||||
@@ -192,7 +192,7 @@ func (m Model) viewMonitorsLayout() string {
|
||||
}
|
||||
monHeight := lipgloss.Height(monPanel)
|
||||
detail := m.viewDetailInline(detailW-2, monHeight)
|
||||
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, detailW, monHeight, m.focusedPanel == panelDetail))
|
||||
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, detailW, monHeight, m.detailScrollOffset, m.focusedPanel == panelDetail))
|
||||
topParts = append(topParts, detailPanel)
|
||||
}
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ func (m Model) fmtStatusWord(status string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) viewSLASidebar(width, height int) string {
|
||||
func (m Model) viewSLASidebar(width, _ int) string {
|
||||
var b strings.Builder
|
||||
label := m.st.subtleStyle
|
||||
innerW := width - 4
|
||||
@@ -277,21 +277,7 @@ func (m Model) viewSLASidebar(width, height int) string {
|
||||
if dayBarW < 10 {
|
||||
dayBarW = 10
|
||||
}
|
||||
slaChrome := 14
|
||||
if r.OutageCount > 0 {
|
||||
slaChrome += 3
|
||||
}
|
||||
maxDaily := len(m.slaDailyBreakdown)
|
||||
if height > 0 && height-slaChrome < maxDaily {
|
||||
maxDaily = height - slaChrome
|
||||
if maxDaily < 1 {
|
||||
maxDaily = 1
|
||||
}
|
||||
}
|
||||
for i, day := range m.slaDailyBreakdown {
|
||||
if i >= maxDaily {
|
||||
break
|
||||
}
|
||||
for _, day := range m.slaDailyBreakdown {
|
||||
dateStr := day.Date.Format("Jan 02")
|
||||
dayBar := m.uptimeBar(day.UptimePct, dayBarW)
|
||||
pctStr := fmtPct(day.UptimePct) + "%"
|
||||
|
||||
Reference in New Issue
Block a user