feat(tui): stable detail panel with scroll, pinned footer, and 60/40 split #165
@@ -6,6 +6,18 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (m Model) titledPanelH(title, content string, width, height int, focused bool) string {
|
||||||
|
s := m.titledPanel(title, content, width, focused)
|
||||||
|
if height <= 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
lines := strings.Split(s, "\n")
|
||||||
|
if len(lines) <= height {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return strings.Join(lines[:height-1], "\n") + "\n" + lines[len(lines)-1]
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) titledPanel(title, content string, width int, focused bool) string {
|
func (m Model) titledPanel(title, content string, width int, focused bool) string {
|
||||||
borderColor := m.theme.Border
|
borderColor := m.theme.Border
|
||||||
titleColor := m.theme.Muted
|
titleColor := m.theme.Muted
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func TestDetailLoad_CachesAndViewDoesNoIO(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
_ = m.viewDetailInline(80)
|
_ = m.viewDetailInline(80, 30)
|
||||||
}
|
}
|
||||||
if ms.stateChangeCalls != 1 {
|
if ms.stateChangeCalls != 1 {
|
||||||
t.Errorf("View performed DB IO: store hit %d times (want 1, from the Cmd only)", ms.stateChangeCalls)
|
t.Errorf("View performed DB IO: store hit %d times (want 1, from the Cmd only)", ms.stateChangeCalls)
|
||||||
|
|||||||
@@ -190,8 +190,9 @@ func (m Model) viewMonitorsLayout() string {
|
|||||||
case detailHistory:
|
case detailHistory:
|
||||||
title = "History · " + title
|
title = "History · " + title
|
||||||
}
|
}
|
||||||
detail := m.viewDetailInline(detailW - 2)
|
monHeight := lipgloss.Height(monPanel)
|
||||||
detailPanel := m.zones.Mark("panel-detail", m.titledPanel(title, detail, detailW, m.focusedPanel == panelDetail))
|
detail := m.viewDetailInline(detailW-2, monHeight)
|
||||||
|
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, detailW, monHeight, m.focusedPanel == panelDetail))
|
||||||
topParts = append(topParts, detailPanel)
|
topParts = append(topParts, detailPanel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,23 +10,23 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m Model) viewDetailInline(width int) string {
|
func (m Model) viewDetailInline(width, height int) string {
|
||||||
if m.cursor >= len(m.sites) {
|
if m.cursor >= len(m.sites) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
switch m.detailMode {
|
switch m.detailMode {
|
||||||
case detailSLA:
|
case detailSLA:
|
||||||
return m.viewSLASidebar(width)
|
return m.viewSLASidebar(width, height)
|
||||||
case detailHistory:
|
case detailHistory:
|
||||||
return m.viewHistorySidebar(width)
|
return m.viewHistorySidebar(width, height)
|
||||||
default:
|
default:
|
||||||
site := m.sites[m.cursor]
|
site := m.sites[m.cursor]
|
||||||
hist, _ := m.engine.GetHistory(site.ID)
|
hist, _ := m.engine.GetHistory(site.ID)
|
||||||
return m.viewDetailSidebar(site, hist, width)
|
return m.viewDetailSidebar(site, hist, width, height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, width int) string {
|
func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, width, _ int) string {
|
||||||
dot := m.st.subtleStyle.Render(" · ")
|
dot := m.st.subtleStyle.Render(" · ")
|
||||||
label := m.st.subtleStyle
|
label := m.st.subtleStyle
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
@@ -235,7 +235,7 @@ func (m Model) fmtStatusWord(status string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) viewSLASidebar(width int) string {
|
func (m Model) viewSLASidebar(width, height int) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
label := m.st.subtleStyle
|
label := m.st.subtleStyle
|
||||||
innerW := width - 4
|
innerW := width - 4
|
||||||
@@ -277,7 +277,21 @@ func (m Model) viewSLASidebar(width int) string {
|
|||||||
if dayBarW < 10 {
|
if dayBarW < 10 {
|
||||||
dayBarW = 10
|
dayBarW = 10
|
||||||
}
|
}
|
||||||
for _, day := range m.slaDailyBreakdown {
|
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
|
||||||
|
}
|
||||||
dateStr := day.Date.Format("Jan 02")
|
dateStr := day.Date.Format("Jan 02")
|
||||||
dayBar := m.uptimeBar(day.UptimePct, dayBarW)
|
dayBar := m.uptimeBar(day.UptimePct, dayBarW)
|
||||||
pctStr := fmtPct(day.UptimePct) + "%"
|
pctStr := fmtPct(day.UptimePct) + "%"
|
||||||
@@ -309,7 +323,7 @@ func (m Model) viewSLASidebar(width int) string {
|
|||||||
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) viewHistorySidebar(width int) string {
|
func (m Model) viewHistorySidebar(width, _ int) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
label := m.st.subtleStyle
|
label := m.st.subtleStyle
|
||||||
innerW := width - 4
|
innerW := width - 4
|
||||||
|
|||||||
Reference in New Issue
Block a user