feat(tui): enter opens detail sidebar, logs as bottom strip
Enter on a site now opens the detail sidebar instead of a full-page takeover. Enter on a group expands/collapses children (same as space). Logs panel moves to a toggleable bottom strip via the l key with j/k scrolling when focused.
This commit is contained in:
@@ -2,8 +2,76 @@ package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderCompactLogLine(entry models.LogEntry, maxW int) string {
|
||||
sev := classifyLog(entry.Message)
|
||||
|
||||
var tag string
|
||||
switch sev {
|
||||
case severityDown:
|
||||
tag = m.st.dangerStyle.Render("▼")
|
||||
case severityUp:
|
||||
tag = m.st.specialStyle.Render("▲")
|
||||
case severityWarn:
|
||||
tag = m.st.warnStyle.Render("◆")
|
||||
case severitySystem:
|
||||
tag = m.st.titleStyle.Render("●")
|
||||
default:
|
||||
tag = m.st.subtleStyle.Render("·")
|
||||
}
|
||||
|
||||
ts := entry.CreatedAt.Local().Format("01/02 15:04")
|
||||
|
||||
msg := entry.Message
|
||||
msg = strings.TrimPrefix(msg, "Monitor ")
|
||||
msg = strings.TrimPrefix(msg, "Push ")
|
||||
|
||||
prefixW := len(ts) + 4
|
||||
msgW := maxW - prefixW
|
||||
if msgW < 5 {
|
||||
msgW = 5
|
||||
}
|
||||
msg = limitStr(msg, msgW)
|
||||
|
||||
return " " + m.st.subtleStyle.Render(ts) + " " + tag + " " + msg
|
||||
}
|
||||
|
||||
func (m Model) viewLogsStrip(width, maxLines int) string {
|
||||
logs := m.engine.GetLogs()
|
||||
if len(logs) == 0 {
|
||||
return m.st.subtleStyle.Render(" No logs yet")
|
||||
}
|
||||
|
||||
style := lipgloss.NewStyle().Width(width).MaxWidth(width)
|
||||
|
||||
var all []string
|
||||
for _, entry := range logs {
|
||||
if strings.TrimSpace(entry.Message) == "" {
|
||||
continue
|
||||
}
|
||||
if m.logFilterImportant && !isImportantLog(classifyLog(entry.Message)) {
|
||||
continue
|
||||
}
|
||||
all = append(all, m.renderCompactLogLine(entry, width))
|
||||
}
|
||||
|
||||
start := m.logScrollOffset
|
||||
if start > len(all) {
|
||||
start = len(all)
|
||||
}
|
||||
end := start + maxLines
|
||||
if end > len(all) {
|
||||
end = len(all)
|
||||
}
|
||||
visible := all[start:end]
|
||||
|
||||
return style.Render(strings.Join(visible, "\n"))
|
||||
}
|
||||
|
||||
func (m *Model) scrollLogs(delta int) {
|
||||
logs := m.engine.GetLogs()
|
||||
total := 0
|
||||
|
||||
+30
-6
@@ -155,11 +155,16 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
const logsStripHeight = 6
|
||||
|
||||
func (m *Model) recalcLayout() {
|
||||
chrome := chromeBase
|
||||
if m.filterMode || m.filterText != "" {
|
||||
chrome++
|
||||
}
|
||||
if m.logsOpen {
|
||||
chrome += logsStripHeight
|
||||
}
|
||||
m.maxTableRows = m.termHeight - chrome
|
||||
if m.maxTableRows < 1 {
|
||||
m.maxTableRows = 1
|
||||
@@ -751,10 +756,14 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.recalcLayout()
|
||||
}
|
||||
case "l":
|
||||
m.refreshLogContent()
|
||||
m.logViewport.GotoTop()
|
||||
m.state = stateLogs
|
||||
return m, nil
|
||||
if m.logsOpen {
|
||||
m.logsOpen = false
|
||||
m.focusedPanel = panelMonitors
|
||||
} else {
|
||||
m.logsOpen = true
|
||||
m.focusedPanel = panelLogs
|
||||
}
|
||||
m.recalcLayout()
|
||||
case "up", "k":
|
||||
if m.focusedPanel == panelMaint {
|
||||
m.scrollMaintCursor(-1)
|
||||
@@ -818,8 +827,23 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
if len(m.sites) > 0 {
|
||||
m.state = stateDetail
|
||||
return m, m.loadDetailCmd(m.sites[m.cursor].ID)
|
||||
site := m.sites[m.cursor]
|
||||
if site.Type == "group" {
|
||||
m.collapsed[site.ID] = !m.collapsed[site.ID]
|
||||
payload := collapsedJSON(m.collapsed)
|
||||
st := m.store
|
||||
ctx := m.ctx
|
||||
m.refreshLive()
|
||||
return m, writeCmd("Save collapsed groups", func() error {
|
||||
return st.SetPreference(ctx, "collapsed_groups", payload)
|
||||
})
|
||||
}
|
||||
if !m.detailOpen {
|
||||
m.detailOpen = true
|
||||
m.detailMode = detailDefault
|
||||
m.recalcLayout()
|
||||
}
|
||||
return m, m.loadDetailCmd(site.ID)
|
||||
}
|
||||
case "e":
|
||||
return m.handleEditItem()
|
||||
|
||||
@@ -201,7 +201,14 @@ func (m Model) viewMonitorsLayout() string {
|
||||
topParts = append(topParts, detailPanel)
|
||||
}
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Top, topParts...)
|
||||
top := lipgloss.JoinHorizontal(lipgloss.Top, topParts...)
|
||||
|
||||
if m.logsOpen {
|
||||
logContent := m.viewLogsStrip(availW-2, logsStripHeight-2)
|
||||
logPanel := m.zones.Mark("panel-logs", m.titledPanel("Logs", logContent, availW, m.focusedPanel == panelLogs))
|
||||
return top + "\n" + logPanel
|
||||
}
|
||||
return top
|
||||
}
|
||||
|
||||
func (m Model) viewDashboard() string {
|
||||
@@ -285,12 +292,14 @@ func (m Model) renderFooter(_ dashboardStats) string {
|
||||
var keys string
|
||||
if m.focusedPanel == panelMaint {
|
||||
keys = "[n]New [x]End [d]Del [Esc]Back [S]Settings [T]Theme [q]Quit"
|
||||
} else if m.focusedPanel == panelLogs {
|
||||
keys = "[↑/↓]Scroll [Enter]Expand [l/Esc]Back [S]Settings [T]Theme [q]Quit"
|
||||
} else if m.detailOpen && m.detailMode == detailSLA {
|
||||
keys = "[1-4]Period [Esc]Back [S]Settings [T]Theme [q]Quit"
|
||||
keys = "[1-4]Period [Esc]Back [l]Logs [S]Settings [T]Theme [q]Quit"
|
||||
} else if m.detailOpen && m.detailMode == detailHistory {
|
||||
keys = "[Esc]Back [S]Settings [T]Theme [q]Quit"
|
||||
keys = "[Esc]Back [l]Logs [S]Settings [T]Theme [q]Quit"
|
||||
} else if m.detailOpen {
|
||||
keys = "[i]Close [Enter]Expand [h]History [s]SLA [e]Edit [m]Maint [l]Logs [S]Settings [T]Theme [q]Quit"
|
||||
keys = "[i]Close [h]History [s]SLA [e]Edit [m]Maint [l]Logs [S]Settings [T]Theme [q]Quit"
|
||||
} else {
|
||||
keys = "[/]Filter [i]Info [Enter]Detail [n]New [e]Edit [d]Del [m]Maint [l]Logs [S]Settings [T]Theme [q]Quit"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user