feat(tui): fullscreen detail view + enriched sidebar content
On narrow terminals (<120 cols), Enter opens a fullscreen detail view instead of the sidebar that can't be seen. Wide terminals keep existing sidebar toggle behavior. Enrich detail panel with: state since, last success, connection chain diagnostic, probe results, latency histogram, extended endpoint fields (interval/timeout/method/codes/regions/description), push token, maintenance window name. Breadcrumb shown in fullscreen mode only.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type latencyBucket struct {
|
||||
label string
|
||||
min int64
|
||||
max int64
|
||||
count int
|
||||
}
|
||||
|
||||
func (m Model) latencyHistogram(latencies []time.Duration, statuses []bool, width int) string {
|
||||
if len(latencies) == 0 || width < 30 {
|
||||
return ""
|
||||
}
|
||||
|
||||
buckets := []latencyBucket{
|
||||
{"0-50ms", 0, 50, 0},
|
||||
{"50-100ms", 50, 100, 0},
|
||||
{"100-200ms", 100, 200, 0},
|
||||
{"200-500ms", 200, 500, 0},
|
||||
{"500ms+", 500, 999999, 0},
|
||||
}
|
||||
|
||||
for i, l := range latencies {
|
||||
if i < len(statuses) && !statuses[i] {
|
||||
continue
|
||||
}
|
||||
ms := l.Milliseconds()
|
||||
for j := range buckets {
|
||||
if ms >= buckets[j].min && ms < buckets[j].max {
|
||||
buckets[j].count++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxCount := 0
|
||||
for _, b := range buckets {
|
||||
if b.count > maxCount {
|
||||
maxCount = b.count
|
||||
}
|
||||
}
|
||||
if maxCount == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
labelW := 10
|
||||
countW := len(fmt.Sprintf("%d", maxCount))
|
||||
barW := width - labelW - countW - 4
|
||||
if barW < 5 {
|
||||
barW = 5
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for _, b := range buckets {
|
||||
fill := 0
|
||||
if maxCount > 0 {
|
||||
fill = b.count * barW / maxCount
|
||||
}
|
||||
|
||||
var barColor lipgloss.Style
|
||||
switch {
|
||||
case b.min < 100:
|
||||
barColor = m.st.specialStyle
|
||||
case b.min < 500:
|
||||
barColor = m.st.warnStyle
|
||||
default:
|
||||
barColor = m.st.dangerStyle
|
||||
}
|
||||
|
||||
bar := barColor.Render(strings.Repeat("█", fill))
|
||||
if fill < barW {
|
||||
bar += m.st.subtleStyle.Render(strings.Repeat("░", barW-fill))
|
||||
}
|
||||
|
||||
label := fmt.Sprintf("%*s", labelW, b.label)
|
||||
count := fmt.Sprintf("%*d", countW, b.count)
|
||||
sb.WriteString(" " + m.st.subtleStyle.Render(label) + " " + bar + " " + count + "\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -121,6 +121,7 @@ type sessionState int
|
||||
const (
|
||||
stateDashboard sessionState = 0
|
||||
stateLogs sessionState = 1
|
||||
stateDetailFullscreen sessionState = 2
|
||||
stateAlertDetail sessionState = 3
|
||||
stateFormSite sessionState = 4
|
||||
stateFormAlert sessionState = 5
|
||||
|
||||
+82
-2
@@ -19,7 +19,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case tabDataMsg:
|
||||
return m.handleTabData(msg)
|
||||
case detailDataMsg:
|
||||
if m.detailOpen && m.cursor < len(m.sites) && m.sites[m.cursor].ID != msg.siteID {
|
||||
detailVisible := m.detailOpen || m.state == stateDetailFullscreen
|
||||
if detailVisible && m.cursor < len(m.sites) && m.sites[m.cursor].ID != msg.siteID {
|
||||
return m, nil
|
||||
}
|
||||
m.detailChanges = msg.changes
|
||||
@@ -51,6 +52,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if m.state == stateLogs {
|
||||
return m.handleLogsFullscreen(msg)
|
||||
}
|
||||
if m.state == stateDetailFullscreen {
|
||||
return m.handleDetailFullscreen(msg)
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.MouseMsg:
|
||||
@@ -203,7 +207,7 @@ func (m *Model) handleTick(t time.Time) (tea.Model, tea.Cmd) {
|
||||
// tab-data cadence, so a flap that happens while the panel is on screen shows
|
||||
// up without leaving and re-entering. Nil when no detail panel is open.
|
||||
func (m *Model) detailRefreshCmd() tea.Cmd {
|
||||
if !m.detailOpen || m.cursor >= len(m.sites) {
|
||||
if (!m.detailOpen && m.state != stateDetailFullscreen) || m.cursor >= len(m.sites) {
|
||||
return nil
|
||||
}
|
||||
return m.loadDetailCmd(m.sites[m.cursor].ID)
|
||||
@@ -278,6 +282,75 @@ func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) handleDetailFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "esc", "q":
|
||||
m.state = stateDashboard
|
||||
m.focusedPanel = panelMonitors
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "e":
|
||||
return m.handleEditItem()
|
||||
case "h":
|
||||
if m.cursor < len(m.sites) {
|
||||
site := m.sites[m.cursor]
|
||||
m.historySiteName = site.Name
|
||||
m.historySiteID = site.ID
|
||||
m.historyChanges = nil
|
||||
m.detailMode = detailHistory
|
||||
m.detailScrollOffset = 0
|
||||
return m, m.loadHistoryCmd(site.ID)
|
||||
}
|
||||
case "s":
|
||||
if m.cursor < len(m.sites) {
|
||||
site := m.sites[m.cursor]
|
||||
m.slaSiteName = site.Name
|
||||
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":
|
||||
if m.detailMode == detailSLA {
|
||||
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)
|
||||
}
|
||||
}
|
||||
case "up", "k":
|
||||
m.detailScrollOffset--
|
||||
if m.detailScrollOffset < 0 {
|
||||
m.detailScrollOffset = 0
|
||||
}
|
||||
case "down", "j":
|
||||
m.detailScrollOffset++
|
||||
case "pgup":
|
||||
m.detailScrollOffset -= 10
|
||||
if m.detailScrollOffset < 0 {
|
||||
m.detailScrollOffset = 0
|
||||
}
|
||||
case "pgdown":
|
||||
m.detailScrollOffset += 10
|
||||
}
|
||||
case tea.MouseMsg:
|
||||
switch msg.Button {
|
||||
case tea.MouseButtonWheelUp:
|
||||
m.detailScrollOffset -= 3
|
||||
if m.detailScrollOffset < 0 {
|
||||
m.detailScrollOffset = 0
|
||||
}
|
||||
case tea.MouseButtonWheelDown:
|
||||
m.detailScrollOffset += 3
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||
if m.state != stateDashboard {
|
||||
return m, nil
|
||||
@@ -673,6 +746,13 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
if len(m.sites) > 0 {
|
||||
narrow := m.termWidth < wideBreakpoint
|
||||
if narrow {
|
||||
m.detailMode = detailDefault
|
||||
m.detailScrollOffset = 0
|
||||
m.state = stateDetailFullscreen
|
||||
return m, m.loadDetailCmd(m.sites[m.cursor].ID)
|
||||
}
|
||||
m.detailOpen = !m.detailOpen
|
||||
m.detailMode = detailDefault
|
||||
m.detailScrollOffset = 0
|
||||
|
||||
@@ -300,6 +300,70 @@ func TestWriteDoneMsg_LogsErrorAndReloads(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnterNarrowTerminal_OpensFullscreen(t *testing.T) {
|
||||
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
||||
m := newTestModel(ms)
|
||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 1, Name: "site"}, SiteState: models.SiteState{Status: "UP"}}}
|
||||
m.termWidth = 80 // narrow, below wideBreakpoint (120)
|
||||
m.termHeight = 40
|
||||
m.focusedPanel = panelMonitors
|
||||
|
||||
updated, cmd := (&m).handleDashboardKey(keyMsg("enter"))
|
||||
got := updated.(*Model)
|
||||
if got.state != stateDetailFullscreen {
|
||||
t.Fatalf("expected stateDetailFullscreen, got %d", got.state)
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("expected a detail load Cmd")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnterWideTerminal_TogglesSidebar(t *testing.T) {
|
||||
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
||||
m := newTestModel(ms)
|
||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 1, Name: "site"}, SiteState: models.SiteState{Status: "UP"}}}
|
||||
m.termWidth = 140 // wide, above wideBreakpoint (120)
|
||||
m.termHeight = 40
|
||||
m.focusedPanel = panelMonitors
|
||||
|
||||
updated, _ := (&m).handleDashboardKey(keyMsg("enter"))
|
||||
got := updated.(*Model)
|
||||
if got.state != stateDashboard {
|
||||
t.Fatalf("expected stateDashboard, got %d", got.state)
|
||||
}
|
||||
if !got.detailOpen {
|
||||
t.Fatal("expected detailOpen to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailFullscreen_EscReturnsToDashboard(t *testing.T) {
|
||||
m := newTestModel(&tuiMockStore{})
|
||||
m.state = stateDetailFullscreen
|
||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 1, Name: "site"}}}
|
||||
|
||||
updated, _ := (&m).handleDetailFullscreen(tea.KeyMsg{Type: tea.KeyEsc})
|
||||
got := updated.(*Model)
|
||||
if got.state != stateDashboard {
|
||||
t.Fatalf("expected stateDashboard after Esc, got %d", got.state)
|
||||
}
|
||||
if got.focusedPanel != panelMonitors {
|
||||
t.Fatalf("expected panelMonitors focus, got %d", got.focusedPanel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailRefreshCmd_FiresForFullscreen(t *testing.T) {
|
||||
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
||||
m := newTestModel(ms)
|
||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 5, Name: "site"}}}
|
||||
m.state = stateDetailFullscreen
|
||||
m.detailOpen = false
|
||||
|
||||
cmd := (&m).detailRefreshCmd()
|
||||
if cmd == nil {
|
||||
t.Fatal("detailRefreshCmd should fire for stateDetailFullscreen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailRefreshCmd_OnlyWhileDetailOpen(t *testing.T) {
|
||||
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
||||
m := newTestModel(ms)
|
||||
|
||||
@@ -92,6 +92,8 @@ func (m Model) View() string {
|
||||
return ""
|
||||
case stateLogs:
|
||||
return m.viewLogsFullscreen()
|
||||
case stateDetailFullscreen:
|
||||
return m.viewDetailFullscreen()
|
||||
case stateAlertDetail:
|
||||
return m.viewAlertDetailPanel()
|
||||
case stateSettings:
|
||||
|
||||
@@ -2,6 +2,7 @@ package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -22,20 +23,90 @@ func (m Model) viewDetailInline(width, height int) string {
|
||||
default:
|
||||
site := m.sites[m.cursor]
|
||||
hist, _ := m.engine.GetHistory(site.ID)
|
||||
return m.viewDetailSidebar(site, hist, width, height)
|
||||
return m.buildDetailContent(site, hist, width, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, width, _ int) string {
|
||||
func (m Model) viewDetailFullscreen() string {
|
||||
if m.cursor >= len(m.sites) {
|
||||
return ""
|
||||
}
|
||||
|
||||
totalW := m.termWidth - chromePadH
|
||||
site := m.sites[m.cursor]
|
||||
|
||||
var title string
|
||||
switch m.detailMode {
|
||||
case detailSLA:
|
||||
title = "SLA · " + site.Name
|
||||
case detailHistory:
|
||||
title = "History · " + site.Name
|
||||
default:
|
||||
title = site.Name
|
||||
}
|
||||
|
||||
var breadcrumb string
|
||||
if site.ParentID > 0 {
|
||||
for _, s := range m.sites {
|
||||
if s.ID == site.ParentID {
|
||||
breadcrumb = m.st.subtleStyle.Render("Monitors > "+s.Name+" > ") + m.st.titleStyle.Render(site.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if breadcrumb == "" {
|
||||
breadcrumb = m.st.subtleStyle.Render("Monitors > ") + m.st.titleStyle.Render(title)
|
||||
}
|
||||
|
||||
header := " " + breadcrumb + "\n" + m.divider()
|
||||
|
||||
var content string
|
||||
switch m.detailMode {
|
||||
case detailSLA:
|
||||
content = m.viewSLASidebar(totalW, 0)
|
||||
case detailHistory:
|
||||
content = m.viewHistorySidebar(totalW, 0)
|
||||
default:
|
||||
hist, _ := m.engine.GetHistory(site.ID)
|
||||
content = m.buildDetailContent(site, hist, totalW, true)
|
||||
}
|
||||
|
||||
footer := m.divider() + "\n" + m.detailFooter(totalW)
|
||||
|
||||
contentLines := strings.Split(content, "\n")
|
||||
contentH := m.termHeight - 8
|
||||
if contentH < 5 {
|
||||
contentH = 5
|
||||
}
|
||||
|
||||
if m.detailScrollOffset > len(contentLines)-contentH {
|
||||
m.detailScrollOffset = len(contentLines) - contentH
|
||||
}
|
||||
if m.detailScrollOffset < 0 {
|
||||
m.detailScrollOffset = 0
|
||||
}
|
||||
|
||||
end := m.detailScrollOffset + contentH
|
||||
if end > len(contentLines) {
|
||||
end = len(contentLines)
|
||||
}
|
||||
visible := strings.Join(contentLines[m.detailScrollOffset:end], "\n")
|
||||
|
||||
return lipgloss.NewStyle().Padding(1, 2).Render(
|
||||
header + "\n" + visible + "\n" + footer)
|
||||
}
|
||||
|
||||
func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, width int, fullscreen bool) string {
|
||||
dot := m.st.subtleStyle.Render(" · ")
|
||||
label := m.st.subtleStyle
|
||||
var b strings.Builder
|
||||
innerW := width - 4
|
||||
if innerW < 20 {
|
||||
innerW = 20
|
||||
}
|
||||
|
||||
// Status + latency + last check
|
||||
var b strings.Builder
|
||||
|
||||
// Status + latency + last check + state since
|
||||
status := m.fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID))
|
||||
statusParts := []string{status}
|
||||
if site.Latency > 0 {
|
||||
@@ -44,6 +115,10 @@ func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, wid
|
||||
if !site.LastCheck.IsZero() {
|
||||
statusParts = append(statusParts, m.fmtTimeAgo(site.LastCheck))
|
||||
}
|
||||
if !site.StatusChangedAt.IsZero() {
|
||||
dur := time.Since(site.StatusChangedAt)
|
||||
statusParts = append(statusParts, label.Render("for")+" "+fmtDuration(dur))
|
||||
}
|
||||
b.WriteString(" " + strings.Join(statusParts, dot) + "\n")
|
||||
|
||||
// Type-specific details
|
||||
@@ -52,7 +127,10 @@ func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, wid
|
||||
b.WriteString(" " + strings.Join(typeParts, dot) + "\n")
|
||||
}
|
||||
|
||||
// Uptime + retries
|
||||
// Extended endpoint fields
|
||||
m.writeEndpointFields(&b, site, label, innerW, fullscreen)
|
||||
|
||||
// Uptime + retries + last success
|
||||
uptimeStr := m.fmtUptime(hist.Statuses)
|
||||
if m.isMonitorInMaintenance(site.ID) {
|
||||
uptimeStr = m.st.subtleStyle.Render("—")
|
||||
@@ -61,8 +139,21 @@ func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, wid
|
||||
if site.Type != "group" && site.MaxRetries > 0 {
|
||||
uptimeParts = append(uptimeParts, label.Render("Retries")+" "+m.fmtRetries(site))
|
||||
}
|
||||
if site.Type != "push" && !site.LastSuccessAt.IsZero() {
|
||||
uptimeParts = append(uptimeParts, label.Render("Last OK")+" "+m.fmtTimeAgo(site.LastSuccessAt))
|
||||
}
|
||||
b.WriteString(" " + strings.Join(uptimeParts, dot) + "\n")
|
||||
|
||||
// Maintenance window name
|
||||
if m.isMonitorInMaintenance(site.ID) {
|
||||
for _, mw := range m.maintenanceWindows {
|
||||
if mw.Type == "maintenance" && (mw.MonitorID == 0 || mw.MonitorID == site.ID || mw.MonitorID == site.ParentID) {
|
||||
b.WriteString(" " + label.Render("Maint") + " " + m.st.maintStyle.Render(mw.Title) + "\n")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error line
|
||||
if (site.Status == models.StatusDown || site.Status == models.StatusSSLExp ||
|
||||
site.Status == models.StatusLate || site.Status == models.StatusStale) && site.LastError != "" {
|
||||
@@ -73,7 +164,61 @@ func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, wid
|
||||
b.WriteString(" " + label.Render("Error") + " " + m.st.dangerStyle.Render(limitStr(site.LastError, errW)) + "\n")
|
||||
}
|
||||
|
||||
// Connection chain
|
||||
if (site.Status == models.StatusDown || site.Status == models.StatusSSLExp) && site.LastError != "" {
|
||||
chain := connectionChain(site.LastError, site.Type, site.StatusCode, strings.HasPrefix(site.URL, "https"))
|
||||
if len(chain) > 0 {
|
||||
b.WriteString("\n")
|
||||
for _, step := range chain {
|
||||
var icon string
|
||||
switch step.Status {
|
||||
case stepPassed:
|
||||
icon = m.st.specialStyle.Render("✓")
|
||||
case stepFailed:
|
||||
icon = m.st.dangerStyle.Render("✗")
|
||||
case stepSkipped:
|
||||
icon = m.st.subtleStyle.Render("·")
|
||||
}
|
||||
line := fmt.Sprintf(" %s %-16s", icon, step.Name)
|
||||
if step.Detail != "" {
|
||||
switch step.Status {
|
||||
case stepFailed:
|
||||
line += " " + m.st.dangerStyle.Render(step.Detail)
|
||||
case stepSkipped:
|
||||
line += " " + m.st.subtleStyle.Render(step.Detail)
|
||||
}
|
||||
}
|
||||
b.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
|
||||
// Probe results
|
||||
probeResults := m.engine.GetProbeResults(site.ID)
|
||||
if len(probeResults) > 0 {
|
||||
nodeIDs := make([]string, 0, len(probeResults))
|
||||
for id := range probeResults {
|
||||
nodeIDs = append(nodeIDs, id)
|
||||
}
|
||||
sort.Strings(nodeIDs)
|
||||
for _, nodeID := range nodeIDs {
|
||||
result := probeResults[nodeID]
|
||||
probeStatus := m.st.specialStyle.Render("UP")
|
||||
if !result.IsUp {
|
||||
probeStatus = m.st.dangerStyle.Render("DN")
|
||||
}
|
||||
latency := time.Duration(result.LatencyNs).Milliseconds()
|
||||
ago := time.Since(result.CheckedAt).Truncate(time.Second)
|
||||
line := fmt.Sprintf(" %-14s %s %dms %s ago", nodeID, probeStatus, latency, ago)
|
||||
if !result.IsUp && result.ErrorReason != "" {
|
||||
line += " " + m.st.dangerStyle.Render(result.ErrorReason)
|
||||
}
|
||||
b.WriteString(line + "\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// Latency chart
|
||||
if len(hist.Latencies) > 0 {
|
||||
@@ -116,6 +261,15 @@ func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, wid
|
||||
}
|
||||
}
|
||||
|
||||
// Latency histogram
|
||||
if site.Type != "push" && len(hist.Latencies) > 5 {
|
||||
histContent := m.latencyHistogram(hist.Latencies, hist.Statuses, innerW)
|
||||
if histContent != "" {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(histContent)
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
|
||||
// State changes
|
||||
@@ -150,6 +304,47 @@ func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, wid
|
||||
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
||||
}
|
||||
|
||||
func (m Model) writeEndpointFields(b *strings.Builder, site models.Site, label lipgloss.Style, innerW int, fullscreen bool) {
|
||||
dot := m.st.subtleStyle.Render(" · ")
|
||||
var fields []string
|
||||
|
||||
if site.Interval > 0 {
|
||||
fields = append(fields, label.Render("Every")+" "+fmt.Sprintf("%ds", site.Interval))
|
||||
}
|
||||
if site.Timeout > 0 {
|
||||
fields = append(fields, label.Render("Timeout")+" "+fmt.Sprintf("%ds", site.Timeout))
|
||||
}
|
||||
if site.Type == "http" && site.Method != "" && site.Method != "GET" {
|
||||
fields = append(fields, label.Render("Method")+" "+site.Method)
|
||||
}
|
||||
if site.Type == "http" {
|
||||
codes := site.AcceptedCodes
|
||||
if codes == "" {
|
||||
codes = "200-299"
|
||||
}
|
||||
fields = append(fields, label.Render("Codes")+" "+codes)
|
||||
}
|
||||
if site.Regions != "" {
|
||||
fields = append(fields, label.Render("Regions")+" "+site.Regions)
|
||||
}
|
||||
|
||||
if len(fields) > 0 {
|
||||
b.WriteString(" " + strings.Join(fields, dot) + "\n")
|
||||
}
|
||||
|
||||
if site.Description != "" {
|
||||
maxDescW := innerW
|
||||
if !fullscreen && maxDescW > 60 {
|
||||
maxDescW = 60
|
||||
}
|
||||
b.WriteString(" " + label.Render(limitStr(site.Description, maxDescW)) + "\n")
|
||||
}
|
||||
|
||||
if site.Type == "push" && site.Token != "" {
|
||||
b.WriteString(" " + label.Render("Token") + " " + site.Token + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) detailTypeLine(site models.Site) []string {
|
||||
label := m.st.subtleStyle
|
||||
var parts []string
|
||||
|
||||
Reference in New Issue
Block a user