refactor(tui): restructure site form to 2 type-aware pages
Replace 4-page paginated form (17 fields for HTTP) with a 2-page type-aware layout. Page 1 shows core fields + type-specific target (URL for HTTP, Hostname for ping, etc). Page 2 shows configuration with pre-filled defaults. Group type gets 1 page. Form rebuilds dynamically when monitor type changes, preserving all entered values via pointer-bound siteFormData. Focus returns to the Type select after rebuild so users can continue forward. WithWidth set explicitly on rebuild to prevent placeholder truncation.
This commit was merged in pull request #132.
This commit is contained in:
+163
-138
@@ -326,101 +326,104 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// m.alerts is the tab-data cache (≤5s stale) — no store IO in Update.
|
return m.rebuildSiteForm()
|
||||||
alertOpts := []huh.Option[string]{huh.NewOption("None", "0")}
|
}
|
||||||
|
|
||||||
|
func (m *Model) rebuildSiteForm() tea.Cmd {
|
||||||
|
groups := m.buildSiteFormGroups()
|
||||||
|
m.huhForm = huh.NewForm(groups...).WithTheme(m.theme.HuhTheme())
|
||||||
|
if m.termWidth > 0 {
|
||||||
|
m.huhForm.WithWidth(m.termWidth)
|
||||||
|
}
|
||||||
|
formHeight := m.termHeight - 7
|
||||||
|
if formHeight < 5 {
|
||||||
|
formHeight = 5
|
||||||
|
}
|
||||||
|
m.huhForm.WithHeight(formHeight)
|
||||||
|
m.lastSiteType = m.siteFormData.SiteType
|
||||||
|
return m.huhForm.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) siteFormOptions() (alertOpts, groupOpts []huh.Option[string]) {
|
||||||
|
alertOpts = []huh.Option[string]{huh.NewOption("None", "0")}
|
||||||
for _, a := range m.alerts {
|
for _, a := range m.alerts {
|
||||||
alertOpts = append(alertOpts, huh.NewOption(
|
alertOpts = append(alertOpts, huh.NewOption(
|
||||||
fmt.Sprintf("%s (%s)", a.Name, a.Type),
|
fmt.Sprintf("%s (%s)", a.Name, a.Type),
|
||||||
strconv.Itoa(a.ID),
|
strconv.Itoa(a.ID),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
groupOpts = []huh.Option[string]{huh.NewOption("None", "0")}
|
||||||
groupOpts := []huh.Option[string]{huh.NewOption("None", "0")}
|
|
||||||
for _, s := range m.sites {
|
for _, s := range m.sites {
|
||||||
if s.Type == "group" && s.ID != m.editID {
|
if s.Type == "group" && s.ID != m.editID {
|
||||||
groupOpts = append(groupOpts, huh.NewOption(s.Name, strconv.Itoa(s.ID)))
|
groupOpts = append(groupOpts, huh.NewOption(s.Name, strconv.Itoa(s.ID)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
m.huhForm = huh.NewForm(
|
func (m *Model) buildSiteFormGroups() []*huh.Group {
|
||||||
huh.NewGroup(
|
d := m.siteFormData
|
||||||
huh.NewInput().Title("Monitor Name").
|
alertOpts, groupOpts := m.siteFormOptions()
|
||||||
Placeholder("My Service").
|
|
||||||
Value(&m.siteFormData.Name).
|
// Page 1 — Monitor Setup: core fields + type-specific target
|
||||||
Validate(func(s string) error {
|
setup := []huh.Field{
|
||||||
if s == "" {
|
huh.NewInput().Title("Monitor Name").
|
||||||
return fmt.Errorf("name is required")
|
Placeholder("My Service").
|
||||||
}
|
Value(&d.Name).
|
||||||
return nil
|
Validate(func(s string) error {
|
||||||
}),
|
if s == "" {
|
||||||
huh.NewSelect[string]().Title("Monitor Type").
|
return fmt.Errorf("name is required")
|
||||||
Options(
|
}
|
||||||
huh.NewOption("HTTP/HTTPS", "http"),
|
return nil
|
||||||
huh.NewOption("Push / Heartbeat", "push"),
|
}),
|
||||||
huh.NewOption("Ping (ICMP)", "ping"),
|
huh.NewSelect[string]().Title("Monitor Type").
|
||||||
huh.NewOption("TCP Port", "port"),
|
Options(
|
||||||
huh.NewOption("DNS", "dns"),
|
huh.NewOption("HTTP/HTTPS", "http"),
|
||||||
huh.NewOption("Group", "group"),
|
huh.NewOption("Push / Heartbeat", "push"),
|
||||||
).Value(&m.siteFormData.SiteType),
|
huh.NewOption("Ping (ICMP)", "ping"),
|
||||||
huh.NewSelect[string]().Title("Alert Channel").
|
huh.NewOption("TCP Port", "port"),
|
||||||
Options(alertOpts...).
|
huh.NewOption("DNS", "dns"),
|
||||||
Value(&m.siteFormData.AlertID),
|
huh.NewOption("Group", "group"),
|
||||||
).Title("Monitor Settings"),
|
).Value(&d.SiteType),
|
||||||
huh.NewGroup(
|
huh.NewSelect[string]().Title("Alert Channel").
|
||||||
huh.NewInput().Title("URL").
|
Options(alertOpts...).
|
||||||
Placeholder("https://example.com").
|
Value(&d.AlertID),
|
||||||
Description("Required for HTTP monitors").
|
}
|
||||||
Value(&m.siteFormData.URL).
|
|
||||||
Validate(func(s string) error {
|
switch d.SiteType {
|
||||||
if m.siteFormData.SiteType != "http" {
|
case "http":
|
||||||
return nil
|
setup = append(setup, huh.NewInput().Title("URL").
|
||||||
}
|
Placeholder("https://example.com").
|
||||||
if s == "" {
|
Value(&d.URL).
|
||||||
return fmt.Errorf("URL is required for HTTP monitors")
|
Validate(func(s string) error {
|
||||||
}
|
if s == "" {
|
||||||
u, err := url.Parse(s)
|
return fmt.Errorf("URL is required")
|
||||||
if err != nil {
|
}
|
||||||
return fmt.Errorf("invalid URL")
|
u, err := url.Parse(s)
|
||||||
}
|
if err != nil {
|
||||||
if u.Scheme != "http" && u.Scheme != "https" {
|
return fmt.Errorf("invalid URL")
|
||||||
return fmt.Errorf("URL must start with http:// or https://")
|
}
|
||||||
}
|
if u.Scheme != "http" && u.Scheme != "https" {
|
||||||
if u.Host == "" {
|
return fmt.Errorf("URL must start with http:// or https://")
|
||||||
return fmt.Errorf("URL must include a host")
|
}
|
||||||
}
|
if u.Host == "" {
|
||||||
return nil
|
return fmt.Errorf("URL must include a host")
|
||||||
}),
|
}
|
||||||
huh.NewInput().Title("Check Interval (seconds)").
|
return nil
|
||||||
Placeholder("60").
|
}))
|
||||||
Value(&m.siteFormData.Interval).
|
case "ping", "dns":
|
||||||
Validate(func(s string) error {
|
setup = append(setup, huh.NewInput().Title("Hostname / IP").
|
||||||
if m.siteFormData.SiteType == "group" {
|
Placeholder("10.0.0.1").
|
||||||
return nil
|
Value(&d.Hostname))
|
||||||
}
|
case "port":
|
||||||
v, err := strconv.Atoi(s)
|
setup = append(setup,
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("must be a number")
|
|
||||||
}
|
|
||||||
if v < 5 {
|
|
||||||
return fmt.Errorf("minimum interval is 5 seconds")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
huh.NewSelect[string]().Title("Parent Group").
|
|
||||||
Options(groupOpts...).
|
|
||||||
Value(&m.siteFormData.GroupID),
|
|
||||||
huh.NewInput().Title("Hostname / IP").
|
huh.NewInput().Title("Hostname / IP").
|
||||||
Placeholder("10.0.0.1").
|
Placeholder("10.0.0.1").
|
||||||
Description("Target for ping/port/DNS monitors").
|
Value(&d.Hostname),
|
||||||
Value(&m.siteFormData.Hostname),
|
|
||||||
huh.NewInput().Title("Port").
|
huh.NewInput().Title("Port").
|
||||||
Placeholder("0").
|
Placeholder("443").
|
||||||
Description("Target port for TCP port monitors").
|
Value(&d.Port).
|
||||||
Value(&m.siteFormData.Port).
|
|
||||||
Validate(func(s string) error {
|
Validate(func(s string) error {
|
||||||
if m.siteFormData.SiteType != "port" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
v, err := strconv.Atoi(s)
|
v, err := strconv.Atoi(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("must be a number")
|
return fmt.Errorf("must be a number")
|
||||||
@@ -429,34 +432,20 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
|
|||||||
return fmt.Errorf("port must be 1-65535")
|
return fmt.Errorf("port must be 1-65535")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}),
|
}))
|
||||||
huh.NewInput().Title("Timeout (seconds)").
|
}
|
||||||
Placeholder("5").
|
|
||||||
Value(&m.siteFormData.Timeout).
|
groups := []*huh.Group{huh.NewGroup(setup...).Title("Monitor Setup")}
|
||||||
Validate(func(s string) error {
|
|
||||||
if m.siteFormData.SiteType == "group" {
|
if d.SiteType == "group" {
|
||||||
return nil
|
return groups
|
||||||
}
|
}
|
||||||
v, err := strconv.Atoi(s)
|
|
||||||
if err != nil {
|
// Page 2 — Configuration: type-specific options + shared defaults
|
||||||
return fmt.Errorf("must be a number")
|
var config []huh.Field
|
||||||
}
|
|
||||||
if v < 1 || v > 300 {
|
if d.SiteType == "http" {
|
||||||
return fmt.Errorf("timeout must be 1-300 seconds")
|
config = append(config,
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
huh.NewInput().Title("Description").
|
|
||||||
Placeholder("Optional description").
|
|
||||||
Value(&m.siteFormData.Description),
|
|
||||||
huh.NewInput().Title("Probe Regions").
|
|
||||||
Placeholder("us-east, eu-west (empty = all)").
|
|
||||||
Description("Comma-separated regions for distributed probing").
|
|
||||||
Value(&m.siteFormData.Regions),
|
|
||||||
).Title("Connection").WithHideFunc(func() bool {
|
|
||||||
return m.siteFormData.SiteType == "group"
|
|
||||||
}),
|
|
||||||
huh.NewGroup(
|
|
||||||
huh.NewSelect[string]().Title("HTTP Method").
|
huh.NewSelect[string]().Title("HTTP Method").
|
||||||
Options(
|
Options(
|
||||||
huh.NewOption("GET", "GET"),
|
huh.NewOption("GET", "GET"),
|
||||||
@@ -466,22 +455,75 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
|
|||||||
huh.NewOption("DELETE", "DELETE"),
|
huh.NewOption("DELETE", "DELETE"),
|
||||||
huh.NewOption("HEAD", "HEAD"),
|
huh.NewOption("HEAD", "HEAD"),
|
||||||
huh.NewOption("OPTIONS", "OPTIONS"),
|
huh.NewOption("OPTIONS", "OPTIONS"),
|
||||||
).Value(&m.siteFormData.Method),
|
).Value(&d.Method),
|
||||||
huh.NewInput().Title("Accepted Status Codes").
|
huh.NewInput().Title("Accepted Status Codes").
|
||||||
Placeholder("200-299").
|
Placeholder("200-299").
|
||||||
Description("Ranges (200-299) and singles (301) separated by commas").
|
Description("Ranges (200-299) and singles (301) separated by commas").
|
||||||
Value(&m.siteFormData.AcceptedCodes),
|
Value(&d.AcceptedCodes),
|
||||||
).Title("HTTP Settings").WithHideFunc(func() bool {
|
)
|
||||||
return m.siteFormData.SiteType != "http"
|
}
|
||||||
}),
|
|
||||||
huh.NewGroup(
|
config = append(config,
|
||||||
|
huh.NewInput().Title("Check Interval (seconds)").
|
||||||
|
Placeholder("60").
|
||||||
|
Value(&d.Interval).
|
||||||
|
Validate(func(s string) error {
|
||||||
|
v, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("must be a number")
|
||||||
|
}
|
||||||
|
if v < 5 {
|
||||||
|
return fmt.Errorf("minimum interval is 5 seconds")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
huh.NewInput().Title("Timeout (seconds)").
|
||||||
|
Placeholder("5").
|
||||||
|
Value(&d.Timeout).
|
||||||
|
Validate(func(s string) error {
|
||||||
|
v, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("must be a number")
|
||||||
|
}
|
||||||
|
if v < 1 || v > 300 {
|
||||||
|
return fmt.Errorf("timeout must be 1-300 seconds")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
huh.NewInput().Title("Max Retries Before Alert").
|
||||||
|
Placeholder("0").
|
||||||
|
Value(&d.Retries).
|
||||||
|
Validate(func(s string) error {
|
||||||
|
v, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("must be a number")
|
||||||
|
}
|
||||||
|
if v < 0 {
|
||||||
|
return fmt.Errorf("retries cannot be negative")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
huh.NewSelect[string]().Title("Parent Group").
|
||||||
|
Options(groupOpts...).
|
||||||
|
Value(&d.GroupID),
|
||||||
|
huh.NewInput().Title("Description").
|
||||||
|
Placeholder("Optional description").
|
||||||
|
Value(&d.Description),
|
||||||
|
huh.NewInput().Title("Probe Regions").
|
||||||
|
Placeholder("us-east, eu-west (empty = all)").
|
||||||
|
Description("Comma-separated regions for distributed probing").
|
||||||
|
Value(&d.Regions),
|
||||||
|
)
|
||||||
|
|
||||||
|
if d.SiteType == "http" {
|
||||||
|
config = append(config,
|
||||||
huh.NewConfirm().Title("Monitor SSL Certificate?").
|
huh.NewConfirm().Title("Monitor SSL Certificate?").
|
||||||
Value(&m.siteFormData.CheckSSL),
|
Value(&d.CheckSSL),
|
||||||
huh.NewInput().Title("SSL Warning Threshold (days)").
|
huh.NewInput().Title("SSL Warning Threshold (days)").
|
||||||
Placeholder("7").
|
Placeholder("7").
|
||||||
Value(&m.siteFormData.Threshold).
|
Value(&d.Threshold).
|
||||||
Validate(func(s string) error {
|
Validate(func(s string) error {
|
||||||
if !m.siteFormData.CheckSSL {
|
if !d.CheckSSL {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
v, err := strconv.Atoi(s)
|
v, err := strconv.Atoi(s)
|
||||||
@@ -493,30 +535,13 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
huh.NewInput().Title("Max Retries Before Alert").
|
|
||||||
Placeholder("0").
|
|
||||||
Value(&m.siteFormData.Retries).
|
|
||||||
Validate(func(s string) error {
|
|
||||||
if m.siteFormData.SiteType == "group" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
v, err := strconv.Atoi(s)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("must be a number")
|
|
||||||
}
|
|
||||||
if v < 0 {
|
|
||||||
return fmt.Errorf("retries cannot be negative")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
huh.NewConfirm().Title("Ignore TLS Errors?").
|
huh.NewConfirm().Title("Ignore TLS Errors?").
|
||||||
Value(&m.siteFormData.IgnoreTLS),
|
Value(&d.IgnoreTLS),
|
||||||
).Title("Advanced").WithHideFunc(func() bool {
|
)
|
||||||
return m.siteFormData.SiteType == "group"
|
}
|
||||||
}),
|
|
||||||
).WithTheme(m.theme.HuhTheme())
|
|
||||||
|
|
||||||
return m.huhForm.Init()
|
groups = append(groups, huh.NewGroup(config...).Title("Configuration"))
|
||||||
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) submitSiteForm() tea.Cmd {
|
func (m *Model) submitSiteForm() tea.Cmd {
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ type Model struct {
|
|||||||
|
|
||||||
huhForm *huh.Form
|
huhForm *huh.Form
|
||||||
siteFormData *siteFormData
|
siteFormData *siteFormData
|
||||||
|
lastSiteType string
|
||||||
alertFormData *alertFormData
|
alertFormData *alertFormData
|
||||||
userFormData *userFormData
|
userFormData *userFormData
|
||||||
maintFormData *maintFormData
|
maintFormData *maintFormData
|
||||||
|
|||||||
@@ -128,6 +128,13 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if f, ok := form.(*huh.Form); ok {
|
if f, ok := form.(*huh.Form); ok {
|
||||||
m.huhForm = f
|
m.huhForm = f
|
||||||
}
|
}
|
||||||
|
if m.state == stateFormSite && m.siteFormData != nil &&
|
||||||
|
m.siteFormData.SiteType != m.lastSiteType {
|
||||||
|
rebuildCmd := m.rebuildSiteForm()
|
||||||
|
// Advance to Type select — user just changed it.
|
||||||
|
skipName := m.huhForm.NextField()
|
||||||
|
return m, tea.Batch(rebuildCmd, skipName)
|
||||||
|
}
|
||||||
if m.huhForm.State == huh.StateCompleted {
|
if m.huhForm.State == huh.StateCompleted {
|
||||||
// The store write runs in the returned Cmd; its writeDoneMsg
|
// The store write runs in the returned Cmd; its writeDoneMsg
|
||||||
// triggers the tab-data reload once the row actually exists.
|
// triggers the tab-data reload once the row actually exists.
|
||||||
|
|||||||
Reference in New Issue
Block a user