Compare commits
3 Commits
1cfa0571c8
...
ce3abda5b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
ce3abda5b9
|
|||
|
ce3cfad7f8
|
|||
|
d6e6011b35
|
+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
|
||||||
|
|||||||
+113
-2
@@ -77,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 {
|
||||||
@@ -98,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)
|
||||||
@@ -169,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
|
||||||
|
|||||||
@@ -218,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
|
||||||
|
|||||||
@@ -252,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
|
||||||
@@ -261,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":
|
||||||
@@ -286,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
|
||||||
@@ -323,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 {
|
||||||
@@ -544,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":
|
||||||
@@ -635,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 "/":
|
||||||
|
|||||||
Reference in New Issue
Block a user