Compare commits
8 Commits
b90033c7f0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
c1955a4c80
|
|||
|
ce3abda5b9
|
|||
|
ce3cfad7f8
|
|||
|
d6e6011b35
|
|||
|
1cfa0571c8
|
|||
|
d6ba7d9af8
|
|||
|
14cec4283d
|
|||
|
a32a443a4a
|
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# --- Stage 1: Builder ---
|
# --- Stage 1: Builder ---
|
||||||
FROM golang:1.26.4-alpine3.23@sha256:f23e8b227fb4493eabe03bede4d5a32d04092da71962f1fb79b5f7d1e6c2a17f AS builder
|
FROM golang:1.26.5-alpine3.23@sha256:622e56dbc11a8cfe87cafa2331e9a201877271cbff918af53d3be315f3da88cc AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module gitea.lerkolabs.com/lerkolabs/uptop
|
module gitea.lerkolabs.com/lerkolabs/uptop
|
||||||
|
|
||||||
go 1.26.4
|
go 1.26.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
|
||||||
|
|||||||
+129
-2
@@ -50,6 +50,21 @@ func writeCmd(op string, fn func() error) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) saveBottomPanelPref() tea.Cmd {
|
||||||
|
v := "logs"
|
||||||
|
switch m.bottomPanel {
|
||||||
|
case bottomNone:
|
||||||
|
v = "none"
|
||||||
|
case bottomMaint:
|
||||||
|
v = "maint"
|
||||||
|
}
|
||||||
|
st := m.store
|
||||||
|
ctx := m.ctx
|
||||||
|
return writeCmd("Save bottom panel preference", func() error {
|
||||||
|
return st.SetPreference(ctx, "bottom_panel", v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func sortSitesForDisplay(allSites []models.Site, collapsed map[int]bool, sortCol int, sortAsc bool) []models.Site {
|
func sortSitesForDisplay(allSites []models.Site, collapsed map[int]bool, sortCol int, sortAsc bool) []models.Site {
|
||||||
var groups, ungrouped []models.Site
|
var groups, ungrouped []models.Site
|
||||||
children := make(map[int][]models.Site)
|
children := make(map[int][]models.Site)
|
||||||
@@ -62,8 +77,6 @@ func sortSitesForDisplay(allSites []models.Site, collapsed map[int]bool, sortCol
|
|||||||
ungrouped = append(ungrouped, s)
|
ungrouped = append(ungrouped, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Slice(groups, func(i, j int) bool { return groups[i].ID < groups[j].ID })
|
|
||||||
|
|
||||||
sortSlice := func(s []models.Site) {
|
sortSlice := func(s []models.Site) {
|
||||||
sort.Slice(s, func(i, j int) bool { return s[i].ID < s[j].ID })
|
sort.Slice(s, func(i, j int) bool { return s[i].ID < s[j].ID })
|
||||||
sort.SliceStable(s, func(i, j int) bool {
|
sort.SliceStable(s, func(i, j int) bool {
|
||||||
@@ -83,6 +96,7 @@ func sortSitesForDisplay(allSites []models.Site, collapsed map[int]bool, sortCol
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sortSlice(groups)
|
||||||
for pid := range children {
|
for pid := range children {
|
||||||
c := children[pid]
|
c := children[pid]
|
||||||
sortSlice(c)
|
sortSlice(c)
|
||||||
@@ -123,6 +137,7 @@ func (m *Model) refreshLive() {
|
|||||||
ordered = filterSites(ordered, m.filterText)
|
ordered = filterSites(ordered, m.filterText)
|
||||||
}
|
}
|
||||||
m.sites = ordered
|
m.sites = ordered
|
||||||
|
m.buildMaintSet()
|
||||||
m.refreshLogContent()
|
m.refreshLogContent()
|
||||||
|
|
||||||
if m.selectedID != 0 {
|
if m.selectedID != 0 {
|
||||||
@@ -153,6 +168,118 @@ func (m *Model) clampCursor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) detailCmdIfNeeded() tea.Cmd {
|
||||||
|
if m.focusedPanel == panelMonitors && m.detailOpen && m.cursor < len(m.sites) {
|
||||||
|
m.detailMode = detailDefault
|
||||||
|
m.detailScrollOffset = 0
|
||||||
|
return m.loadDetailCmd(m.sites[m.cursor].ID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) jumpToTop() {
|
||||||
|
switch m.focusedPanel {
|
||||||
|
case panelDetail:
|
||||||
|
m.detailScrollOffset = 0
|
||||||
|
case panelMaint:
|
||||||
|
m.maintCursor = 0
|
||||||
|
case panelLogs:
|
||||||
|
m.logScrollOffset = 0
|
||||||
|
default:
|
||||||
|
m.cursor = 0
|
||||||
|
m.tableOffset = 0
|
||||||
|
m.syncSelectedID()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) jumpToBottom() {
|
||||||
|
switch m.focusedPanel {
|
||||||
|
case panelDetail:
|
||||||
|
m.detailScrollOffset = 9999
|
||||||
|
case panelMaint:
|
||||||
|
windows := m.activeMaintWindows()
|
||||||
|
if len(windows) > 0 {
|
||||||
|
m.maintCursor = len(windows) - 1
|
||||||
|
}
|
||||||
|
case panelLogs:
|
||||||
|
total := m.filteredLogCount()
|
||||||
|
if total > 0 {
|
||||||
|
m.logScrollOffset = total - 1
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
max := m.currentListLen() - 1
|
||||||
|
if max >= 0 {
|
||||||
|
m.cursor = max
|
||||||
|
if m.cursor >= m.tableOffset+m.maxTableRows {
|
||||||
|
m.tableOffset = m.cursor - m.maxTableRows + 1
|
||||||
|
}
|
||||||
|
m.syncSelectedID()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) halfPageUp() {
|
||||||
|
half := m.maxTableRows / 2
|
||||||
|
if half < 1 {
|
||||||
|
half = 1
|
||||||
|
}
|
||||||
|
switch m.focusedPanel {
|
||||||
|
case panelDetail:
|
||||||
|
m.detailScrollOffset -= half
|
||||||
|
if m.detailScrollOffset < 0 {
|
||||||
|
m.detailScrollOffset = 0
|
||||||
|
}
|
||||||
|
case panelMaint:
|
||||||
|
m.maintCursor -= half
|
||||||
|
if m.maintCursor < 0 {
|
||||||
|
m.maintCursor = 0
|
||||||
|
}
|
||||||
|
case panelLogs:
|
||||||
|
m.scrollLogs(-half)
|
||||||
|
default:
|
||||||
|
m.cursor -= half
|
||||||
|
if m.cursor < 0 {
|
||||||
|
m.cursor = 0
|
||||||
|
}
|
||||||
|
if m.cursor < m.tableOffset {
|
||||||
|
m.tableOffset = m.cursor
|
||||||
|
}
|
||||||
|
m.syncSelectedID()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) halfPageDown() {
|
||||||
|
half := m.maxTableRows / 2
|
||||||
|
if half < 1 {
|
||||||
|
half = 1
|
||||||
|
}
|
||||||
|
switch m.focusedPanel {
|
||||||
|
case panelDetail:
|
||||||
|
m.detailScrollOffset += half
|
||||||
|
case panelMaint:
|
||||||
|
windows := m.activeMaintWindows()
|
||||||
|
m.maintCursor += half
|
||||||
|
if len(windows) > 0 && m.maintCursor >= len(windows) {
|
||||||
|
m.maintCursor = len(windows) - 1
|
||||||
|
}
|
||||||
|
case panelLogs:
|
||||||
|
m.scrollLogs(half)
|
||||||
|
default:
|
||||||
|
max := m.currentListLen() - 1
|
||||||
|
m.cursor += half
|
||||||
|
if m.cursor > max {
|
||||||
|
m.cursor = max
|
||||||
|
}
|
||||||
|
if m.cursor < 0 {
|
||||||
|
m.cursor = 0
|
||||||
|
}
|
||||||
|
if m.cursor >= m.tableOffset+m.maxTableRows {
|
||||||
|
m.tableOffset = m.cursor - m.maxTableRows + 1
|
||||||
|
}
|
||||||
|
m.syncSelectedID()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// loadTabDataCmd returns a tea.Cmd that loads the DB-backed tab tables off the
|
// loadTabDataCmd returns a tea.Cmd that loads the DB-backed tab tables off the
|
||||||
// UI goroutine. Each call bumps tabSeq and stamps the reply with it, so
|
// UI goroutine. Each call bumps tabSeq and stamps the reply with it, so
|
||||||
// handleTabData can drop out-of-order results from slower earlier loads. The
|
// handleTabData can drop out-of-order results from slower earlier loads. The
|
||||||
|
|||||||
+55
-9
@@ -6,7 +6,13 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset int, focused bool) string {
|
type scrollbar struct {
|
||||||
|
pos int
|
||||||
|
total int
|
||||||
|
visible int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset int, sb scrollbar, focused bool) string {
|
||||||
if height <= 0 {
|
if height <= 0 {
|
||||||
return m.titledPanel(title, content, width, focused)
|
return m.titledPanel(title, content, width, focused)
|
||||||
}
|
}
|
||||||
@@ -64,21 +70,61 @@ func (m Model) titledPanelH(title, content, footer string, width, height, scroll
|
|||||||
}
|
}
|
||||||
visible := contentLines[scrollOffset:end]
|
visible := contentLines[scrollOffset:end]
|
||||||
|
|
||||||
borderLine := func(line string) string {
|
if sb.total == 0 && len(contentLines) > bodyH {
|
||||||
return bc.Render("│") + line + strings.Repeat(" ", max(0, innerW-lipgloss.Width(line))) + bc.Render("│")
|
sb = scrollbar{pos: scrollOffset, total: len(contentLines), visible: bodyH}
|
||||||
|
}
|
||||||
|
sbVisible := sb.visible
|
||||||
|
if sbVisible <= 0 {
|
||||||
|
sbVisible = bodyH
|
||||||
|
}
|
||||||
|
showScrollbar := sb.total > 0 && sb.total > sbVisible
|
||||||
|
var thumbStart, thumbEnd int
|
||||||
|
if showScrollbar {
|
||||||
|
thumbSize := bodyH * sbVisible / sb.total
|
||||||
|
if thumbSize < 1 {
|
||||||
|
thumbSize = 1
|
||||||
|
}
|
||||||
|
scrollRange := sb.total - sbVisible
|
||||||
|
if scrollRange < 1 {
|
||||||
|
scrollRange = 1
|
||||||
|
}
|
||||||
|
trackSpace := bodyH - thumbSize
|
||||||
|
thumbStart = sb.pos * trackSpace / scrollRange
|
||||||
|
if thumbStart < 0 {
|
||||||
|
thumbStart = 0
|
||||||
|
}
|
||||||
|
thumbEnd = thumbStart + thumbSize
|
||||||
|
if thumbEnd > bodyH {
|
||||||
|
thumbEnd = bodyH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTrack := lipgloss.NewStyle().Foreground(m.theme.Border).Render("░")
|
||||||
|
scrollThumb := lipgloss.NewStyle().Foreground(m.theme.Accent).Render("█")
|
||||||
|
|
||||||
|
borderLine := func(line string, idx int) string {
|
||||||
|
rightBorder := bc.Render("│")
|
||||||
|
if showScrollbar && idx >= thumbStart && idx < thumbEnd {
|
||||||
|
rightBorder = scrollThumb
|
||||||
|
} else if showScrollbar {
|
||||||
|
rightBorder = scrollTrack
|
||||||
|
}
|
||||||
|
return bc.Render("│") + line + strings.Repeat(" ", max(0, innerW-lipgloss.Width(line))) + rightBorder
|
||||||
|
}
|
||||||
|
emptyLine := func(idx int) string {
|
||||||
|
return borderLine(strings.Repeat(" ", innerW), idx)
|
||||||
}
|
}
|
||||||
emptyLine := borderLine(strings.Repeat(" ", innerW))
|
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
lines = append(lines, top)
|
lines = append(lines, top)
|
||||||
for _, line := range visible {
|
for i, line := range visible {
|
||||||
lines = append(lines, borderLine(line))
|
lines = append(lines, borderLine(line, i))
|
||||||
}
|
}
|
||||||
for len(lines) < height-1-len(footerLines) {
|
for i := len(visible); len(lines) < height-1-len(footerLines); i++ {
|
||||||
lines = append(lines, emptyLine)
|
lines = append(lines, emptyLine(i))
|
||||||
}
|
}
|
||||||
for _, line := range footerLines {
|
for _, line := range footerLines {
|
||||||
lines = append(lines, borderLine(line))
|
lines = append(lines, borderLine(line, -1))
|
||||||
}
|
}
|
||||||
lines = append(lines, bottom)
|
lines = append(lines, bottom)
|
||||||
|
|
||||||
|
|||||||
@@ -156,9 +156,8 @@ func resolveSparklineIndex(x, sparkWidth, dataLen int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) groupSparkline(groupID int, width int, bg lipgloss.TerminalColor) string {
|
func (m Model) groupSparkline(groupID int, width int, bg lipgloss.TerminalColor) string {
|
||||||
allSites := m.engine.GetAllSites()
|
|
||||||
var childStatuses [][]bool
|
var childStatuses [][]bool
|
||||||
for _, s := range allSites {
|
for _, s := range m.sites {
|
||||||
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
||||||
hist, _ := m.engine.GetHistory(s.ID)
|
hist, _ := m.engine.GetHistory(s.ID)
|
||||||
if len(hist.Statuses) > 0 {
|
if len(hist.Statuses) > 0 {
|
||||||
@@ -209,9 +208,8 @@ func (m Model) groupSparkline(groupID int, width int, bg lipgloss.TerminalColor)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) groupUptime(groupID int) string {
|
func (m Model) groupUptime(groupID int) string {
|
||||||
allSites := m.engine.GetAllSites()
|
|
||||||
var allStatuses [][]bool
|
var allStatuses [][]bool
|
||||||
for _, s := range allSites {
|
for _, s := range m.sites {
|
||||||
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
||||||
hist, _ := m.engine.GetHistory(s.ID)
|
hist, _ := m.engine.GetHistory(s.ID)
|
||||||
if len(hist.Statuses) > 0 {
|
if len(hist.Statuses) > 0 {
|
||||||
|
|||||||
@@ -72,6 +72,20 @@ func (m Model) viewLogsStrip(width, maxLines int) string {
|
|||||||
return style.Render(strings.Join(visible, "\n"))
|
return style.Render(strings.Join(visible, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) filteredLogCount() int {
|
||||||
|
count := 0
|
||||||
|
for _, entry := range m.engine.GetLogs() {
|
||||||
|
if strings.TrimSpace(entry.Message) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if m.logFilterImportant && !isImportantLog(classifyLog(entry.Message)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) scrollLogs(delta int) {
|
func (m *Model) scrollLogs(delta int) {
|
||||||
logs := m.engine.GetLogs()
|
logs := m.engine.GetLogs()
|
||||||
total := 0
|
total := 0
|
||||||
|
|||||||
@@ -21,27 +21,36 @@ type maintFormData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) isMonitorInMaintenance(monitorID int) bool {
|
func (m Model) isMonitorInMaintenance(monitorID int) bool {
|
||||||
|
return m.maintSet[monitorID]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) buildMaintSet() {
|
||||||
|
set := make(map[int]bool)
|
||||||
|
now := time.Now()
|
||||||
for _, mw := range m.maintenanceWindows {
|
for _, mw := range m.maintenanceWindows {
|
||||||
if mw.Type != "maintenance" {
|
if mw.Type != "maintenance" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
now := time.Now()
|
|
||||||
if mw.StartTime.After(now) {
|
if mw.StartTime.After(now) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if mw.MonitorID == 0 || mw.MonitorID == monitorID {
|
if mw.MonitorID == 0 {
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, s := range m.sites {
|
for _, s := range m.sites {
|
||||||
if s.ID == monitorID && s.ParentID > 0 && mw.MonitorID == s.ParentID {
|
set[s.ID] = true
|
||||||
return true
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
set[mw.MonitorID] = true
|
||||||
|
for _, s := range m.sites {
|
||||||
|
if s.ParentID == mw.MonitorID {
|
||||||
|
set[s.ID] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
m.maintSet = set
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) initMaintHuhForm() tea.Cmd {
|
func (m *Model) initMaintHuhForm() tea.Cmd {
|
||||||
|
|||||||
+91
-47
@@ -46,10 +46,12 @@ type Theme struct {
|
|||||||
|
|
||||||
var themes = []Theme{
|
var themes = []Theme{
|
||||||
themeFlexokiDark,
|
themeFlexokiDark,
|
||||||
|
themeEverforest,
|
||||||
|
themeKanagawa,
|
||||||
themeTokyoNight,
|
themeTokyoNight,
|
||||||
themeCatppuccinMocha,
|
themeCatppuccinMocha,
|
||||||
themeNord,
|
themeRosePine,
|
||||||
themeGruvbox,
|
themeDracula,
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeFlexokiDark = Theme{
|
var themeFlexokiDark = Theme{
|
||||||
@@ -80,7 +82,7 @@ var themeTokyoNight = Theme{
|
|||||||
Panel: cc("#292e42", ""),
|
Panel: cc("#292e42", ""),
|
||||||
Border: cc("#3b4261", "8"),
|
Border: cc("#3b4261", "8"),
|
||||||
Fg: cc("#c0caf5", "15"),
|
Fg: cc("#c0caf5", "15"),
|
||||||
Muted: cc("#a9b1d6", "7"),
|
Muted: cc("#7982a9", "7"),
|
||||||
Subtle: cc("#565f89", "7"),
|
Subtle: cc("#565f89", "7"),
|
||||||
Success: cc("#9ece6a", "10"),
|
Success: cc("#9ece6a", "10"),
|
||||||
Warning: cc("#e0af68", "11"),
|
Warning: cc("#e0af68", "11"),
|
||||||
@@ -91,28 +93,28 @@ var themeTokyoNight = Theme{
|
|||||||
Purple: cc("#bb9af7", "13"),
|
Purple: cc("#bb9af7", "13"),
|
||||||
ZebraBg: cc("#1c1d28", ""),
|
ZebraBg: cc("#1c1d28", ""),
|
||||||
SelectedFg: cc("#c0caf5", "15"),
|
SelectedFg: cc("#c0caf5", "15"),
|
||||||
SelectedBg: cc("#292e42", "4"),
|
SelectedBg: cc("#363c53", "4"),
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeGruvbox = Theme{
|
var themeDracula = Theme{
|
||||||
Name: "Gruvbox",
|
Name: "Dracula",
|
||||||
Bg: cc("#282828", ""),
|
Bg: cc("#282a36", ""),
|
||||||
Surface: cc("#3c3836", ""),
|
Surface: cc("#343746", ""),
|
||||||
Panel: cc("#504945", ""),
|
Panel: cc("#44475a", ""),
|
||||||
Border: cc("#665c54", "8"),
|
Border: cc("#6272a4", "8"),
|
||||||
Fg: cc("#ebdbb2", "15"),
|
Fg: cc("#f8f8f2", "15"),
|
||||||
Muted: cc("#bdae93", "7"),
|
Muted: cc("#a9b0cb", "7"),
|
||||||
Subtle: cc("#7c6f64", "7"),
|
Subtle: cc("#6272a4", "7"),
|
||||||
Success: cc("#b8bb26", "10"),
|
Success: cc("#50fa7b", "10"),
|
||||||
Warning: cc("#fabd2f", "11"),
|
Warning: cc("#f1fa8c", "11"),
|
||||||
Stale: cc("#fe8019", "3"),
|
Stale: cc("#ffb86c", "3"),
|
||||||
Danger: cc("#fb4934", "9"),
|
Danger: cc("#ff5555", "9"),
|
||||||
Info: cc("#83a598", "12"),
|
Info: cc("#8be9fd", "12"),
|
||||||
Accent: cc("#8ec07c", "14"),
|
Accent: cc("#bd93f9", "14"),
|
||||||
Purple: cc("#d3869b", "13"),
|
Purple: cc("#ff79c6", "13"),
|
||||||
ZebraBg: cc("#2a2a2a", ""),
|
ZebraBg: cc("#2c2e3a", ""),
|
||||||
SelectedFg: cc("#fbf1c7", "15"),
|
SelectedFg: cc("#f8f8f2", "15"),
|
||||||
SelectedBg: cc("#504945", "4"),
|
SelectedBg: cc("#52556b", "4"),
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeCatppuccinMocha = Theme{
|
var themeCatppuccinMocha = Theme{
|
||||||
@@ -124,37 +126,79 @@ var themeCatppuccinMocha = Theme{
|
|||||||
Fg: cc("#cdd6f4", "15"),
|
Fg: cc("#cdd6f4", "15"),
|
||||||
Muted: cc("#a6adc8", "7"),
|
Muted: cc("#a6adc8", "7"),
|
||||||
Subtle: cc("#6c7086", "7"),
|
Subtle: cc("#6c7086", "7"),
|
||||||
Success: cc("#a6e3a1", "10"),
|
Success: cc("#7dc47a", "10"),
|
||||||
Warning: cc("#f9e2af", "11"),
|
Warning: cc("#f0c644", "11"),
|
||||||
Stale: cc("#fab387", "3"),
|
Stale: cc("#fab387", "3"),
|
||||||
Danger: cc("#f38ba8", "9"),
|
Danger: cc("#e6546e", "9"),
|
||||||
Info: cc("#89b4fa", "12"),
|
Info: cc("#89b4fa", "12"),
|
||||||
Accent: cc("#94e2d5", "14"),
|
Accent: cc("#94e2d5", "14"),
|
||||||
Purple: cc("#cba6f7", "13"),
|
Purple: cc("#cba6f7", "13"),
|
||||||
ZebraBg: cc("#232334", ""),
|
ZebraBg: cc("#212130", ""),
|
||||||
SelectedFg: cc("#cdd6f4", "15"),
|
SelectedFg: cc("#cdd6f4", "15"),
|
||||||
SelectedBg: cc("#45475a", "4"),
|
SelectedBg: cc("#585b70", "4"),
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeNord = Theme{
|
var themeRosePine = Theme{
|
||||||
Name: "Nord",
|
Name: "Rosé Pine",
|
||||||
Bg: cc("#2e3440", ""),
|
Bg: cc("#191724", ""),
|
||||||
Surface: cc("#3b4252", ""),
|
Surface: cc("#1f1d2e", ""),
|
||||||
Panel: cc("#434c5e", ""),
|
Panel: cc("#26233a", ""),
|
||||||
Border: cc("#4c566a", "8"),
|
Border: cc("#524f67", "8"),
|
||||||
Fg: cc("#d8dee9", "15"),
|
Fg: cc("#e0def4", "15"),
|
||||||
Muted: cc("#d8dee9", "7"),
|
Muted: cc("#908caa", "7"),
|
||||||
Subtle: cc("#4c566a", "7"),
|
Subtle: cc("#6e6a86", "7"),
|
||||||
Success: cc("#a3be8c", "10"),
|
Success: cc("#9ccfd8", "10"),
|
||||||
Warning: cc("#ebcb8b", "11"),
|
Warning: cc("#f6c177", "11"),
|
||||||
Stale: cc("#d08770", "3"),
|
Stale: cc("#ebbcba", "3"),
|
||||||
Danger: cc("#bf616a", "9"),
|
Danger: cc("#eb6f92", "9"),
|
||||||
Info: cc("#81a1c1", "12"),
|
Info: cc("#3e8fb0", "12"),
|
||||||
Accent: cc("#88c0d0", "14"),
|
Accent: cc("#c4a7e7", "14"),
|
||||||
Purple: cc("#b48ead", "13"),
|
Purple: cc("#ebbcba", "13"),
|
||||||
ZebraBg: cc("#323845", ""),
|
ZebraBg: cc("#1c1a28", ""),
|
||||||
SelectedFg: cc("#eceff4", "15"),
|
SelectedFg: cc("#e0def4", "15"),
|
||||||
SelectedBg: cc("#434c5e", "4"),
|
SelectedBg: cc("#403d52", "4"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var themeKanagawa = Theme{
|
||||||
|
Name: "Kanagawa",
|
||||||
|
Bg: cc("#1F1F28", ""),
|
||||||
|
Surface: cc("#2A2A37", ""),
|
||||||
|
Panel: cc("#363646", ""),
|
||||||
|
Border: cc("#54546D", "8"),
|
||||||
|
Fg: cc("#DCD7BA", "15"),
|
||||||
|
Muted: cc("#C8C093", "7"),
|
||||||
|
Subtle: cc("#727169", "7"),
|
||||||
|
Success: cc("#98BB6C", "10"),
|
||||||
|
Warning: cc("#E6C384", "11"),
|
||||||
|
Stale: cc("#FFA066", "3"),
|
||||||
|
Danger: cc("#E46876", "9"),
|
||||||
|
Info: cc("#7E9CD8", "12"),
|
||||||
|
Accent: cc("#7FB4CA", "14"),
|
||||||
|
Purple: cc("#D27E99", "13"),
|
||||||
|
ZebraBg: cc("#22222c", ""),
|
||||||
|
SelectedFg: cc("#DCD7BA", "15"),
|
||||||
|
SelectedBg: cc("#2D4F67", "4"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var themeEverforest = Theme{
|
||||||
|
Name: "Everforest",
|
||||||
|
Bg: cc("#2F383E", ""),
|
||||||
|
Surface: cc("#343F44", ""),
|
||||||
|
Panel: cc("#3D484D", ""),
|
||||||
|
Border: cc("#56635F", "8"),
|
||||||
|
Fg: cc("#D3C6AA", "15"),
|
||||||
|
Muted: cc("#9DA9A0", "7"),
|
||||||
|
Subtle: cc("#7A8478", "7"),
|
||||||
|
Success: cc("#A7C080", "10"),
|
||||||
|
Warning: cc("#DBBC7F", "11"),
|
||||||
|
Stale: cc("#E69875", "3"),
|
||||||
|
Danger: cc("#E67E80", "9"),
|
||||||
|
Info: cc("#7FBBB3", "12"),
|
||||||
|
Accent: cc("#83C092", "14"),
|
||||||
|
Purple: cc("#D699B6", "13"),
|
||||||
|
ZebraBg: cc("#30393F", ""),
|
||||||
|
SelectedFg: cc("#D3C6AA", "15"),
|
||||||
|
SelectedBg: cc("#475258", "4"),
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Theme) HuhTheme() *huh.Theme {
|
func (t Theme) HuhTheme() *huh.Theme {
|
||||||
|
|||||||
+14
-1
@@ -207,6 +207,7 @@ 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
|
||||||
|
|
||||||
|
maintSet map[int]bool
|
||||||
bottomPanel bottomPanel
|
bottomPanel bottomPanel
|
||||||
detailOpen bool
|
detailOpen bool
|
||||||
maintCursor int
|
maintCursor int
|
||||||
@@ -217,6 +218,8 @@ type Model struct {
|
|||||||
filterMode bool
|
filterMode bool
|
||||||
filterText string
|
filterText string
|
||||||
|
|
||||||
|
pendingG bool
|
||||||
|
|
||||||
// demoMode renders a stable status dot instead of the animated pulse so
|
// demoMode renders a stable status dot instead of the animated pulse so
|
||||||
// screenshots/recordings don't capture the spinner mid-frame. Set via UPTOP_DEMO=1.
|
// screenshots/recordings don't capture the spinner mid-frame. Set via UPTOP_DEMO=1.
|
||||||
demoMode bool
|
demoMode bool
|
||||||
@@ -242,6 +245,16 @@ func InitialModel(ctx context.Context, isAdmin bool, s store.Store, eng *monitor
|
|||||||
|
|
||||||
detailPref, _ := s.GetPreference(ctx, "detail_open")
|
detailPref, _ := s.GetPreference(ctx, "detail_open")
|
||||||
|
|
||||||
|
bp := bottomLogs
|
||||||
|
if bpPref, _ := s.GetPreference(ctx, "bottom_panel"); bpPref != "" {
|
||||||
|
switch bpPref {
|
||||||
|
case "none":
|
||||||
|
bp = bottomNone
|
||||||
|
case "maint":
|
||||||
|
bp = bottomMaint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Model{
|
return Model{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
state: stateDashboard,
|
state: stateDashboard,
|
||||||
@@ -256,7 +269,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),
|
||||||
bottomPanel: bottomLogs,
|
bottomPanel: bp,
|
||||||
detailOpen: detailPref == "true",
|
detailOpen: detailPref == "true",
|
||||||
demoMode: os.Getenv("UPTOP_DEMO") == "1",
|
demoMode: os.Getenv("UPTOP_DEMO") == "1",
|
||||||
version: version,
|
version: version,
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ func (m *Model) handleTabData(msg tabDataMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
m.nodes = msg.nodes
|
m.nodes = msg.nodes
|
||||||
m.maintenanceWindows = msg.maint
|
m.maintenanceWindows = msg.maint
|
||||||
|
m.buildMaintSet()
|
||||||
m.clampCursor()
|
m.clampCursor()
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -251,6 +252,17 @@ func (m *Model) testAlertCmd(id int, name string) tea.Cmd {
|
|||||||
func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
if msg.String() == "g" {
|
||||||
|
if m.pendingG {
|
||||||
|
m.pendingG = false
|
||||||
|
m.logViewport.GotoTop()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.pendingG = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.pendingG = false
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc", "q":
|
case "esc", "q":
|
||||||
m.state = stateDashboard
|
m.state = stateDashboard
|
||||||
@@ -260,6 +272,12 @@ func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case "f":
|
case "f":
|
||||||
m.logFilterImportant = !m.logFilterImportant
|
m.logFilterImportant = !m.logFilterImportant
|
||||||
m.refreshLogContent()
|
m.refreshLogContent()
|
||||||
|
case "G":
|
||||||
|
m.logViewport.GotoBottom()
|
||||||
|
case "ctrl+u":
|
||||||
|
m.logViewport.ScrollUp(m.logViewport.Height / 2)
|
||||||
|
case "ctrl+d":
|
||||||
|
m.logViewport.ScrollDown(m.logViewport.Height / 2)
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
m.logViewport.ScrollUp(1)
|
m.logViewport.ScrollUp(1)
|
||||||
case "down", "j":
|
case "down", "j":
|
||||||
@@ -285,6 +303,17 @@ func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
func (m *Model) handleDetailFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *Model) handleDetailFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
if msg.String() == "g" {
|
||||||
|
if m.pendingG {
|
||||||
|
m.pendingG = false
|
||||||
|
m.detailScrollOffset = 0
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.pendingG = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.pendingG = false
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc", "q":
|
case "esc", "q":
|
||||||
m.state = stateDashboard
|
m.state = stateDashboard
|
||||||
@@ -322,6 +351,15 @@ func (m *Model) handleDetailFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, m.loadSLACmd(m.slaSiteID, idx)
|
return m, m.loadSLACmd(m.slaSiteID, idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "G":
|
||||||
|
m.detailScrollOffset = 9999
|
||||||
|
case "ctrl+u":
|
||||||
|
m.detailScrollOffset -= 5
|
||||||
|
if m.detailScrollOffset < 0 {
|
||||||
|
m.detailScrollOffset = 0
|
||||||
|
}
|
||||||
|
case "ctrl+d":
|
||||||
|
m.detailScrollOffset += 5
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
m.detailScrollOffset--
|
m.detailScrollOffset--
|
||||||
if m.detailScrollOffset < 0 {
|
if m.detailScrollOffset < 0 {
|
||||||
@@ -543,7 +581,52 @@ func (m *Model) handleAlertDetailKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleSettingsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *Model) handleSettingsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
if msg.String() == "g" {
|
||||||
|
if m.pendingG {
|
||||||
|
m.pendingG = false
|
||||||
|
m.settingsCursor = 0
|
||||||
|
m.settingsOffset = 0
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.pendingG = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.pendingG = false
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
|
case "G":
|
||||||
|
max := m.settingsListLen() - 1
|
||||||
|
if max >= 0 {
|
||||||
|
m.settingsCursor = max
|
||||||
|
if m.settingsCursor >= m.settingsOffset+m.maxTableRows {
|
||||||
|
m.settingsOffset = m.settingsCursor - m.maxTableRows + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "ctrl+u":
|
||||||
|
half := m.maxTableRows / 2
|
||||||
|
if half < 1 {
|
||||||
|
half = 1
|
||||||
|
}
|
||||||
|
m.settingsCursor -= half
|
||||||
|
if m.settingsCursor < 0 {
|
||||||
|
m.settingsCursor = 0
|
||||||
|
}
|
||||||
|
if m.settingsCursor < m.settingsOffset {
|
||||||
|
m.settingsOffset = m.settingsCursor
|
||||||
|
}
|
||||||
|
case "ctrl+d":
|
||||||
|
half := m.maxTableRows / 2
|
||||||
|
if half < 1 {
|
||||||
|
half = 1
|
||||||
|
}
|
||||||
|
max := m.settingsListLen() - 1
|
||||||
|
m.settingsCursor += half
|
||||||
|
if m.settingsCursor > max {
|
||||||
|
m.settingsCursor = max
|
||||||
|
}
|
||||||
|
if m.settingsCursor >= m.settingsOffset+m.maxTableRows {
|
||||||
|
m.settingsOffset = m.settingsCursor - m.maxTableRows + 1
|
||||||
|
}
|
||||||
case "esc", "S":
|
case "esc", "S":
|
||||||
m.state = stateDashboard
|
m.state = stateDashboard
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
@@ -634,7 +717,27 @@ func (m *Model) handleSettingsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
if msg.String() == "g" {
|
||||||
|
if m.pendingG {
|
||||||
|
m.pendingG = false
|
||||||
|
m.jumpToTop()
|
||||||
|
return m, m.detailCmdIfNeeded()
|
||||||
|
}
|
||||||
|
m.pendingG = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.pendingG = false
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
|
case "G":
|
||||||
|
m.jumpToBottom()
|
||||||
|
return m, m.detailCmdIfNeeded()
|
||||||
|
case "ctrl+u":
|
||||||
|
m.halfPageUp()
|
||||||
|
return m, m.detailCmdIfNeeded()
|
||||||
|
case "ctrl+d":
|
||||||
|
m.halfPageDown()
|
||||||
|
return m, m.detailCmdIfNeeded()
|
||||||
case "q":
|
case "q":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "/":
|
case "/":
|
||||||
@@ -661,6 +764,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.focusedPanel = panelMaint
|
m.focusedPanel = panelMaint
|
||||||
}
|
}
|
||||||
m.recalcLayout()
|
m.recalcLayout()
|
||||||
|
return m, m.saveBottomPanelPref()
|
||||||
case "l":
|
case "l":
|
||||||
if m.bottomPanel == bottomLogs {
|
if m.bottomPanel == bottomLogs {
|
||||||
m.bottomPanel = bottomNone
|
m.bottomPanel = bottomNone
|
||||||
@@ -670,6 +774,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.focusedPanel = panelLogs
|
m.focusedPanel = panelLogs
|
||||||
}
|
}
|
||||||
m.recalcLayout()
|
m.recalcLayout()
|
||||||
|
return m, m.saveBottomPanelPref()
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
if m.focusedPanel == panelDetail && m.detailOpen {
|
if m.focusedPanel == panelDetail && m.detailOpen {
|
||||||
m.detailScrollOffset--
|
m.detailScrollOffset--
|
||||||
|
|||||||
@@ -389,3 +389,86 @@ func TestDetailRefreshCmd_OnlyWhileDetailOpen(t *testing.T) {
|
|||||||
t.Error("refresh Cmd issued for an out-of-range cursor")
|
t.Error("refresh Cmd issued for an out-of-range cursor")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildMaintSet_GlobalWindow(t *testing.T) {
|
||||||
|
m := newTestModel(&tuiMockStore{})
|
||||||
|
m.sites = []models.Site{
|
||||||
|
{SiteConfig: models.SiteConfig{ID: 1, Name: "a"}},
|
||||||
|
{SiteConfig: models.SiteConfig{ID: 2, Name: "b"}},
|
||||||
|
}
|
||||||
|
m.maintenanceWindows = []models.MaintenanceWindow{
|
||||||
|
{ID: 1, MonitorID: 0, Type: "maintenance", StartTime: time.Now().Add(-time.Hour)},
|
||||||
|
}
|
||||||
|
m.buildMaintSet()
|
||||||
|
if !m.maintSet[1] || !m.maintSet[2] {
|
||||||
|
t.Error("global maint window should mark all monitors")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildMaintSet_TargetedWindow(t *testing.T) {
|
||||||
|
m := newTestModel(&tuiMockStore{})
|
||||||
|
m.sites = []models.Site{
|
||||||
|
{SiteConfig: models.SiteConfig{ID: 1, Name: "a"}},
|
||||||
|
{SiteConfig: models.SiteConfig{ID: 2, Name: "b"}},
|
||||||
|
}
|
||||||
|
m.maintenanceWindows = []models.MaintenanceWindow{
|
||||||
|
{ID: 1, MonitorID: 1, Type: "maintenance", StartTime: time.Now().Add(-time.Hour)},
|
||||||
|
}
|
||||||
|
m.buildMaintSet()
|
||||||
|
if !m.maintSet[1] {
|
||||||
|
t.Error("targeted window should mark monitor 1")
|
||||||
|
}
|
||||||
|
if m.maintSet[2] {
|
||||||
|
t.Error("targeted window should NOT mark monitor 2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildMaintSet_GroupPropagates(t *testing.T) {
|
||||||
|
m := newTestModel(&tuiMockStore{})
|
||||||
|
m.sites = []models.Site{
|
||||||
|
{SiteConfig: models.SiteConfig{ID: 10, Name: "group", Type: "group"}},
|
||||||
|
{SiteConfig: models.SiteConfig{ID: 11, Name: "child1", ParentID: 10}},
|
||||||
|
{SiteConfig: models.SiteConfig{ID: 12, Name: "child2", ParentID: 10}},
|
||||||
|
{SiteConfig: models.SiteConfig{ID: 20, Name: "other"}},
|
||||||
|
}
|
||||||
|
m.maintenanceWindows = []models.MaintenanceWindow{
|
||||||
|
{ID: 1, MonitorID: 10, Type: "maintenance", StartTime: time.Now().Add(-time.Hour)},
|
||||||
|
}
|
||||||
|
m.buildMaintSet()
|
||||||
|
if !m.maintSet[10] || !m.maintSet[11] || !m.maintSet[12] {
|
||||||
|
t.Error("group maint window should mark group + children")
|
||||||
|
}
|
||||||
|
if m.maintSet[20] {
|
||||||
|
t.Error("unrelated monitor should NOT be marked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildMaintSet_ExpiredIgnored(t *testing.T) {
|
||||||
|
m := newTestModel(&tuiMockStore{})
|
||||||
|
m.sites = []models.Site{
|
||||||
|
{SiteConfig: models.SiteConfig{ID: 1, Name: "a"}},
|
||||||
|
}
|
||||||
|
m.maintenanceWindows = []models.MaintenanceWindow{
|
||||||
|
{ID: 1, MonitorID: 1, Type: "maintenance",
|
||||||
|
StartTime: time.Now().Add(-2 * time.Hour),
|
||||||
|
EndTime: time.Now().Add(-1 * time.Hour)},
|
||||||
|
}
|
||||||
|
m.buildMaintSet()
|
||||||
|
if m.maintSet[1] {
|
||||||
|
t.Error("expired maint window should not mark monitor")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildMaintSet_IncidentIgnored(t *testing.T) {
|
||||||
|
m := newTestModel(&tuiMockStore{})
|
||||||
|
m.sites = []models.Site{
|
||||||
|
{SiteConfig: models.SiteConfig{ID: 1, Name: "a"}},
|
||||||
|
}
|
||||||
|
m.maintenanceWindows = []models.MaintenanceWindow{
|
||||||
|
{ID: 1, MonitorID: 0, Type: "incident", StartTime: time.Now().Add(-time.Hour)},
|
||||||
|
}
|
||||||
|
m.buildMaintSet()
|
||||||
|
if m.maintSet[1] {
|
||||||
|
t.Error("incident windows should not mark monitors as in maintenance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ func (m Model) viewMonitorsLayout() string {
|
|||||||
|
|
||||||
monTargetH := m.maxTableRows + 5
|
monTargetH := m.maxTableRows + 5
|
||||||
monitors := m.viewSitesTab()
|
monitors := m.viewSitesTab()
|
||||||
monPanel := m.zones.Mark("panel-monitors", m.titledPanelH("Monitors", monitors, "", monW, monTargetH, 0, m.focusedPanel == panelMonitors))
|
monPanel := m.zones.Mark("panel-monitors", m.titledPanelH("Monitors", monitors, "", monW, monTargetH, 0, scrollbar{pos: m.tableOffset, total: len(m.sites), visible: m.maxTableRows}, m.focusedPanel == panelMonitors))
|
||||||
|
|
||||||
var topParts []string
|
var topParts []string
|
||||||
topParts = append(topParts, monPanel)
|
topParts = append(topParts, monPanel)
|
||||||
@@ -181,7 +181,7 @@ func (m Model) viewMonitorsLayout() string {
|
|||||||
monHeight := lipgloss.Height(monPanel)
|
monHeight := lipgloss.Height(monPanel)
|
||||||
detail := m.viewDetailInline(detailW-2, monHeight)
|
detail := m.viewDetailInline(detailW-2, monHeight)
|
||||||
footer := m.detailFooter(detailW - 2)
|
footer := m.detailFooter(detailW - 2)
|
||||||
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, footer, detailW, monHeight, m.detailScrollOffset, m.focusedPanel == panelDetail))
|
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, footer, detailW, monHeight, m.detailScrollOffset, scrollbar{}, m.focusedPanel == panelDetail))
|
||||||
topParts = append(topParts, detailPanel)
|
topParts = append(topParts, detailPanel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,12 +189,16 @@ func (m Model) viewMonitorsLayout() string {
|
|||||||
|
|
||||||
switch m.bottomPanel {
|
switch m.bottomPanel {
|
||||||
case bottomLogs:
|
case bottomLogs:
|
||||||
logContent := m.viewLogsStrip(availW-2, logsStripHeight-2)
|
maxLines := logsStripHeight - 2
|
||||||
logPanel := m.zones.Mark("panel-logs", m.titledPanel("Logs", logContent, availW, m.focusedPanel == panelLogs))
|
logContent := m.viewLogsStrip(availW-2, maxLines)
|
||||||
|
totalLogs := m.filteredLogCount()
|
||||||
|
logPanel := m.zones.Mark("panel-logs", m.titledPanelH("Logs", logContent, "", availW, logsStripHeight, 0, scrollbar{pos: m.logScrollOffset, total: totalLogs, visible: maxLines}, m.focusedPanel == panelLogs))
|
||||||
return top + "\n" + logPanel
|
return top + "\n" + logPanel
|
||||||
case bottomMaint:
|
case bottomMaint:
|
||||||
maintContent := m.viewMaintStrip(availW-2, logsStripHeight-2)
|
maxLines := logsStripHeight - 2
|
||||||
maintPanel := m.zones.Mark("panel-maint", m.titledPanel("Maint", maintContent, availW, m.focusedPanel == panelMaint))
|
maintContent := m.viewMaintStrip(availW-2, maxLines)
|
||||||
|
totalMaint := len(m.activeMaintWindows())
|
||||||
|
maintPanel := m.zones.Mark("panel-maint", m.titledPanelH("Maint", maintContent, "", availW, logsStripHeight, 0, scrollbar{pos: 0, total: totalMaint, visible: maxLines}, m.focusedPanel == panelMaint))
|
||||||
return top + "\n" + maintPanel
|
return top + "\n" + maintPanel
|
||||||
}
|
}
|
||||||
return top
|
return top
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (m Model) viewDetailFullscreen() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return lipgloss.NewStyle().Padding(1, 2).Render(
|
return lipgloss.NewStyle().Padding(1, 2).Render(
|
||||||
m.titledPanelH(title, content, footer, availW, panelH, m.detailScrollOffset, true))
|
m.titledPanelH(title, content, footer, availW, panelH, m.detailScrollOffset, scrollbar{}, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, width int, fullscreen bool) string {
|
func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, width int, fullscreen bool) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user