chore(tui): visual polish — detail sections, column headers, alert detail #37
@@ -2,6 +2,7 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.lerkolabs.com/lerko/uptop/internal/monitor"
|
"gitea.lerkolabs.com/lerko/uptop/internal/monitor"
|
||||||
@@ -147,8 +148,20 @@ func (m Model) viewAlertsTab() string {
|
|||||||
return "\n No alert channels configured. Press [n] to add one."
|
return "\n No alert channels configured. Press [n] to add one."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var headers []string
|
||||||
|
var widths []int
|
||||||
|
if m.isWide() {
|
||||||
|
headers = []string{"#", "", "NAME", "TYPE", "CONFIG", "LAST SENT"}
|
||||||
|
widths = []int{4, 3, 18, 12, 40, 12}
|
||||||
|
} else {
|
||||||
|
headers = []string{"#", "", "NAME", "TYPE", "CONFIG", "SENT"}
|
||||||
|
widths = []int{4, 3, 14, 10, 24, 8}
|
||||||
|
}
|
||||||
|
nameW := widths[2]
|
||||||
|
cfgW := widths[4]
|
||||||
|
|
||||||
return m.renderTable(
|
return m.renderTable(
|
||||||
[]string{"#", "", "NAME", "TYPE", "CONFIG", "LAST SENT"},
|
headers,
|
||||||
len(m.alerts),
|
len(m.alerts),
|
||||||
func(start, end int) [][]string {
|
func(start, end int) [][]string {
|
||||||
var rows [][]string
|
var rows [][]string
|
||||||
@@ -158,21 +171,67 @@ func (m Model) viewAlertsTab() string {
|
|||||||
rows = append(rows, []string{
|
rows = append(rows, []string{
|
||||||
fmt.Sprintf("%d", i+1),
|
fmt.Sprintf("%d", i+1),
|
||||||
fmtAlertHealth(h),
|
fmtAlertHealth(h),
|
||||||
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, 15)),
|
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)),
|
||||||
fmtAlertType(a.Type),
|
fmtAlertType(a.Type),
|
||||||
fmtAlertConfig(struct {
|
limitStr(fmtAlertConfig(struct {
|
||||||
Type string
|
Type string
|
||||||
Settings map[string]string
|
Settings map[string]string
|
||||||
}{a.Type, a.Settings}),
|
}{a.Type, a.Settings}), cfgW-2),
|
||||||
fmtAlertLastSent(h),
|
fmtAlertLastSent(h),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
},
|
},
|
||||||
nil, nil,
|
widths, nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) viewAlertDetailPanel() string {
|
||||||
|
if m.cursor >= len(m.alerts) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
a := m.alerts[m.cursor]
|
||||||
|
h := m.engine.GetAlertHealth(a.ID)
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
b.WriteString(subtleStyle.Render(" Alerts > ") + titleStyle.Render(a.Name) + "\n\n")
|
||||||
|
|
||||||
|
row := func(label, value string) {
|
||||||
|
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
row("Type", fmtAlertType(a.Type))
|
||||||
|
|
||||||
|
if h.LastSendAt.IsZero() {
|
||||||
|
row("Health", subtleStyle.Render("never sent"))
|
||||||
|
} else if h.LastSendOK {
|
||||||
|
row("Health", specialStyle.Render("OK"))
|
||||||
|
} else {
|
||||||
|
row("Health", dangerStyle.Render("FAILED"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.LastSendAt.IsZero() {
|
||||||
|
row("Last Sent", h.LastSendAt.Format("2006-01-02 15:04:05")+" ("+fmtAlertLastSent(h)+")")
|
||||||
|
}
|
||||||
|
if h.SendCount > 0 {
|
||||||
|
row("Sends", fmt.Sprintf("%d sent, %d failed", h.SendCount, h.FailCount))
|
||||||
|
}
|
||||||
|
if h.LastError != "" {
|
||||||
|
row("Last Error", dangerStyle.Render(limitStr(h.LastError, 60)))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("\n" + subtleStyle.Render(" CONFIGURATION") + "\n")
|
||||||
|
for k, v := range a.Settings {
|
||||||
|
row(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [t] Test [q] Quit"))
|
||||||
|
|
||||||
|
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) initAlertHuhForm() tea.Cmd {
|
func (m *Model) initAlertHuhForm() tea.Cmd {
|
||||||
m.alertFormData = &alertFormData{
|
m.alertFormData = &alertFormData{
|
||||||
AlertType: "discord",
|
AlertType: "discord",
|
||||||
|
|||||||
+27
-10
@@ -2,10 +2,11 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"gitea.lerkolabs.com/lerko/uptop/internal/models"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.lerkolabs.com/lerko/uptop/internal/models"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
@@ -40,19 +41,19 @@ func fmtMaintType(t string) string {
|
|||||||
return maintStyle.Render("maintenance")
|
return maintStyle.Render("maintenance")
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtMaintMonitor(monitorID int, sites []models.Site) string {
|
func fmtMaintMonitorW(monitorID int, sites []models.Site, maxW int) string {
|
||||||
if monitorID == 0 {
|
if monitorID == 0 {
|
||||||
return "All"
|
return "All"
|
||||||
}
|
}
|
||||||
for _, s := range sites {
|
for _, s := range sites {
|
||||||
if s.ID == monitorID {
|
if s.ID == monitorID {
|
||||||
return limitStr(s.Name, 18)
|
return limitStr(s.Name, maxW)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("#%d", monitorID)
|
return fmt.Sprintf("#%d", monitorID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtMaintTime(t time.Time) string {
|
func fmtMaintTime(t time.Time, colW int) string {
|
||||||
if t.IsZero() {
|
if t.IsZero() {
|
||||||
return subtleStyle.Render("—")
|
return subtleStyle.Render("—")
|
||||||
}
|
}
|
||||||
@@ -60,7 +61,10 @@ func fmtMaintTime(t time.Time) string {
|
|||||||
if t.Year() == now.Year() && t.YearDay() == now.YearDay() {
|
if t.Year() == now.Year() && t.YearDay() == now.YearDay() {
|
||||||
return t.Format("15:04")
|
return t.Format("15:04")
|
||||||
}
|
}
|
||||||
|
if colW >= 14 {
|
||||||
return t.Format("15:04 Jan 02")
|
return t.Format("15:04 Jan 02")
|
||||||
|
}
|
||||||
|
return t.Format("Jan 02")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) isMonitorInMaintenance(monitorID int) bool {
|
func (m Model) isMonitorInMaintenance(monitorID int) bool {
|
||||||
@@ -92,8 +96,21 @@ func (m Model) viewMaintTab() string {
|
|||||||
return "\n No maintenance windows or incidents. Press [n] to create one."
|
return "\n No maintenance windows or incidents. Press [n] to create one."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var headers []string
|
||||||
|
var widths []int
|
||||||
|
if m.isWide() {
|
||||||
|
headers = []string{"#", "TITLE", "TYPE", "MONITORS", "STATUS", "STARTED", "ENDS"}
|
||||||
|
widths = []int{4, 24, 14, 22, 12, 16, 16}
|
||||||
|
} else {
|
||||||
|
headers = []string{"#", "TITLE", "TYPE", "MON", "ST", "START", "ENDS"}
|
||||||
|
widths = []int{4, 14, 13, 14, 11, 14, 14}
|
||||||
|
}
|
||||||
|
titleW := widths[1]
|
||||||
|
monW := widths[3]
|
||||||
|
timeW := widths[5]
|
||||||
|
|
||||||
return m.renderTable(
|
return m.renderTable(
|
||||||
[]string{"#", "TITLE", "TYPE", "MONITORS", "STATUS", "STARTED", "ENDS"},
|
headers,
|
||||||
len(m.maintenanceWindows),
|
len(m.maintenanceWindows),
|
||||||
func(start, end int) [][]string {
|
func(start, end int) [][]string {
|
||||||
var rows [][]string
|
var rows [][]string
|
||||||
@@ -102,17 +119,17 @@ func (m Model) viewMaintTab() string {
|
|||||||
mw := m.maintenanceWindows[i]
|
mw := m.maintenanceWindows[i]
|
||||||
rows = append(rows, []string{
|
rows = append(rows, []string{
|
||||||
strconv.Itoa(i + 1),
|
strconv.Itoa(i + 1),
|
||||||
m.zones.Mark(fmt.Sprintf("maint-%d", i), limitStr(mw.Title, 24)),
|
m.zones.Mark(fmt.Sprintf("maint-%d", i), limitStr(mw.Title, titleW-2)),
|
||||||
fmtMaintType(mw.Type),
|
fmtMaintType(mw.Type),
|
||||||
fmtMaintMonitor(mw.MonitorID, allSites),
|
fmtMaintMonitorW(mw.MonitorID, allSites, monW-2),
|
||||||
fmtMaintStatus(mw),
|
fmtMaintStatus(mw),
|
||||||
fmtMaintTime(mw.StartTime),
|
fmtMaintTime(mw.StartTime, timeW),
|
||||||
fmtMaintTime(mw.EndTime),
|
fmtMaintTime(mw.EndTime, timeW),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
},
|
},
|
||||||
[]int{6, 0, 14, 20, 12, 16, 16},
|
widths,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,25 @@ func (m Model) viewNodesTab() string {
|
|||||||
return "\n No probe nodes connected."
|
return "\n No probe nodes connected."
|
||||||
}
|
}
|
||||||
|
|
||||||
colWidths := []int{0, 12, 20, 10, 8}
|
var headers []string
|
||||||
|
var widths []int
|
||||||
|
if m.isWide() {
|
||||||
|
headers = []string{"NAME", "REGION", "LAST SEEN", "VERSION", "STATUS"}
|
||||||
|
widths = []int{24, 14, 16, 12, 10}
|
||||||
|
} else {
|
||||||
|
headers = []string{"NAME", "REGION", "SEEN", "VER", "STATUS"}
|
||||||
|
widths = []int{16, 10, 10, 8, 8}
|
||||||
|
}
|
||||||
|
nameW := widths[0]
|
||||||
|
|
||||||
return m.renderTable(
|
return m.renderTable(
|
||||||
[]string{"NAME", "REGION", "LAST SEEN", "VERSION", "STATUS"},
|
headers,
|
||||||
len(m.nodes),
|
len(m.nodes),
|
||||||
func(start, end int) [][]string {
|
func(start, end int) [][]string {
|
||||||
var rows [][]string
|
var rows [][]string
|
||||||
for i := start; i < end; i++ {
|
for i := start; i < end; i++ {
|
||||||
node := m.nodes[i]
|
node := m.nodes[i]
|
||||||
name := limitStr(node.Name, 20)
|
name := limitStr(node.Name, nameW-2)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = node.ID
|
name = node.ID
|
||||||
}
|
}
|
||||||
@@ -37,7 +46,7 @@ func (m Model) viewNodesTab() string {
|
|||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
},
|
},
|
||||||
colWidths,
|
widths,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-22
@@ -334,28 +334,71 @@ func fmtDuration(d time.Duration) string {
|
|||||||
return fmt.Sprintf("%dd", days)
|
return fmt.Sprintf("%dd", days)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) dynamicWidths() (nameW, sparkW int) {
|
type tableLayout struct {
|
||||||
fixed := 6 + 10 + 10 + 8 + 8 + 7 + 9 // #, TYPE, STATUS, LATENCY, UPTIME, SSL, RETRY
|
nameW, sparkW int
|
||||||
overhead := 30 // cell padding + borders
|
headers []string
|
||||||
avail := m.termWidth - chromePadH - 2 - fixed - overhead
|
colWidths []int
|
||||||
if avail < 30 {
|
}
|
||||||
avail = 30
|
|
||||||
|
func (m Model) computeLayout() tableLayout {
|
||||||
|
wide := m.isWide()
|
||||||
|
|
||||||
|
var fixed int
|
||||||
|
var headers []string
|
||||||
|
var widths []int
|
||||||
|
|
||||||
|
if wide {
|
||||||
|
// # NAME TYPE STATUS LATENCY UPTIME HISTORY SSL RETRIES
|
||||||
|
headers = []string{"#", "NAME", "TYPE", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRIES"}
|
||||||
|
widths = []int{4, 0, 10, 10, 10, 8, 0, 7, 9}
|
||||||
|
fixed = 4 + 10 + 10 + 10 + 8 + 7 + 9
|
||||||
|
} else {
|
||||||
|
// # NAME TYPE STATUS LAT UP% HISTORY SSL RT
|
||||||
|
headers = []string{"#", "NAME", "TYPE", "STATUS", "LAT", "UP%", "HISTORY", "SSL", "RT"}
|
||||||
|
widths = []int{4, 0, 8, 8, 7, 8, 0, 5, 5}
|
||||||
|
fixed = 4 + 8 + 8 + 7 + 8 + 5 + 5
|
||||||
|
}
|
||||||
|
|
||||||
|
numCols := len(headers)
|
||||||
|
borderOverhead := 2 + (numCols - 1)
|
||||||
|
avail := m.termWidth - chromePadH - 2 - borderOverhead - fixed
|
||||||
|
if avail < 20 {
|
||||||
|
avail = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
maxName := 0
|
||||||
|
for _, s := range m.sites {
|
||||||
|
if n := len([]rune(s.Name)); n > maxName {
|
||||||
|
maxName = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maxName += 4
|
||||||
|
|
||||||
|
nameW := avail / 2
|
||||||
|
if nameW > maxName {
|
||||||
|
nameW = maxName
|
||||||
}
|
}
|
||||||
nameW = avail / 2
|
|
||||||
sparkW = avail - nameW - 2 // -2 for spark column padding
|
|
||||||
if nameW < 13 {
|
if nameW < 13 {
|
||||||
nameW = 13
|
nameW = 13
|
||||||
}
|
}
|
||||||
if nameW > 40 {
|
if nameW > 40 {
|
||||||
nameW = 40
|
nameW = 40
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sparkW := avail - nameW
|
||||||
if sparkW < 10 {
|
if sparkW < 10 {
|
||||||
sparkW = 10
|
sparkW = 10
|
||||||
}
|
}
|
||||||
if sparkW > 60 {
|
|
||||||
sparkW = 60
|
widths[1] = nameW
|
||||||
|
widths[6] = sparkW
|
||||||
|
|
||||||
|
return tableLayout{
|
||||||
|
nameW: nameW,
|
||||||
|
sparkW: sparkW,
|
||||||
|
headers: headers,
|
||||||
|
colWidths: widths,
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) viewSitesTab() string {
|
func (m Model) viewSitesTab() string {
|
||||||
@@ -373,12 +416,16 @@ func (m Model) viewSitesTab() string {
|
|||||||
return "\n" + welcome
|
return "\n" + welcome
|
||||||
}
|
}
|
||||||
|
|
||||||
nameW, sparkWidth := m.dynamicWidths()
|
layout := m.computeLayout()
|
||||||
colWidths := []int{6, 0, 10, 10, 8, 8, sparkWidth + 2, 7, 9}
|
nameW := layout.nameW
|
||||||
|
sparkWidth := layout.sparkW - 2
|
||||||
|
if sparkWidth < 8 {
|
||||||
|
sparkWidth = 8
|
||||||
|
}
|
||||||
|
|
||||||
var groupRows map[int]bool
|
var groupRows map[int]bool
|
||||||
return m.renderTable(
|
return m.renderTable(
|
||||||
[]string{"#", "NAME", "TYPE", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRY"},
|
layout.headers,
|
||||||
len(m.sites),
|
len(m.sites),
|
||||||
func(start, end int) [][]string {
|
func(start, end int) [][]string {
|
||||||
groupRows = make(map[int]bool)
|
groupRows = make(map[int]bool)
|
||||||
@@ -391,7 +438,7 @@ func (m Model) viewSitesTab() string {
|
|||||||
icon := typeIcon("group", m.collapsed[site.ID])
|
icon := typeIcon("group", m.collapsed[site.ID])
|
||||||
rows = append(rows, []string{
|
rows = append(rows, []string{
|
||||||
strconv.Itoa(i + 1),
|
strconv.Itoa(i + 1),
|
||||||
m.zones.Mark(fmt.Sprintf("site-%d", i), icon+" "+limitStr(site.Name, nameW-2)),
|
m.zones.Mark(fmt.Sprintf("site-%d", i), icon+" "+limitStr(site.Name, nameW-4)),
|
||||||
"group",
|
"group",
|
||||||
fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
||||||
subtleStyle.Render("—"),
|
subtleStyle.Render("—"),
|
||||||
@@ -409,14 +456,14 @@ func (m Model) viewSitesTab() string {
|
|||||||
if i+1 >= len(m.sites) || m.sites[i+1].ParentID != site.ParentID {
|
if i+1 >= len(m.sites) || m.sites[i+1].ParentID != site.ParentID {
|
||||||
prefix = "└"
|
prefix = "└"
|
||||||
}
|
}
|
||||||
name = prefix + " " + limitStr(name, nameW-2)
|
name = prefix + " " + limitStr(name, nameW-4)
|
||||||
} else {
|
} else {
|
||||||
name = limitStr(name, nameW)
|
name = limitStr(name, nameW-2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (site.Status == "DOWN" || site.Status == "SSL EXP" || site.Status == "LATE") && site.LastError != "" {
|
if (site.Status == "DOWN" || site.Status == "SSL EXP" || site.Status == "LATE") && site.LastError != "" {
|
||||||
nameLen := len([]rune(name))
|
nameLen := len([]rune(name))
|
||||||
errSpace := nameW - nameLen - 1
|
errSpace := nameW - nameLen - 3
|
||||||
if errSpace > 10 {
|
if errSpace > 10 {
|
||||||
name = name + " " + subtleStyle.Render(limitStr(site.LastError, errSpace))
|
name = name + " " + subtleStyle.Render(limitStr(site.LastError, errSpace))
|
||||||
}
|
}
|
||||||
@@ -444,7 +491,7 @@ func (m Model) viewSitesTab() string {
|
|||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
},
|
},
|
||||||
colWidths,
|
layout.colWidths,
|
||||||
func(row, col int) *lipgloss.Style {
|
func(row, col int) *lipgloss.Style {
|
||||||
if groupRows[row] {
|
if groupRows[row] {
|
||||||
s := siteGroupStyle
|
s := siteGroupStyle
|
||||||
@@ -764,6 +811,10 @@ func (m Model) viewDetailPanel() string {
|
|||||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
|
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
section := func(label string) {
|
||||||
|
b.WriteString("\n" + subtleStyle.Render(" "+label) + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
row("Status", fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)))
|
row("Status", fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)))
|
||||||
|
|
||||||
if (site.Status == "DOWN" || site.Status == "SSL EXP" || site.Status == "LATE") && site.LastError != "" {
|
if (site.Status == "DOWN" || site.Status == "SSL EXP" || site.Status == "LATE") && site.LastError != "" {
|
||||||
@@ -792,6 +843,8 @@ func (m Model) viewDetailPanel() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
section("ENDPOINT")
|
||||||
row("Type", site.Type)
|
row("Type", site.Type)
|
||||||
if site.URL != "" {
|
if site.URL != "" {
|
||||||
row("URL", site.URL)
|
row("URL", site.URL)
|
||||||
@@ -802,20 +855,36 @@ func (m Model) viewDetailPanel() string {
|
|||||||
if site.Port > 0 {
|
if site.Port > 0 {
|
||||||
row("Port", strconv.Itoa(site.Port))
|
row("Port", strconv.Itoa(site.Port))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
section("TIMING")
|
||||||
row("Interval", fmt.Sprintf("%ds", site.Interval))
|
row("Interval", fmt.Sprintf("%ds", site.Interval))
|
||||||
|
if site.Timeout > 0 {
|
||||||
row("Timeout", fmt.Sprintf("%ds", site.Timeout))
|
row("Timeout", fmt.Sprintf("%ds", site.Timeout))
|
||||||
|
}
|
||||||
row("Latency", fmtLatency(site.Latency))
|
row("Latency", fmtLatency(site.Latency))
|
||||||
row("Uptime", fmtUptime(hist.Statuses))
|
row("Uptime", fmtUptime(hist.Statuses))
|
||||||
|
if !site.LastCheck.IsZero() {
|
||||||
|
row("Last Check", site.LastCheck.Format("15:04:05"))
|
||||||
|
}
|
||||||
|
|
||||||
if site.Type == "http" {
|
if site.Type == "http" {
|
||||||
|
section("HTTP")
|
||||||
|
if site.Method != "" && site.Method != "GET" {
|
||||||
row("Method", site.Method)
|
row("Method", site.Method)
|
||||||
row("Codes", site.AcceptedCodes)
|
}
|
||||||
|
codes := site.AcceptedCodes
|
||||||
|
if codes == "" {
|
||||||
|
codes = "200-299"
|
||||||
|
}
|
||||||
|
row("Codes", codes)
|
||||||
row("SSL", fmtSSL(site))
|
row("SSL", fmtSSL(site))
|
||||||
if site.IgnoreTLS {
|
if site.IgnoreTLS {
|
||||||
row("TLS Verify", dangerStyle.Render("disabled"))
|
row("TLS Verify", dangerStyle.Render("disabled"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if site.MaxRetries > 0 || site.Regions != "" || site.Description != "" {
|
||||||
|
section("CONFIG")
|
||||||
if site.MaxRetries > 0 {
|
if site.MaxRetries > 0 {
|
||||||
row("Retries", fmtRetries(site))
|
row("Retries", fmtRetries(site))
|
||||||
}
|
}
|
||||||
@@ -825,8 +894,6 @@ func (m Model) viewDetailPanel() string {
|
|||||||
if site.Description != "" {
|
if site.Description != "" {
|
||||||
row("Description", site.Description)
|
row("Description", site.Description)
|
||||||
}
|
}
|
||||||
if !site.LastCheck.IsZero() {
|
|
||||||
row("Last Check", site.LastCheck.Format("15:04:05"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
probeResults := m.engine.GetProbeResults(site.ID)
|
probeResults := m.engine.GetProbeResults(site.ID)
|
||||||
|
|||||||
@@ -32,8 +32,19 @@ func (m Model) viewUsersTab() string {
|
|||||||
return "\n No users configured. Press [n] to add one."
|
return "\n No users configured. Press [n] to add one."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var headers []string
|
||||||
|
var widths []int
|
||||||
|
if m.isWide() {
|
||||||
|
headers = []string{"#", "USERNAME", "ROLE", "PUBLIC KEY"}
|
||||||
|
widths = []int{4, 18, 10, 50}
|
||||||
|
} else {
|
||||||
|
headers = []string{"#", "USER", "ROLE", "KEY"}
|
||||||
|
widths = []int{4, 14, 8, 30}
|
||||||
|
}
|
||||||
|
userW := widths[1]
|
||||||
|
|
||||||
return m.renderTable(
|
return m.renderTable(
|
||||||
[]string{"#", "USERNAME", "ROLE", "PUBLIC KEY"},
|
headers,
|
||||||
len(m.users),
|
len(m.users),
|
||||||
func(start, end int) [][]string {
|
func(start, end int) [][]string {
|
||||||
var rows [][]string
|
var rows [][]string
|
||||||
@@ -41,14 +52,14 @@ func (m Model) viewUsersTab() string {
|
|||||||
u := m.users[i]
|
u := m.users[i]
|
||||||
rows = append(rows, []string{
|
rows = append(rows, []string{
|
||||||
fmt.Sprintf("%d", i+1),
|
fmt.Sprintf("%d", i+1),
|
||||||
m.zones.Mark(fmt.Sprintf("user-%d", i), limitStr(u.Username, 15)),
|
m.zones.Mark(fmt.Sprintf("user-%d", i), limitStr(u.Username, userW-2)),
|
||||||
fmtRole(u.Role),
|
fmtRole(u.Role),
|
||||||
fmtKey(u.PublicKey),
|
fmtKey(u.PublicKey),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
},
|
},
|
||||||
nil, nil,
|
widths, nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ var (
|
|||||||
|
|
||||||
type StyleOverride func(row, col int) *lipgloss.Style
|
type StyleOverride func(row, col int) *lipgloss.Style
|
||||||
|
|
||||||
|
const wideBreakpoint = 120
|
||||||
|
|
||||||
|
func (m Model) isWide() bool {
|
||||||
|
return m.termWidth >= 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 {
|
||||||
if items == 0 {
|
if items == 0 {
|
||||||
return ""
|
return ""
|
||||||
@@ -28,7 +34,16 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
selectedVisual := m.cursor - m.tableOffset
|
selectedVisual := m.cursor - m.tableOffset
|
||||||
rows := buildRows(m.tableOffset, end)
|
rows := buildRows(m.tableOffset, end)
|
||||||
|
|
||||||
tableWidth := m.termWidth - chromePadH - 2
|
colTotal := 0
|
||||||
|
for _, w := range colWidths {
|
||||||
|
colTotal += w
|
||||||
|
}
|
||||||
|
borderOverhead := 2 + len(colWidths) - 1
|
||||||
|
tableWidth := colTotal + borderOverhead
|
||||||
|
maxWidth := m.termWidth - chromePadH - 2
|
||||||
|
if tableWidth > maxWidth {
|
||||||
|
tableWidth = maxWidth
|
||||||
|
}
|
||||||
if tableWidth < 40 {
|
if tableWidth < 40 {
|
||||||
tableWidth = 40
|
tableWidth = 40
|
||||||
}
|
}
|
||||||
@@ -41,7 +56,11 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
Rows(rows...).
|
Rows(rows...).
|
||||||
StyleFunc(func(row, col int) lipgloss.Style {
|
StyleFunc(func(row, col int) lipgloss.Style {
|
||||||
if row == table.HeaderRow {
|
if row == table.HeaderRow {
|
||||||
return tableHeaderStyle
|
h := tableHeaderStyle
|
||||||
|
if col < len(colWidths) && colWidths[col] > 0 {
|
||||||
|
h = h.Width(colWidths[col]).MaxWidth(colWidths[col])
|
||||||
|
}
|
||||||
|
return h
|
||||||
}
|
}
|
||||||
isSelected := row == selectedVisual
|
isSelected := row == selectedVisual
|
||||||
if styleOverride != nil {
|
if styleOverride != nil {
|
||||||
@@ -51,7 +70,7 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
style = tableSelectedStyle.Foreground(s.GetForeground())
|
style = tableSelectedStyle.Foreground(s.GetForeground())
|
||||||
}
|
}
|
||||||
if col < len(colWidths) && colWidths[col] > 0 {
|
if col < len(colWidths) && colWidths[col] > 0 {
|
||||||
style = style.Width(colWidths[col])
|
style = style.Width(colWidths[col]).MaxWidth(colWidths[col])
|
||||||
}
|
}
|
||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
@@ -64,7 +83,7 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
base = tableSelectedStyle
|
base = tableSelectedStyle
|
||||||
}
|
}
|
||||||
if col < len(colWidths) && colWidths[col] > 0 {
|
if col < len(colWidths) && colWidths[col] > 0 {
|
||||||
base = base.Width(colWidths[col])
|
base = base.Width(colWidths[col]).MaxWidth(colWidths[col])
|
||||||
}
|
}
|
||||||
return base
|
return base
|
||||||
})
|
})
|
||||||
|
|||||||
+14
-1
@@ -68,6 +68,7 @@ const (
|
|||||||
stateLogs
|
stateLogs
|
||||||
stateUsers
|
stateUsers
|
||||||
stateDetail
|
stateDetail
|
||||||
|
stateAlertDetail
|
||||||
stateFormSite
|
stateFormSite
|
||||||
stateFormAlert
|
stateFormAlert
|
||||||
stateFormUser
|
stateFormUser
|
||||||
@@ -384,6 +385,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
case stateAlertDetail:
|
||||||
|
switch msg.String() {
|
||||||
|
case "i", "esc":
|
||||||
|
m.state = stateDashboard
|
||||||
|
case "q":
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
case stateDashboard, stateLogs, stateUsers:
|
case stateDashboard, stateLogs, stateUsers:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "q":
|
case "q":
|
||||||
@@ -497,6 +506,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case "i":
|
case "i":
|
||||||
if m.currentTab == 0 && len(m.sites) > 0 {
|
if m.currentTab == 0 && len(m.sites) > 0 {
|
||||||
m.state = stateDetail
|
m.state = stateDetail
|
||||||
|
} else if m.currentTab == 1 && len(m.alerts) > 0 {
|
||||||
|
m.state = stateAlertDetail
|
||||||
}
|
}
|
||||||
case "x":
|
case "x":
|
||||||
if m.currentTab == 4 && len(m.maintenanceWindows) > 0 {
|
if m.currentTab == 4 && len(m.maintenanceWindows) > 0 {
|
||||||
@@ -818,6 +829,8 @@ func (m Model) View() string {
|
|||||||
return ""
|
return ""
|
||||||
case stateDetail:
|
case stateDetail:
|
||||||
return m.viewDetailPanel()
|
return m.viewDetailPanel()
|
||||||
|
case stateAlertDetail:
|
||||||
|
return m.viewAlertDetailPanel()
|
||||||
default:
|
default:
|
||||||
return m.zones.Scan(m.viewDashboard())
|
return m.zones.Scan(m.viewDashboard())
|
||||||
}
|
}
|
||||||
@@ -954,7 +967,7 @@ func (m Model) viewDashboard() string {
|
|||||||
case 0:
|
case 0:
|
||||||
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [T]Theme [Tab]Switch [q]Quit"
|
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [T]Theme [Tab]Switch [q]Quit"
|
||||||
case 1:
|
case 1:
|
||||||
keys = "[n]New [e]Edit [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit"
|
keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit"
|
||||||
case 2:
|
case 2:
|
||||||
keys = "[f]Filter [T]Theme [Tab]Switch [q]Quit"
|
keys = "[f]Filter [T]Theme [Tab]Switch [q]Quit"
|
||||||
case 4:
|
case 4:
|
||||||
|
|||||||
Reference in New Issue
Block a user