diff --git a/src/webui/internal/handlers/configure_interfaces.go b/src/webui/internal/handlers/configure_interfaces.go index b59fd1cb..0389988c 100644 --- a/src/webui/internal/handlers/configure_interfaces.go +++ b/src/webui/internal/handlers/configure_interfaces.go @@ -128,6 +128,18 @@ type cfgIfaceRow struct { // "auto-injected placeholder" apart. DHCPv4Enabled bool DHCPv6Enabled bool + + // Page-level bits copied onto each row so the per-interface IPv4/IPv6 + // blocks can render as standalone fragments — both inline on the full + // page and on their own when a save handler re-renders just one block + // (to surface confd-inferred values without collapsing the page). + Desc map[string]string + DHCPv4Options []schema.IdentityOption + DHCPv6Options []schema.IdentityOption + // JustSaved marks a post-save re-render (renderIPBlock) so the DHCP + // foldout auto-expands to reveal the freshly-inferred options. False + // on the normal page render, so foldouts stay collapsed there. + JustSaved bool } // ifaceRadioMirror is the subset of wifi-radio fields we expose on the @@ -469,6 +481,13 @@ func (h *ConfigureInterfacesHandler) Overview(w http.ResponseWriter, r *http.Req sort.Strings(data.WizardAvailableRadios) data.Interfaces = h.buildRows(ifaces, operWrap.Interfaces.Interface) + // Copy page-level descriptions + DHCP option enums onto each row so the + // IPv4/IPv6 blocks render standalone (see cfgIfaceRow.Desc). + for i := range data.Interfaces { + data.Interfaces[i].Desc = data.Desc + data.Interfaces[i].DHCPv4Options = data.DHCPv4Options + data.Interfaces[i].DHCPv6Options = data.DHCPv6Options + } // Populate the mirrored radio editor for WiFi interface rows from // the already-fetched candidate hardware tree (no extra fetch). radios := indexWifiRadios(hwCand.Hardware.Component) @@ -1166,25 +1185,25 @@ func (h *ConfigureInterfacesHandler) SaveGeneral(w http.ResponseWriter, r *http. // AddIPv4 adds an IPv4 address to an interface. // POST /configure/interfaces/{name}/ipv4 func (h *ConfigureInterfacesHandler) AddIPv4(w http.ResponseWriter, r *http.Request) { - h.addAddr(w, r, "ipv4") + h.addAddr(w, r, "ipv4", "IPv4", "iface-ipv4-block") } // DeleteIPv4 removes an IPv4 address from an interface. // DELETE /configure/interfaces/{name}/ipv4/{ip} func (h *ConfigureInterfacesHandler) DeleteIPv4(w http.ResponseWriter, r *http.Request) { - h.deleteAddr(w, r, "ipv4") + h.deleteAddr(w, r, "ipv4", "IPv4", "iface-ipv4-block") } // AddIPv6 adds an IPv6 address to an interface. // POST /configure/interfaces/{name}/ipv6 func (h *ConfigureInterfacesHandler) AddIPv6(w http.ResponseWriter, r *http.Request) { - h.addAddr(w, r, "ipv6") + h.addAddr(w, r, "ipv6", "IPv6", "iface-ipv6-block") } // DeleteIPv6 removes an IPv6 address from an interface. // DELETE /configure/interfaces/{name}/ipv6/{ip} func (h *ConfigureInterfacesHandler) DeleteIPv6(w http.ResponseWriter, r *http.Request) { - h.deleteAddr(w, r, "ipv6") + h.deleteAddr(w, r, "ipv6", "IPv6", "iface-ipv6-block") } // SaveBridgePort assigns or updates an interface's bridge membership. @@ -2103,7 +2122,7 @@ func portCandidatesFor(masterName string, ifaces []ifaceJSON, memberOf map[strin return out } -func (h *ConfigureInterfacesHandler) addAddr(w http.ResponseWriter, r *http.Request, family string) { +func (h *ConfigureInterfacesHandler) addAddr(w http.ResponseWriter, r *http.Request, family, famCap, frag string) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return @@ -2130,10 +2149,12 @@ func (h *ConfigureInterfacesHandler) addAddr(w http.ResponseWriter, r *http.Requ renderSaveError(w, err) return } - renderSavedRedirect(w, "Address added", "/configure/interfaces") + // Re-render the IP block in place so the new address appears without + // collapsing the interface foldout — ready to add another straight away. + h.renderIPBlock(w, r, name, famCap, frag) } -func (h *ConfigureInterfacesHandler) deleteAddr(w http.ResponseWriter, r *http.Request, family string) { +func (h *ConfigureInterfacesHandler) deleteAddr(w http.ResponseWriter, r *http.Request, family, famCap, frag string) { name := r.PathValue("name") ip := r.PathValue("ip") path := ifacePath(name) + "/ietf-ip:" + family + "/address=" + restconf.EscapeKey(ip) @@ -2142,7 +2163,7 @@ func (h *ConfigureInterfacesHandler) deleteAddr(w http.ResponseWriter, r *http.R renderSaveError(w, err) return } - renderSavedRedirect(w, "Address removed", "/configure/interfaces") + h.renderIPBlock(w, r, name, famCap, frag) } // SaveIPv4Settings PATCHes the per-interface IPv4 group settings — forwarding @@ -2151,7 +2172,7 @@ func (h *ConfigureInterfacesHandler) deleteAddr(w http.ResponseWriter, r *http.R // PUT (enable) or DELETE (disable) per checkbox state; forwarding is PATCHed. // POST /configure/interfaces/{name}/ipv4/settings func (h *ConfigureInterfacesHandler) SaveIPv4Settings(w http.ResponseWriter, r *http.Request) { - h.saveIPSettings(w, r, "ietf-ip:ipv4", "IPv4", map[string]string{ + h.saveIPSettings(w, r, "ietf-ip:ipv4", "IPv4", "iface-ipv4-block", map[string]string{ "dhcp": "infix-dhcp-client:dhcp", "autoconf": "infix-ip:autoconf", }) @@ -2161,7 +2182,7 @@ func (h *ConfigureInterfacesHandler) SaveIPv4Settings(w http.ResponseWriter, r * // the standard ietf-ip "autoconf" container; DHCPv6 is the Infix augment. // POST /configure/interfaces/{name}/ipv6/settings func (h *ConfigureInterfacesHandler) SaveIPv6Settings(w http.ResponseWriter, r *http.Request) { - h.saveIPSettings(w, r, "ietf-ip:ipv6", "IPv6", map[string]string{ + h.saveIPSettings(w, r, "ietf-ip:ipv6", "IPv6", "iface-ipv6-block", map[string]string{ "dhcp": "infix-dhcpv6-client:dhcp", "slaac": "autoconf", }) @@ -2171,7 +2192,7 @@ func (h *ConfigureInterfacesHandler) SaveIPv6Settings(w http.ResponseWriter, r * // presenceMap maps form-field names (e.g. "dhcp") to their YANG presence // container key (e.g. "infix-dhcp-client:dhcp"); each one is PUT when checked // and DELETEd otherwise. Forwarding is always PATCHed. -func (h *ConfigureInterfacesHandler) saveIPSettings(w http.ResponseWriter, r *http.Request, container, family string, presenceMap map[string]string) { +func (h *ConfigureInterfacesHandler) saveIPSettings(w http.ResponseWriter, r *http.Request, container, family, fragName string, presenceMap map[string]string) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return @@ -2220,9 +2241,63 @@ func (h *ConfigureInterfacesHandler) saveIPSettings(w http.ResponseWriter, r *ht } } + // Re-render just this interface's IP block from the fresh candidate so + // confd-inferred values (the DHCP option list, etc.) appear in place, + // without collapsing the page. fragName empty → fall back to a toast + // (e.g. families whose block fragment isn't wired up yet). + if fragName != "" { + h.renderIPBlock(w, r, name, family, fragName) + return + } renderSaved(w, family+" settings saved") } +// renderIPBlock re-renders one interface's IPv4/IPv6 block fragment from the +// fresh candidate. Falls back to a plain saved-toast if the interface or +// schema can't be read, so a save is never reported as failed just because +// the in-place refresh couldn't be built. +func (h *ConfigureInterfacesHandler) renderIPBlock(w http.ResponseWriter, r *http.Request, name, family, fragName string) { + ifaces, err := h.fetchAllInterfaces(r.Context()) + if err != nil { + renderSaved(w, family+" settings saved") + return + } + rows := h.buildRows(ifaces, nil) + var row *cfgIfaceRow + for i := range rows { + if rows[i].Name == name { + row = &rows[i] + break + } + } + if row == nil { + renderSaved(w, family+" settings saved") + return + } + if mgr := h.Schema.Manager(); mgr != nil { + ifPath := "/ietf-interfaces:interfaces/interface" + ip4, ip6 := "/ietf-ip:ipv4", "/ietf-ip:ipv6" + row.DHCPv4Options = schema.OptionsFor(mgr, ifPath+ip4+"/infix-dhcp-client:dhcp/option/id") + row.DHCPv6Options = schema.OptionsFor(mgr, ifPath+ip6+"/infix-dhcpv6-client:dhcp/option/id") + row.Desc = map[string]string{ + "ipv4-address": schema.DescriptionOf(mgr, ifPath+ip4+"/address/ip"), + "ipv4-dhcp": schema.DescriptionOf(mgr, ifPath+ip4+"/infix-dhcp-client:dhcp"), + "ipv4-autoconf": schema.DescriptionOf(mgr, ifPath+ip4+"/infix-ip:autoconf"), + "ipv4-forwarding": schema.DescriptionOf(mgr, ifPath+ip4+"/forwarding"), + "ipv6-address": schema.DescriptionOf(mgr, ifPath+ip6+"/address/ip"), + "ipv6-dhcp": schema.DescriptionOf(mgr, ifPath+ip6+"/infix-dhcpv6-client:dhcp"), + "ipv6-slaac": schema.DescriptionOf(mgr, ifPath+ip6+"/autoconf"), + "ipv6-forwarding": schema.DescriptionOf(mgr, ifPath+ip6+"/forwarding"), + } + } + row.JustSaved = true // expand the DHCP foldout to reveal inferred options + // Keep the cfgSaved toast behaviour even though we swap the block. + w.Header().Set("HX-Trigger", `{"cfgSaved":"`+family+` settings saved"}`) + if err := h.Template.ExecuteTemplate(w, fragName, row); err != nil { + log.Printf("configure interfaces %s: render %s: %v", name, fragName, err) + } +} + func (h *ConfigureInterfacesHandler) SaveIPv4DHCPSettings(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) diff --git a/src/webui/templates/pages/configure-interfaces.html b/src/webui/templates/pages/configure-interfaces.html index 250f8c17..841cb0dc 100644 --- a/src/webui/templates/pages/configure-interfaces.html +++ b/src/webui/templates/pages/configure-interfaces.html @@ -138,259 +138,11 @@ row visually compose one IPv4 group; the Save button at the bottom is bound to the toggles form via the HTML5 `form` attribute, so users see the whole group before clicking Save. */}} - {{$ipv4Form := printf "ipv4-form-%s" $r.Name}} - {{$ipv4Fold := printf "ipv4-dhcp-%s" $r.Name}} -
IPv4{{template "field-info" (index $d "ipv4-address")}}
-
-
- - - -
-
- - {{/* DHCPv4 foldout — always in the DOM, hidden when DHCP is off. - The data-fold-target hook on the DHCP checkbox toggles - this without waiting for a server round-trip so the - foldout is discoverable before clicking Save. */}} -
- DHCP Settings & Options -
-
- - - - - - - - - - - - - -
Client ID
ARP check
Route preference
- -
-
DHCP Options
- {{if .IPv4.DHCP.Options}} - - - - {{range .IPv4.DHCP.Options}} - - - - - - {{end}} - -
OptionValue
{{.ID}}{{if .Value}}{{.Value}}{{else if .Hex}}hex:{{.Hex}}{{else}}{{end}} - -
- {{end}} - {{if $.DHCPv4Options}} -
- - - - -
- {{end}} -
-
- - {{if and .IPv4 .IPv4.Address}} - - {{range .IPv4.Address}} - - - - - - {{end}} -
{{.IP}}/{{.PrefixLength}}{{if .Origin}}{{.Origin}}{{end}} - {{if or (eq .Origin "") (eq .Origin "static")}} - - {{end}} -
- {{else}} -

No IPv4 addresses configured.

- {{end}} -
- - / - - - -
- - + {{template "iface-ipv4-block" .}} {{/* IPv6 — mirrors the IPv4 form structure above. */}} {{/* IPv6 — mirrors the IPv4 layout above. */}} - {{$ipv6Form := printf "ipv6-form-%s" $r.Name}} - {{$ipv6Fold := printf "ipv6-dhcp-%s" $r.Name}} -
IPv6{{template "field-info" (index $d "ipv6-address")}}
-
-
- - - -
-
- -
- DHCPv6 Settings & Options -
-
- - - - - - - - - - - - - -
DUID
Information only
Route preference
- -
-
DHCPv6 Options
- {{if .IPv6.DHCPv6.Options}} - - - - {{range .IPv6.DHCPv6.Options}} - - - - - - {{end}} - -
OptionValue
{{.ID}}{{if .Value}}{{.Value}}{{else if .Hex}}hex:{{.Hex}}{{else}}{{end}} - -
- {{end}} - {{if $.DHCPv6Options}} -
- - - - -
- {{end}} -
-
- - {{if and .IPv6 .IPv6.Address}} - - {{range .IPv6.Address}} - - - - - - {{end}} -
{{.IP}}/{{.PrefixLength}}{{if .Origin}}{{.Origin}}{{end}} - {{if or (eq .Origin "") (eq .Origin "static")}} - - {{end}} -
- {{else}} -

No IPv6 addresses configured.

- {{end}} -
- - / - - - -
- - + {{template "iface-ipv6-block" .}} {{end}}{{/* HasIP */}} @@ -1839,3 +1591,264 @@ {{end}} + + +{{define "iface-ipv4-block"}} +
+ {{$ipv4Form := printf "ipv4-form-%s" $.Name}} + {{$ipv4Fold := printf "ipv4-dhcp-%s" $.Name}} +
IPv4{{template "field-info" (index $.Desc "ipv4-address")}}
+
+
+ + + +
+
+ + {{/* DHCPv4 foldout — always in the DOM, hidden when DHCP is off. + The data-fold-target hook on the DHCP checkbox toggles + this without waiting for a server round-trip so the + foldout is discoverable before clicking Save. */}} +
+ DHCP Settings & Options +
+
+ + + + + + + + + + + + + +
Client ID
ARP check
Route preference
+ +
+
DHCP Options
+ {{if .IPv4.DHCP.Options}} + + + + {{range .IPv4.DHCP.Options}} + + + + + + {{end}} + +
OptionValue
{{.ID}}{{if .Value}}{{.Value}}{{else if .Hex}}hex:{{.Hex}}{{else}}{{end}} + +
+ {{end}} + {{if $.DHCPv4Options}} +
+ + + + +
+ {{end}} +
+
+ + {{if and .IPv4 .IPv4.Address}} + + {{range .IPv4.Address}} + + + + + + {{end}} +
{{.IP}}/{{.PrefixLength}}{{if .Origin}}{{.Origin}}{{end}} + {{if or (eq .Origin "") (eq .Origin "static")}} + + {{end}} +
+ {{else}} +

No IPv4 addresses configured.

+ {{end}} +
+ + / + + + +
+ + +
+{{end}} + +{{define "iface-ipv6-block"}} +
+ {{$ipv6Form := printf "ipv6-form-%s" $.Name}} + {{$ipv6Fold := printf "ipv6-dhcp-%s" $.Name}} +
IPv6{{template "field-info" (index $.Desc "ipv6-address")}}
+
+
+ + + +
+
+ +
+ DHCPv6 Settings & Options +
+
+ + + + + + + + + + + + + +
DUID
Information only
Route preference
+ +
+
DHCPv6 Options
+ {{if .IPv6.DHCPv6.Options}} + + + + {{range .IPv6.DHCPv6.Options}} + + + + + + {{end}} + +
OptionValue
{{.ID}}{{if .Value}}{{.Value}}{{else if .Hex}}hex:{{.Hex}}{{else}}{{end}} + +
+ {{end}} + {{if $.DHCPv6Options}} +
+ + + + +
+ {{end}} +
+
+ + {{if and .IPv6 .IPv6.Address}} + + {{range .IPv6.Address}} + + + + + + {{end}} +
{{.IP}}/{{.PrefixLength}}{{if .Origin}}{{.Origin}}{{end}} + {{if or (eq .Origin "") (eq .Origin "static")}} + + {{end}} +
+ {{else}} +

No IPv6 addresses configured.

+ {{end}} +
+ + / + + + +
+ + +
+{{end}} \ No newline at end of file