cfbf01274d
## Summary Bundled remaining UX polish items from the screenshot review. ### Changes **Detail panel sections (#5)** - Fields grouped into ENDPOINT, TIMING, HTTP, CONFIG sections with subtle headers - Matches existing PROBE RESULTS and STATE CHANGES section pattern - Cleaner visual hierarchy without box-drawing clutter **Omit unconfigured fields (#6)** - Timeout hidden when 0 (unconfigured) - Method hidden when default GET - AcceptedCodes shows "200-299" explicitly when empty **Column header (#7)** - `LATENCY` → `LAT` (design short, never truncate — htop/btop pattern) **Alert detail view (#8)** - `i` key on Alerts tab opens full detail panel - Shows: type, health status, last sent time, send/fail counts, last error - Full config key:value pairs (untruncated) - Keybinding: `[i/Esc] Back [e] Edit [t] Test [q] Quit` ### Files (3) - `internal/tui/tab_sites.go` — section headers, field omission, LAT header - `internal/tui/tab_alerts.go` — viewAlertDetailPanel() - `internal/tui/tui.go` — stateAlertDetail, key handler, render routing Reviewed-on: lerko/uptop#37
126 lines
2.6 KiB
Go
126 lines
2.6 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/huh"
|
|
)
|
|
|
|
type userFormData struct {
|
|
Username string
|
|
PublicKey string
|
|
Role string
|
|
}
|
|
|
|
func fmtRole(role string) string {
|
|
if role == "admin" {
|
|
return specialStyle.Render(role)
|
|
}
|
|
return role
|
|
}
|
|
|
|
func fmtKey(key string) string {
|
|
if len(key) > 40 {
|
|
return key[:20] + "..." + key[len(key)-17:]
|
|
}
|
|
return key
|
|
}
|
|
|
|
func (m Model) viewUsersTab() string {
|
|
if len(m.users) == 0 {
|
|
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(
|
|
headers,
|
|
len(m.users),
|
|
func(start, end int) [][]string {
|
|
var rows [][]string
|
|
for i := start; i < end; i++ {
|
|
u := m.users[i]
|
|
rows = append(rows, []string{
|
|
fmt.Sprintf("%d", i+1),
|
|
m.zones.Mark(fmt.Sprintf("user-%d", i), limitStr(u.Username, userW-2)),
|
|
fmtRole(u.Role),
|
|
fmtKey(u.PublicKey),
|
|
})
|
|
}
|
|
return rows
|
|
},
|
|
widths, nil,
|
|
)
|
|
}
|
|
|
|
func (m *Model) initUserHuhForm() tea.Cmd {
|
|
m.userFormData = &userFormData{
|
|
Role: "user",
|
|
}
|
|
|
|
if m.editID > 0 {
|
|
for _, u := range m.users {
|
|
if u.ID == m.editID {
|
|
m.userFormData.Username = u.Username
|
|
m.userFormData.PublicKey = u.PublicKey
|
|
m.userFormData.Role = u.Role
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
m.huhForm = huh.NewForm(
|
|
huh.NewGroup(
|
|
huh.NewInput().Title("Username").
|
|
Placeholder("admin").
|
|
Value(&m.userFormData.Username).
|
|
Validate(func(s string) error {
|
|
if s == "" {
|
|
return fmt.Errorf("username is required")
|
|
}
|
|
return nil
|
|
}),
|
|
huh.NewInput().Title("SSH Public Key").
|
|
Placeholder("ssh-ed25519 AAAA...").
|
|
Value(&m.userFormData.PublicKey).
|
|
Validate(func(s string) error {
|
|
if s == "" {
|
|
return fmt.Errorf("public key is required")
|
|
}
|
|
return nil
|
|
}),
|
|
huh.NewSelect[string]().Title("Role").
|
|
Options(
|
|
huh.NewOption("User", "user"),
|
|
huh.NewOption("Admin", "admin"),
|
|
).Value(&m.userFormData.Role),
|
|
).Title("SSH Access"),
|
|
).WithTheme(m.theme.HuhTheme())
|
|
|
|
return m.huhForm.Init()
|
|
}
|
|
|
|
func (m *Model) submitUserForm() {
|
|
d := m.userFormData
|
|
if m.editID > 0 {
|
|
if err := m.store.UpdateUser(m.editID, d.Username, d.PublicKey, d.Role); err != nil {
|
|
m.engine.AddLog("Update user failed: " + err.Error())
|
|
}
|
|
} else {
|
|
if err := m.store.AddUser(d.Username, d.PublicKey, d.Role); err != nil {
|
|
m.engine.AddLog("Add user failed: " + err.Error())
|
|
}
|
|
}
|
|
m.state = stateUsers
|
|
}
|