feat(tui): consolidate tabs 6→3, add log sidebar #145
@@ -106,7 +106,7 @@ func (m *Model) refreshLive() {
|
|||||||
m.sites = ordered
|
m.sites = ordered
|
||||||
m.refreshLogContent()
|
m.refreshLogContent()
|
||||||
|
|
||||||
if m.currentTab == 0 && m.selectedID != 0 {
|
if m.currentTab == tabMonitors && m.selectedID != 0 {
|
||||||
for i, s := range m.sites {
|
for i, s := range m.sites {
|
||||||
if s.ID == m.selectedID {
|
if s.ID == m.selectedID {
|
||||||
m.cursor = i
|
m.cursor = i
|
||||||
@@ -118,7 +118,7 @@ func (m *Model) refreshLive() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) syncSelectedID() {
|
func (m *Model) syncSelectedID() {
|
||||||
if m.currentTab == 0 && m.cursor < len(m.sites) {
|
if m.currentTab == tabMonitors && m.cursor < len(m.sites) {
|
||||||
m.selectedID = m.sites[m.cursor].ID
|
m.selectedID = m.sites[m.cursor].ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,23 +107,3 @@ func (m *Model) refreshLogContent() {
|
|||||||
m.logShown = shown
|
m.logShown = shown
|
||||||
m.logViewport.SetContent(strings.Join(rendered, "\n"))
|
m.logViewport.SetContent(strings.Join(rendered, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) viewLogsTab() string {
|
|
||||||
if m.logTotal == 0 {
|
|
||||||
return m.emptyState("No log entries yet.", "Logs appear as monitors run checks")
|
|
||||||
}
|
|
||||||
|
|
||||||
filterLabel := "All"
|
|
||||||
if m.logFilterImportant {
|
|
||||||
filterLabel = "Important"
|
|
||||||
}
|
|
||||||
|
|
||||||
header := m.st.subtleStyle.Render(fmt.Sprintf(
|
|
||||||
" %d entries Filter: %s", m.logShown, filterLabel))
|
|
||||||
|
|
||||||
if m.logFilterImportant && m.logShown < m.logTotal {
|
|
||||||
header += m.st.subtleStyle.Render(fmt.Sprintf(" (%d hidden)", m.logTotal-m.logShown))
|
|
||||||
}
|
|
||||||
|
|
||||||
return "\n" + header + "\n\n" + m.logViewport.View()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m Model) renderCompactLogLine(line string, maxW int) string {
|
||||||
|
sev := classifyLog(line)
|
||||||
|
|
||||||
|
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 := ""
|
||||||
|
msg := line
|
||||||
|
if len(line) > 10 && line[0] == '[' {
|
||||||
|
if idx := strings.Index(line, "]"); idx > 0 && idx < 12 {
|
||||||
|
ts = line[1:idx]
|
||||||
|
msg = strings.TrimSpace(line[idx+1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = strings.TrimPrefix(msg, "Monitor ")
|
||||||
|
msg = strings.TrimPrefix(msg, "Push ")
|
||||||
|
|
||||||
|
// prefix: " HH:MM ● " = 9 visible chars, or " ● " = 3 without timestamp
|
||||||
|
prefixW := 3
|
||||||
|
if ts != "" {
|
||||||
|
prefixW = len(ts) + 4
|
||||||
|
}
|
||||||
|
msgW := maxW - prefixW
|
||||||
|
if msgW < 5 {
|
||||||
|
msgW = 5
|
||||||
|
}
|
||||||
|
msg = limitStr(msg, msgW)
|
||||||
|
|
||||||
|
if ts != "" {
|
||||||
|
return " " + m.st.subtleStyle.Render(ts) + " " + tag + " " + msg
|
||||||
|
}
|
||||||
|
return " " + tag + " " + msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) viewLogsSidebar(width int) string {
|
||||||
|
logs := m.engine.GetLogs()
|
||||||
|
if len(logs) == 0 {
|
||||||
|
return m.st.subtleStyle.Render(" No logs yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebarStyle := lipgloss.NewStyle().Width(width).MaxWidth(width)
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
for _, line := range logs {
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if m.logFilterImportant && !isImportantLog(classifyLog(line)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lines = append(lines, m.renderCompactLogLine(line, width))
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\n" + sidebarStyle.Render(strings.Join(lines, "\n"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m Model) viewSettingsTab() string {
|
||||||
|
maxSections := 2
|
||||||
|
if m.isAdmin {
|
||||||
|
maxSections = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
sections := []string{"Alerts", "Nodes"}
|
||||||
|
if m.isAdmin {
|
||||||
|
sections = append(sections, "Users")
|
||||||
|
}
|
||||||
|
_ = maxSections
|
||||||
|
|
||||||
|
var tabs []string
|
||||||
|
for i, name := range sections {
|
||||||
|
if i == m.settingsSection {
|
||||||
|
tabs = append(tabs, m.st.activeTab.Render(name))
|
||||||
|
} else {
|
||||||
|
tabs = append(tabs, m.st.inactiveTab.Render(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header := lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
|
||||||
|
|
||||||
|
var content string
|
||||||
|
switch m.settingsSection {
|
||||||
|
case sectionAlerts:
|
||||||
|
content = m.viewAlertsTab()
|
||||||
|
case sectionNodes:
|
||||||
|
content = m.viewNodesTab()
|
||||||
|
case sectionUsers:
|
||||||
|
if m.isAdmin {
|
||||||
|
content = m.viewUsersTab()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return header + "\n" + content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) switchSettingsSection(idx int) {
|
||||||
|
max := 1
|
||||||
|
if m.isAdmin {
|
||||||
|
max = 2
|
||||||
|
}
|
||||||
|
if idx > max {
|
||||||
|
idx = 0
|
||||||
|
}
|
||||||
|
if idx < 0 {
|
||||||
|
idx = max
|
||||||
|
}
|
||||||
|
m.settingsSection = idx
|
||||||
|
m.cursor = 0
|
||||||
|
m.tableOffset = 0
|
||||||
|
}
|
||||||
@@ -82,8 +82,12 @@ func (m Model) computeLayout() tableLayout {
|
|||||||
var widths []int
|
var widths []int
|
||||||
var fixed int
|
var fixed int
|
||||||
|
|
||||||
|
cw := m.contentWidth
|
||||||
|
if cw == 0 {
|
||||||
|
cw = m.termWidth
|
||||||
|
}
|
||||||
for _, c := range siteColumns {
|
for _, c := range siteColumns {
|
||||||
if c.minTerm > 0 && m.termWidth < c.minTerm {
|
if c.minTerm > 0 && cw < c.minTerm {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
active = append(active, c.key)
|
active = append(active, c.key)
|
||||||
@@ -104,7 +108,7 @@ func (m Model) computeLayout() tableLayout {
|
|||||||
|
|
||||||
numCols := len(headers)
|
numCols := len(headers)
|
||||||
borderOverhead := 2 + (numCols - 1)
|
borderOverhead := 2 + (numCols - 1)
|
||||||
avail := m.termWidth - chromePadH - 2 - borderOverhead - fixed
|
avail := cw - chromePadH - 2 - borderOverhead - fixed
|
||||||
if avail < 20 {
|
if avail < 20 {
|
||||||
avail = 20
|
avail = 20
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func (m *Model) submitUserForm() tea.Cmd {
|
|||||||
st := m.store
|
st := m.store
|
||||||
id := m.editID
|
id := m.editID
|
||||||
username, key, role := d.Username, d.PublicKey, d.Role
|
username, key, role := d.Username, d.PublicKey, d.Role
|
||||||
m.state = stateUsers
|
m.state = stateDashboard
|
||||||
if id > 0 {
|
if id > 0 {
|
||||||
return writeCmd("Update user", func() error {
|
return writeCmd("Update user", func() error {
|
||||||
return st.UpdateUser(context.Background(), id, username, key, role)
|
return st.UpdateUser(context.Background(), id, username, key, role)
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (m Model) isWide() bool {
|
func (m Model) isWide() bool {
|
||||||
return m.termWidth >= wideBreakpoint
|
w := m.contentWidth
|
||||||
|
if w == 0 {
|
||||||
|
w = m.termWidth
|
||||||
|
}
|
||||||
|
return w >= wideBreakpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderTable(headers []string, items int, buildRows func(start, end int) [][]string, colWidths []int, styleOverride StyleOverride) string {
|
func (m Model) renderTable(headers []string, items int, buildRows func(start, end int) [][]string, colWidths []int, styleOverride StyleOverride) string {
|
||||||
@@ -35,7 +39,11 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
}
|
}
|
||||||
borderOverhead := 2 + len(colWidths) - 1
|
borderOverhead := 2 + len(colWidths) - 1
|
||||||
tableWidth := colTotal + borderOverhead
|
tableWidth := colTotal + borderOverhead
|
||||||
maxWidth := m.termWidth - chromePadH - 2
|
cw := m.contentWidth
|
||||||
|
if cw == 0 {
|
||||||
|
cw = m.termWidth
|
||||||
|
}
|
||||||
|
maxWidth := cw - chromePadH - 2
|
||||||
if tableWidth > maxWidth {
|
if tableWidth > maxWidth {
|
||||||
tableWidth = maxWidth
|
tableWidth = maxWidth
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,18 @@ const (
|
|||||||
detailSparkWidth = 40
|
detailSparkWidth = 40
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tabMonitors = 0
|
||||||
|
tabMaint = 1
|
||||||
|
tabSettings = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sectionAlerts = 0
|
||||||
|
sectionNodes = 1
|
||||||
|
sectionUsers = 2
|
||||||
|
)
|
||||||
|
|
||||||
type sessionState int
|
type sessionState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -104,12 +116,14 @@ const (
|
|||||||
type Model struct {
|
type Model struct {
|
||||||
state sessionState
|
state sessionState
|
||||||
currentTab int
|
currentTab int
|
||||||
|
settingsSection int
|
||||||
cursor int
|
cursor int
|
||||||
selectedID int
|
selectedID int
|
||||||
tableOffset int
|
tableOffset int
|
||||||
maxTableRows int
|
maxTableRows int
|
||||||
termWidth int
|
termWidth int
|
||||||
termHeight int
|
termHeight int
|
||||||
|
contentWidth int
|
||||||
editID int
|
editID int
|
||||||
editToken string
|
editToken string
|
||||||
|
|
||||||
|
|||||||
+81
-104
@@ -78,31 +78,28 @@ func (m *Model) handleConfirmDelete(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
id := m.deleteID
|
id := m.deleteID
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
switch m.deleteTab {
|
switch m.deleteTab {
|
||||||
case 0:
|
case tabMonitors:
|
||||||
cmd = writeCmd("Delete site", func() error { return st.DeleteSite(context.Background(), id) })
|
cmd = writeCmd("Delete site", func() error { return st.DeleteSite(context.Background(), id) })
|
||||||
m.engine.RemoveSite(id)
|
m.engine.RemoveSite(id)
|
||||||
m.adjustCursor(len(m.sites) - 1)
|
m.adjustCursor(len(m.sites) - 1)
|
||||||
case 1:
|
case tabMaint:
|
||||||
cmd = writeCmd("Delete alert", func() error { return st.DeleteAlert(context.Background(), id) })
|
|
||||||
m.adjustCursor(len(m.alerts) - 1)
|
|
||||||
case 4:
|
|
||||||
cmd = writeCmd("Delete maintenance window", func() error { return st.DeleteMaintenanceWindow(context.Background(), id) })
|
cmd = writeCmd("Delete maintenance window", func() error { return st.DeleteMaintenanceWindow(context.Background(), id) })
|
||||||
m.adjustCursor(len(m.maintenanceWindows) - 1)
|
m.adjustCursor(len(m.maintenanceWindows) - 1)
|
||||||
case 5:
|
case tabSettings:
|
||||||
|
switch m.settingsSection {
|
||||||
|
case sectionAlerts:
|
||||||
|
cmd = writeCmd("Delete alert", func() error { return st.DeleteAlert(context.Background(), id) })
|
||||||
|
m.adjustCursor(len(m.alerts) - 1)
|
||||||
|
case sectionUsers:
|
||||||
cmd = writeCmd("Delete user", func() error { return st.DeleteUser(context.Background(), id) })
|
cmd = writeCmd("Delete user", func() error { return st.DeleteUser(context.Background(), id) })
|
||||||
m.adjustCursor(len(m.users) - 1)
|
m.adjustCursor(len(m.users) - 1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
m.refreshLive()
|
m.refreshLive()
|
||||||
m.state = stateDashboard
|
m.state = stateDashboard
|
||||||
if m.deleteTab == 5 {
|
|
||||||
m.state = stateUsers
|
|
||||||
}
|
|
||||||
return m, cmd
|
return m, cmd
|
||||||
case "n", "N", "esc":
|
case "n", "N", "esc":
|
||||||
m.state = stateDashboard
|
m.state = stateDashboard
|
||||||
if m.deleteTab == 5 {
|
|
||||||
m.state = stateUsers
|
|
||||||
}
|
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
@@ -117,9 +114,6 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if keyMsg.String() == "esc" {
|
if keyMsg.String() == "esc" {
|
||||||
m.huhForm = nil
|
m.huhForm = nil
|
||||||
m.state = stateDashboard
|
m.state = stateDashboard
|
||||||
if m.currentTab == 5 {
|
|
||||||
m.state = stateUsers
|
|
||||||
}
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,7 +259,7 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
if m.state != stateDashboard && m.state != stateLogs && m.state != stateUsers {
|
if m.state != stateDashboard {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
|
if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
|
||||||
@@ -275,15 +269,6 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.state == stateLogs {
|
|
||||||
if msg.Button == tea.MouseButtonWheelUp {
|
|
||||||
m.logViewport.ScrollUp(3)
|
|
||||||
} else {
|
|
||||||
m.logViewport.ScrollDown(3)
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
listLen := m.currentListLen()
|
listLen := m.currentListLen()
|
||||||
if msg.Button == tea.MouseButtonWheelUp {
|
if msg.Button == tea.MouseButtonWheelUp {
|
||||||
if m.cursor > 0 {
|
if m.cursor > 0 {
|
||||||
@@ -325,7 +310,7 @@ func (m *Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m.handleSLAKey(msg)
|
return m.handleSLAKey(msg)
|
||||||
case stateAlertDetail:
|
case stateAlertDetail:
|
||||||
return m.handleAlertDetailKey(msg)
|
return m.handleAlertDetailKey(msg)
|
||||||
case stateDashboard, stateLogs, stateUsers:
|
case stateDashboard:
|
||||||
return m.handleDashboardKey(msg)
|
return m.handleDashboardKey(msg)
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -521,33 +506,27 @@ func (m *Model) handleAlertDetailKey(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) {
|
||||||
var cmd tea.Cmd
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "q":
|
case "q":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "/":
|
case "/":
|
||||||
if m.currentTab == 0 {
|
if m.currentTab == tabMonitors {
|
||||||
m.filterMode = true
|
m.filterMode = true
|
||||||
m.recalcLayout()
|
m.recalcLayout()
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
case "f":
|
|
||||||
if m.state == stateLogs {
|
|
||||||
m.logFilterImportant = !m.logFilterImportant
|
|
||||||
m.refreshLogContent()
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
case "tab":
|
case "tab":
|
||||||
m.switchTab(m.currentTab + 1)
|
m.switchTab(m.currentTab + 1)
|
||||||
case "pgup", "pgdown":
|
case "left", "h":
|
||||||
if m.state == stateLogs {
|
if m.currentTab == tabSettings {
|
||||||
m.logViewport, cmd = m.logViewport.Update(msg)
|
m.switchSettingsSection(m.settingsSection - 1)
|
||||||
return m, cmd
|
}
|
||||||
|
case "right", "l":
|
||||||
|
if m.currentTab == tabSettings {
|
||||||
|
m.switchSettingsSection(m.settingsSection + 1)
|
||||||
}
|
}
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
if m.state == stateLogs {
|
if m.cursor > 0 {
|
||||||
m.logViewport.ScrollUp(1)
|
|
||||||
} else if m.cursor > 0 {
|
|
||||||
m.cursor--
|
m.cursor--
|
||||||
if m.cursor < m.tableOffset {
|
if m.cursor < m.tableOffset {
|
||||||
m.tableOffset = m.cursor
|
m.tableOffset = m.cursor
|
||||||
@@ -555,9 +534,6 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.syncSelectedID()
|
m.syncSelectedID()
|
||||||
}
|
}
|
||||||
case "down", "j":
|
case "down", "j":
|
||||||
if m.state == stateLogs {
|
|
||||||
m.logViewport.ScrollDown(1)
|
|
||||||
} else {
|
|
||||||
max := m.currentListLen() - 1
|
max := m.currentListLen() - 1
|
||||||
if m.cursor < max {
|
if m.cursor < max {
|
||||||
m.cursor++
|
m.cursor++
|
||||||
@@ -566,18 +542,17 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
m.syncSelectedID()
|
m.syncSelectedID()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case "n":
|
case "n":
|
||||||
return m.handleNewItem()
|
return m.handleNewItem()
|
||||||
case "e", "enter":
|
case "e", "enter":
|
||||||
return m.handleEditItem()
|
return m.handleEditItem()
|
||||||
case "t":
|
case "t":
|
||||||
if m.currentTab == 1 && len(m.alerts) > 0 {
|
if m.currentTab == tabSettings && m.settingsSection == sectionAlerts && len(m.alerts) > 0 {
|
||||||
a := m.alerts[m.cursor]
|
a := m.alerts[m.cursor]
|
||||||
return m, m.testAlertCmd(a.ID, a.Name)
|
return m, m.testAlertCmd(a.ID, a.Name)
|
||||||
}
|
}
|
||||||
case " ":
|
case " ":
|
||||||
if m.currentTab == 0 && len(m.sites) > 0 && m.sites[m.cursor].Type == "group" {
|
if m.currentTab == tabMonitors && len(m.sites) > 0 && m.sites[m.cursor].Type == "group" {
|
||||||
gid := m.sites[m.cursor].ID
|
gid := m.sites[m.cursor].ID
|
||||||
m.collapsed[gid] = !m.collapsed[gid]
|
m.collapsed[gid] = !m.collapsed[gid]
|
||||||
payload := collapsedJSON(m.collapsed)
|
payload := collapsedJSON(m.collapsed)
|
||||||
@@ -588,7 +563,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
case "p":
|
case "p":
|
||||||
if m.currentTab == 0 && len(m.sites) > 0 {
|
if m.currentTab == tabMonitors && len(m.sites) > 0 {
|
||||||
id := m.sites[m.cursor].ID
|
id := m.sites[m.cursor].ID
|
||||||
paused := m.engine.ToggleSitePause(id)
|
paused := m.engine.ToggleSitePause(id)
|
||||||
st := m.store
|
st := m.store
|
||||||
@@ -598,14 +573,14 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
case "i":
|
case "i":
|
||||||
if m.currentTab == 0 && len(m.sites) > 0 {
|
if m.currentTab == tabMonitors && len(m.sites) > 0 {
|
||||||
m.state = stateDetail
|
m.state = stateDetail
|
||||||
return m, m.loadDetailCmd(m.sites[m.cursor].ID)
|
return m, m.loadDetailCmd(m.sites[m.cursor].ID)
|
||||||
} else if m.currentTab == 1 && len(m.alerts) > 0 {
|
} else if m.currentTab == tabSettings && m.settingsSection == sectionAlerts && len(m.alerts) > 0 {
|
||||||
m.state = stateAlertDetail
|
m.state = stateAlertDetail
|
||||||
}
|
}
|
||||||
case "x":
|
case "x":
|
||||||
if m.currentTab == 4 && len(m.maintenanceWindows) > 0 {
|
if m.currentTab == tabMaint && len(m.maintenanceWindows) > 0 {
|
||||||
mw := m.maintenanceWindows[m.cursor]
|
mw := m.maintenanceWindows[m.cursor]
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
isActive := !mw.StartTime.After(now) && (mw.EndTime.IsZero() || mw.EndTime.After(now))
|
isActive := !mw.StartTime.After(now) && (mw.EndTime.IsZero() || mw.EndTime.After(now))
|
||||||
@@ -637,88 +612,94 @@ func (m *Model) handleNewItem() (tea.Model, tea.Cmd) {
|
|||||||
m.editID = 0
|
m.editID = 0
|
||||||
m.editToken = ""
|
m.editToken = ""
|
||||||
switch m.currentTab {
|
switch m.currentTab {
|
||||||
case 0:
|
case tabMonitors:
|
||||||
m.state = stateFormSite
|
m.state = stateFormSite
|
||||||
return m, m.initSiteHuhForm()
|
return m, m.initSiteHuhForm()
|
||||||
case 1:
|
case tabMaint:
|
||||||
m.state = stateFormAlert
|
|
||||||
return m, m.initAlertHuhForm()
|
|
||||||
case 4:
|
|
||||||
m.state = stateFormMaint
|
m.state = stateFormMaint
|
||||||
return m, m.initMaintHuhForm()
|
return m, m.initMaintHuhForm()
|
||||||
case 5:
|
case tabSettings:
|
||||||
|
switch m.settingsSection {
|
||||||
|
case sectionAlerts:
|
||||||
|
m.state = stateFormAlert
|
||||||
|
return m, m.initAlertHuhForm()
|
||||||
|
case sectionUsers:
|
||||||
if m.isAdmin {
|
if m.isAdmin {
|
||||||
m.state = stateFormUser
|
m.state = stateFormUser
|
||||||
return m, m.initUserHuhForm()
|
return m, m.initUserHuhForm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleEditItem() (tea.Model, tea.Cmd) {
|
func (m *Model) handleEditItem() (tea.Model, tea.Cmd) {
|
||||||
switch m.currentTab {
|
switch m.currentTab {
|
||||||
case 0:
|
case tabMonitors:
|
||||||
if len(m.sites) > 0 {
|
if len(m.sites) > 0 {
|
||||||
m.editID = m.sites[m.cursor].ID
|
m.editID = m.sites[m.cursor].ID
|
||||||
m.editToken = m.sites[m.cursor].Token
|
m.editToken = m.sites[m.cursor].Token
|
||||||
m.state = stateFormSite
|
m.state = stateFormSite
|
||||||
return m, m.initSiteHuhForm()
|
return m, m.initSiteHuhForm()
|
||||||
}
|
}
|
||||||
case 1:
|
case tabSettings:
|
||||||
|
switch m.settingsSection {
|
||||||
|
case sectionAlerts:
|
||||||
if len(m.alerts) > 0 {
|
if len(m.alerts) > 0 {
|
||||||
m.editID = m.alerts[m.cursor].ID
|
m.editID = m.alerts[m.cursor].ID
|
||||||
m.state = stateFormAlert
|
m.state = stateFormAlert
|
||||||
return m, m.initAlertHuhForm()
|
return m, m.initAlertHuhForm()
|
||||||
}
|
}
|
||||||
case 5:
|
case sectionUsers:
|
||||||
if m.isAdmin && len(m.users) > 0 {
|
if m.isAdmin && len(m.users) > 0 {
|
||||||
m.editID = m.users[m.cursor].ID
|
m.editID = m.users[m.cursor].ID
|
||||||
m.state = stateFormUser
|
m.state = stateFormUser
|
||||||
return m, m.initUserHuhForm()
|
return m, m.initUserHuhForm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleDeleteItem() (tea.Model, tea.Cmd) {
|
func (m *Model) handleDeleteItem() (tea.Model, tea.Cmd) {
|
||||||
switch m.currentTab {
|
switch m.currentTab {
|
||||||
case 0:
|
case tabMonitors:
|
||||||
if len(m.sites) > 0 {
|
if len(m.sites) > 0 {
|
||||||
m.deleteID = m.sites[m.cursor].ID
|
m.deleteID = m.sites[m.cursor].ID
|
||||||
m.deleteName = m.sites[m.cursor].Name
|
m.deleteName = m.sites[m.cursor].Name
|
||||||
m.deleteTab = 0
|
m.deleteTab = tabMonitors
|
||||||
m.state = stateConfirmDelete
|
m.state = stateConfirmDelete
|
||||||
}
|
}
|
||||||
case 1:
|
case tabMaint:
|
||||||
if len(m.alerts) > 0 {
|
|
||||||
m.deleteID = m.alerts[m.cursor].ID
|
|
||||||
m.deleteName = m.alerts[m.cursor].Name
|
|
||||||
m.deleteTab = 1
|
|
||||||
m.state = stateConfirmDelete
|
|
||||||
}
|
|
||||||
case 4:
|
|
||||||
if len(m.maintenanceWindows) > 0 {
|
if len(m.maintenanceWindows) > 0 {
|
||||||
m.deleteID = m.maintenanceWindows[m.cursor].ID
|
m.deleteID = m.maintenanceWindows[m.cursor].ID
|
||||||
m.deleteName = m.maintenanceWindows[m.cursor].Title
|
m.deleteName = m.maintenanceWindows[m.cursor].Title
|
||||||
m.deleteTab = 4
|
m.deleteTab = tabMaint
|
||||||
m.state = stateConfirmDelete
|
m.state = stateConfirmDelete
|
||||||
}
|
}
|
||||||
case 5:
|
case tabSettings:
|
||||||
|
switch m.settingsSection {
|
||||||
|
case sectionAlerts:
|
||||||
|
if len(m.alerts) > 0 {
|
||||||
|
m.deleteID = m.alerts[m.cursor].ID
|
||||||
|
m.deleteName = m.alerts[m.cursor].Name
|
||||||
|
m.deleteTab = tabSettings
|
||||||
|
m.state = stateConfirmDelete
|
||||||
|
}
|
||||||
|
case sectionUsers:
|
||||||
if m.isAdmin && len(m.users) > 0 {
|
if m.isAdmin && len(m.users) > 0 {
|
||||||
m.deleteID = m.users[m.cursor].ID
|
m.deleteID = m.users[m.cursor].ID
|
||||||
m.deleteName = m.users[m.cursor].Username
|
m.deleteName = m.users[m.cursor].Username
|
||||||
m.deleteTab = 5
|
m.deleteTab = tabSettings
|
||||||
m.state = stateConfirmDelete
|
m.state = stateConfirmDelete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||||
tabCount := 5
|
tabCount := tabSettings + 1
|
||||||
if m.isAdmin {
|
|
||||||
tabCount = 6
|
|
||||||
}
|
|
||||||
for i := 0; i < tabCount; i++ {
|
for i := 0; i < tabCount; i++ {
|
||||||
if m.zones.Get(fmt.Sprintf("tab-%d", i)).InBounds(msg) {
|
if m.zones.Get(fmt.Sprintf("tab-%d", i)).InBounds(msg) {
|
||||||
m.switchTab(i)
|
m.switchTab(i)
|
||||||
@@ -743,24 +724,14 @@ func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) switchTab(idx int) {
|
func (m *Model) switchTab(idx int) {
|
||||||
maxTabs := 4
|
maxTabs := tabSettings
|
||||||
if m.isAdmin {
|
|
||||||
maxTabs = 5
|
|
||||||
}
|
|
||||||
if idx > maxTabs {
|
if idx > maxTabs {
|
||||||
idx = 0
|
idx = 0
|
||||||
}
|
}
|
||||||
m.currentTab = idx
|
m.currentTab = idx
|
||||||
m.cursor = 0
|
m.cursor = 0
|
||||||
m.tableOffset = 0
|
m.tableOffset = 0
|
||||||
switch idx {
|
|
||||||
case 2:
|
|
||||||
m.state = stateLogs
|
|
||||||
case 5:
|
|
||||||
m.state = stateUsers
|
|
||||||
default:
|
|
||||||
m.state = stateDashboard
|
m.state = stateDashboard
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) adjustCursor(_ int) {
|
func (m *Model) adjustCursor(_ int) {
|
||||||
@@ -791,30 +762,36 @@ func (m *Model) submitForm() tea.Cmd {
|
|||||||
|
|
||||||
func (m Model) currentListLen() int {
|
func (m Model) currentListLen() int {
|
||||||
switch m.currentTab {
|
switch m.currentTab {
|
||||||
case 1:
|
case tabMonitors:
|
||||||
return len(m.alerts)
|
|
||||||
case 3:
|
|
||||||
return len(m.nodes)
|
|
||||||
case 4:
|
|
||||||
return len(m.maintenanceWindows)
|
|
||||||
case 5:
|
|
||||||
return len(m.users)
|
|
||||||
default:
|
|
||||||
return len(m.sites)
|
return len(m.sites)
|
||||||
|
case tabMaint:
|
||||||
|
return len(m.maintenanceWindows)
|
||||||
|
case tabSettings:
|
||||||
|
switch m.settingsSection {
|
||||||
|
case sectionAlerts:
|
||||||
|
return len(m.alerts)
|
||||||
|
case sectionNodes:
|
||||||
|
return len(m.nodes)
|
||||||
|
case sectionUsers:
|
||||||
|
return len(m.users)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) currentZonePrefix() (string, int) {
|
func (m Model) currentZonePrefix() (string, int) {
|
||||||
switch m.currentTab {
|
switch m.currentTab {
|
||||||
case 0:
|
case tabMonitors:
|
||||||
return "site", len(m.sites)
|
return "site", len(m.sites)
|
||||||
case 1:
|
case tabMaint:
|
||||||
return "alert", len(m.alerts)
|
|
||||||
case 4:
|
|
||||||
return "maint", len(m.maintenanceWindows)
|
return "maint", len(m.maintenanceWindows)
|
||||||
case 5:
|
case tabSettings:
|
||||||
|
switch m.settingsSection {
|
||||||
|
case sectionAlerts:
|
||||||
|
return "alert", len(m.alerts)
|
||||||
|
case sectionUsers:
|
||||||
return "user", len(m.users)
|
return "user", len(m.users)
|
||||||
default:
|
|
||||||
return "site", 0
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return "site", 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,20 +151,28 @@ func (m Model) viewDashboard() string {
|
|||||||
|
|
||||||
var content string
|
var content string
|
||||||
switch m.currentTab {
|
switch m.currentTab {
|
||||||
case 0:
|
case tabMonitors:
|
||||||
|
showSidebar := m.termWidth >= wideBreakpoint
|
||||||
|
if showSidebar {
|
||||||
|
availW := m.termWidth - chromePadH
|
||||||
|
leftW := availW * 70 / 100
|
||||||
|
rightW := availW - leftW
|
||||||
|
m.contentWidth = leftW
|
||||||
|
monitors := m.viewSitesTab()
|
||||||
|
left := lipgloss.NewStyle().Width(leftW).Render(monitors)
|
||||||
|
sidebar := m.viewLogsSidebar(rightW)
|
||||||
|
right := lipgloss.NewStyle().Width(rightW).Render(sidebar)
|
||||||
|
content = lipgloss.JoinHorizontal(lipgloss.Top, left, right)
|
||||||
|
} else {
|
||||||
|
m.contentWidth = m.termWidth
|
||||||
content = m.viewSitesTab()
|
content = m.viewSitesTab()
|
||||||
case 1:
|
|
||||||
content = m.viewAlertsTab()
|
|
||||||
case 2:
|
|
||||||
content = m.viewLogsTab()
|
|
||||||
case 3:
|
|
||||||
content = m.viewNodesTab()
|
|
||||||
case 4:
|
|
||||||
content = m.viewMaintTab()
|
|
||||||
case 5:
|
|
||||||
if m.isAdmin {
|
|
||||||
content = m.viewUsersTab()
|
|
||||||
}
|
}
|
||||||
|
case tabMaint:
|
||||||
|
m.contentWidth = m.termWidth
|
||||||
|
content = m.viewMaintTab()
|
||||||
|
case tabSettings:
|
||||||
|
m.contentWidth = m.termWidth
|
||||||
|
content = m.viewSettingsTab()
|
||||||
}
|
}
|
||||||
|
|
||||||
content = strings.TrimSpace(content)
|
content = strings.TrimSpace(content)
|
||||||
@@ -199,15 +207,15 @@ type tabEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderTabBar(stats dashboardStats) string {
|
func (m Model) renderTabBar(stats dashboardStats) string {
|
||||||
|
settingsCount := len(m.alerts) + len(m.nodes)
|
||||||
|
settingsWarn := stats.offlineNodes
|
||||||
|
if m.isAdmin {
|
||||||
|
settingsCount += len(m.users)
|
||||||
|
}
|
||||||
tabs := []tabEntry{
|
tabs := []tabEntry{
|
||||||
{"Monitors", stats.totalMonitors, stats.downCount + stats.lateCount},
|
{"Monitors", stats.totalMonitors, stats.downCount + stats.lateCount},
|
||||||
{"Alerts", len(m.alerts), 0},
|
|
||||||
{"Logs", 0, 0},
|
|
||||||
{"Nodes", len(m.nodes), stats.offlineNodes},
|
|
||||||
{"Maint", len(m.maintenanceWindows), stats.activeMaint},
|
{"Maint", len(m.maintenanceWindows), stats.activeMaint},
|
||||||
}
|
{"Settings", settingsCount, settingsWarn},
|
||||||
if m.isAdmin {
|
|
||||||
tabs = append(tabs, tabEntry{"Users", len(m.users), 0})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
countStyle := lipgloss.NewStyle().Foreground(m.theme.Muted)
|
countStyle := lipgloss.NewStyle().Foreground(m.theme.Muted)
|
||||||
@@ -270,23 +278,26 @@ func (m Model) renderFooter(stats dashboardStats) string {
|
|||||||
|
|
||||||
var keys string
|
var keys string
|
||||||
switch m.currentTab {
|
switch m.currentTab {
|
||||||
case 0:
|
case tabMonitors:
|
||||||
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [Space]Collapse [T]Theme [Tab]Switch [q]Quit"
|
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [Space]Collapse [T]Theme [Tab]Switch [q]Quit"
|
||||||
case 1:
|
case tabMaint:
|
||||||
keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit"
|
|
||||||
case 2:
|
|
||||||
keys = "[↑/↓]Scroll [PgUp/PgDn]Page [f]Filter [T]Theme [Tab]Switch [q]Quit"
|
|
||||||
case 4:
|
|
||||||
keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit"
|
keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit"
|
||||||
case 5:
|
case tabSettings:
|
||||||
keys = "[n]Add [d]Revoke [T]Theme [Tab]Switch [q]Quit"
|
switch m.settingsSection {
|
||||||
|
case sectionAlerts:
|
||||||
|
keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [←/→]Section [T]Theme [Tab]Switch [q]Quit"
|
||||||
|
case sectionUsers:
|
||||||
|
keys = "[n]Add [d]Revoke [←/→]Section [T]Theme [Tab]Switch [q]Quit"
|
||||||
|
default:
|
||||||
|
keys = "[←/→]Section [T]Theme [Tab]Switch [q]Quit"
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
keys = "[T]Theme [Tab]Switch [q]Quit"
|
keys = "[T]Theme [Tab]Switch [q]Quit"
|
||||||
}
|
}
|
||||||
|
|
||||||
ver := m.st.subtleStyle.Render("v" + m.version)
|
ver := m.st.subtleStyle.Render("v" + m.version)
|
||||||
line := statusLine + " " + m.st.subtleStyle.Render(keys) + " " + ver
|
line := statusLine + " " + m.st.subtleStyle.Render(keys) + " " + ver
|
||||||
if m.filterText != "" && m.currentTab == 0 {
|
if m.filterText != "" && m.currentTab == tabMonitors {
|
||||||
line = m.st.subtleStyle.Render(fmt.Sprintf("filter: %s", m.filterText)) + " " + statusLine + " " + m.st.subtleStyle.Render(keys) + " " + ver
|
line = m.st.subtleStyle.Render(fmt.Sprintf("filter: %s", m.filterText)) + " " + statusLine + " " + m.st.subtleStyle.Render(keys) + " " + ver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user