mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
webui: re-render interface IP block in place after save
Surface confd-inferred values (DHCP option lists, etc.) on the curated Interfaces page: SaveIPv4/IPv6Settings re-render just the interface's IP block from the fresh candidate, swapped in place via outerHTML, so the row stays expanded. The DHCP foldout auto-expands after a save to reveal the inferred options. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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}}
|
||||
<h5 class="cfg-subsection-head">IPv4{{template "field-info" (index $d "ipv4-address")}}</h5>
|
||||
<form id="{{$ipv4Form}}" hx-post="/configure/interfaces/{{.Name}}/ipv4/settings" hx-swap="none">
|
||||
<div style="display:flex;gap:1.5rem;flex-wrap:wrap;margin-bottom:0.5rem">
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="forwarding" value="true"
|
||||
{{if and .IPv4 .IPv4.Forwarding}}checked{{end}}>
|
||||
Forwarding{{template "field-info" (index $d "ipv4-forwarding")}}
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="dhcp" value="true" data-fold-target="{{$ipv4Fold}}"
|
||||
{{if .DHCPv4Enabled}}checked{{end}}>
|
||||
DHCP client{{template "field-info" (index $d "ipv4-dhcp")}}
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="autoconf" value="true"
|
||||
{{if and .IPv4 .IPv4.Autoconf}}checked{{end}}>
|
||||
Link-Local (Zeroconf){{template "field-info" (index $d "ipv4-autoconf")}}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{/* 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. */}}
|
||||
<details id="{{$ipv4Fold}}" class="cfg-fold" {{if not .DHCPv4Enabled}}hidden{{end}}>
|
||||
<summary>DHCP Settings & Options</summary>
|
||||
<div class="cfg-fold-body">
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv4/dhcp/settings" hx-swap="none">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<th>Client ID</th>
|
||||
<td><input class="cfg-input cfg-input-sm" type="text" name="client-id"
|
||||
value="{{.IPv4.DHCP.ClientID}}" placeholder="default (MAC address)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>ARP check</th>
|
||||
<td><label><input type="checkbox" name="arping" value="true"
|
||||
{{if or (not .IPv4.DHCP.Arping) (deref .IPv4.DHCP.Arping)}}checked{{end}}> Probe for address collision before use</label></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Route preference</th>
|
||||
<td><input class="cfg-input cfg-input-sm" type="number" name="route-preference"
|
||||
{{if .IPv4.DHCP.RoutePreference}}value="{{deref .IPv4.DHCP.RoutePreference}}"{{end}}
|
||||
placeholder="5" min="0" max="255" style="width:6rem"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Save</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</div>
|
||||
</form>
|
||||
<h6 class="cfg-subsection-head" style="margin-top:0.75rem">DHCP Options</h6>
|
||||
{{if .IPv4.DHCP.Options}}
|
||||
<table class="data-table cfg-table" style="margin-bottom:0.5rem">
|
||||
<thead><tr><th>Option</th><th>Value</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .IPv4.DHCP.Options}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{if .Value}}{{.Value}}{{else if .Hex}}<span class="text-muted">hex:{{.Hex}}</span>{{else}}<span class="text-muted">—</span>{{end}}</td>
|
||||
<td style="text-align:right">
|
||||
<button type="button" class="btn-icon btn-icon-danger"
|
||||
hx-delete="/configure/interfaces/{{$r.Name}}/ipv4/dhcp/options/{{.ID}}"
|
||||
hx-confirm="Remove option {{.ID}}?"
|
||||
hx-target="closest tr" hx-swap="delete" title="Remove option">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
{{if $.DHCPv4Options}}
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv4/dhcp/options" hx-swap="none"
|
||||
class="user-add-inline">
|
||||
<select class="cfg-input cfg-input-sm" name="id">
|
||||
{{range $.DHCPv4Options}}<option value="{{.Value}}">{{.Label}}</option>{{end}}
|
||||
</select>
|
||||
<input class="cfg-input cfg-input-sm" type="text" name="value"
|
||||
placeholder="value (optional)" style="flex:2">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{{if and .IPv4 .IPv4.Address}}
|
||||
<table class="info-table" style="margin-bottom:0.5rem">
|
||||
{{range .IPv4.Address}}
|
||||
<tr>
|
||||
<td>{{.IP}}/{{.PrefixLength}}</td>
|
||||
<td>{{if .Origin}}<span class="text-muted">{{.Origin}}</span>{{end}}</td>
|
||||
<td class="cfg-reset-col">
|
||||
{{if or (eq .Origin "") (eq .Origin "static")}}
|
||||
<button type="button" class="btn-icon btn-icon-danger"
|
||||
hx-delete="/configure/interfaces/{{$r.Name}}/ipv4/{{.IP}}"
|
||||
hx-confirm="Remove {{.IP}}?"
|
||||
hx-swap="none" title="Remove address">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{else}}
|
||||
<p class="text-muted" style="font-size:0.85em;margin-bottom:0.5rem">No IPv4 addresses configured.</p>
|
||||
{{end}}
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv4" hx-swap="none" class="user-add-inline">
|
||||
<input class="cfg-input cfg-input-sm" type="text" name="ip"
|
||||
placeholder="192.168.1.1" style="flex:2">
|
||||
<span style="padding:0 0.25rem">/</span>
|
||||
<input class="cfg-input cfg-input-sm" type="number" name="prefix-length"
|
||||
placeholder="24" min="0" max="32" style="flex:0 0 5rem">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</form>
|
||||
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
<button form="{{$ipv4Form}}" type="submit" class="btn btn-primary btn-sm">Save IPv4 Settings</button>
|
||||
<span class="cfg-save-status" data-cfg-status-for="{{$ipv4Form}}"></span>
|
||||
</div>
|
||||
{{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}}
|
||||
<h5 class="cfg-subsection-head" style="margin-top:0.75rem">IPv6{{template "field-info" (index $d "ipv6-address")}}</h5>
|
||||
<form id="{{$ipv6Form}}" hx-post="/configure/interfaces/{{.Name}}/ipv6/settings" hx-swap="none">
|
||||
<div style="display:flex;gap:1.5rem;flex-wrap:wrap;margin-bottom:0.5rem">
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="forwarding" value="true"
|
||||
{{if and .IPv6 .IPv6.Forwarding}}checked{{end}}>
|
||||
Forwarding{{template "field-info" (index $d "ipv6-forwarding")}}
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="dhcp" value="true" data-fold-target="{{$ipv6Fold}}"
|
||||
{{if .DHCPv6Enabled}}checked{{end}}>
|
||||
DHCPv6 client{{template "field-info" (index $d "ipv6-dhcp")}}
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="slaac" value="true"
|
||||
{{if and .IPv6 .IPv6.SLAACv6}}checked{{end}}>
|
||||
SLAAC{{template "field-info" (index $d "ipv6-slaac")}}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<details id="{{$ipv6Fold}}" class="cfg-fold" {{if not .DHCPv6Enabled}}hidden{{end}}>
|
||||
<summary>DHCPv6 Settings & Options</summary>
|
||||
<div class="cfg-fold-body">
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv6/dhcp/settings" hx-swap="none">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<th>DUID</th>
|
||||
<td><input class="cfg-input cfg-input-sm cfg-input-mono" type="text" name="duid"
|
||||
value="{{.IPv6.DHCPv6.DUID}}" placeholder="default (auto-generated)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Information only</th>
|
||||
<td><label><input type="checkbox" name="information-only" value="true"
|
||||
{{if and .IPv6.DHCPv6.InformationOnly (deref .IPv6.DHCPv6.InformationOnly)}}checked{{end}}> Request config only, no address (requires SLAAC)</label></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Route preference</th>
|
||||
<td><input class="cfg-input cfg-input-sm" type="number" name="route-preference"
|
||||
{{if .IPv6.DHCPv6.RoutePreference}}value="{{deref .IPv6.DHCPv6.RoutePreference}}"{{end}}
|
||||
placeholder="5" min="0" max="255" style="width:6rem"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Save</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</div>
|
||||
</form>
|
||||
<h6 class="cfg-subsection-head" style="margin-top:0.75rem">DHCPv6 Options</h6>
|
||||
{{if .IPv6.DHCPv6.Options}}
|
||||
<table class="data-table cfg-table" style="margin-bottom:0.5rem">
|
||||
<thead><tr><th>Option</th><th>Value</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .IPv6.DHCPv6.Options}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{if .Value}}{{.Value}}{{else if .Hex}}<span class="text-muted">hex:{{.Hex}}</span>{{else}}<span class="text-muted">—</span>{{end}}</td>
|
||||
<td style="text-align:right">
|
||||
<button type="button" class="btn-icon btn-icon-danger"
|
||||
hx-delete="/configure/interfaces/{{$r.Name}}/ipv6/dhcp/options/{{.ID}}"
|
||||
hx-confirm="Remove option {{.ID}}?"
|
||||
hx-target="closest tr" hx-swap="delete" title="Remove option">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
{{if $.DHCPv6Options}}
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv6/dhcp/options" hx-swap="none"
|
||||
class="user-add-inline">
|
||||
<select class="cfg-input cfg-input-sm" name="id">
|
||||
{{range $.DHCPv6Options}}<option value="{{.Value}}">{{.Label}}</option>{{end}}
|
||||
</select>
|
||||
<input class="cfg-input cfg-input-sm" type="text" name="value"
|
||||
placeholder="value (optional)" style="flex:2">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{{if and .IPv6 .IPv6.Address}}
|
||||
<table class="info-table" style="margin-bottom:0.5rem">
|
||||
{{range .IPv6.Address}}
|
||||
<tr>
|
||||
<td class="cfg-input-mono">{{.IP}}/{{.PrefixLength}}</td>
|
||||
<td>{{if .Origin}}<span class="text-muted">{{.Origin}}</span>{{end}}</td>
|
||||
<td class="cfg-reset-col">
|
||||
{{if or (eq .Origin "") (eq .Origin "static")}}
|
||||
<button type="button" class="btn-icon btn-icon-danger"
|
||||
hx-delete="/configure/interfaces/{{$r.Name}}/ipv6/{{.IP}}"
|
||||
hx-confirm="Remove {{.IP}}?"
|
||||
hx-swap="none" title="Remove address">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{else}}
|
||||
<p class="text-muted" style="font-size:0.85em;margin-bottom:0.5rem">No IPv6 addresses configured.</p>
|
||||
{{end}}
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv6" hx-swap="none" class="user-add-inline">
|
||||
<input class="cfg-input" type="text" name="ip"
|
||||
placeholder="2001:db8::1" style="flex:2">
|
||||
<span style="padding:0 0.25rem">/</span>
|
||||
<input class="cfg-input cfg-input-sm" type="number" name="prefix-length"
|
||||
placeholder="64" min="0" max="128" style="flex:0 0 5rem">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</form>
|
||||
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
<button form="{{$ipv6Form}}" type="submit" class="btn btn-primary btn-sm">Save IPv6 Settings</button>
|
||||
<span class="cfg-save-status" data-cfg-status-for="{{$ipv6Form}}"></span>
|
||||
</div>
|
||||
{{template "iface-ipv6-block" .}}
|
||||
</div>
|
||||
{{end}}{{/* HasIP */}}
|
||||
|
||||
@@ -1839,3 +1591,264 @@
|
||||
|
||||
{{end}}
|
||||
|
||||
|
||||
|
||||
{{define "iface-ipv4-block"}}
|
||||
<div id="ipv4-block-{{.Name}}" class="ipv4-block">
|
||||
{{$ipv4Form := printf "ipv4-form-%s" $.Name}}
|
||||
{{$ipv4Fold := printf "ipv4-dhcp-%s" $.Name}}
|
||||
<h5 class="cfg-subsection-head">IPv4{{template "field-info" (index $.Desc "ipv4-address")}}</h5>
|
||||
<form id="{{$ipv4Form}}" hx-post="/configure/interfaces/{{.Name}}/ipv4/settings" hx-target="#ipv4-block-{{.Name}}" hx-swap="outerHTML">
|
||||
<div style="display:flex;gap:1.5rem;flex-wrap:wrap;margin-bottom:0.5rem">
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="forwarding" value="true"
|
||||
{{if and .IPv4 .IPv4.Forwarding}}checked{{end}}>
|
||||
Forwarding{{template "field-info" (index $.Desc "ipv4-forwarding")}}
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="dhcp" value="true" data-fold-target="{{$ipv4Fold}}"
|
||||
{{if .DHCPv4Enabled}}checked{{end}}>
|
||||
DHCP client{{template "field-info" (index $.Desc "ipv4-dhcp")}}
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="autoconf" value="true"
|
||||
{{if and .IPv4 .IPv4.Autoconf}}checked{{end}}>
|
||||
Link-Local (Zeroconf){{template "field-info" (index $.Desc "ipv4-autoconf")}}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{/* 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. */}}
|
||||
<details id="{{$ipv4Fold}}" class="cfg-fold" {{if .JustSaved}}open{{end}} {{if not .DHCPv4Enabled}}hidden{{end}}>
|
||||
<summary>DHCP Settings & Options</summary>
|
||||
<div class="cfg-fold-body">
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv4/dhcp/settings" hx-swap="none">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<th>Client ID</th>
|
||||
<td><input class="cfg-input cfg-input-sm" type="text" name="client-id"
|
||||
value="{{.IPv4.DHCP.ClientID}}" placeholder="default (MAC address)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>ARP check</th>
|
||||
<td><label><input type="checkbox" name="arping" value="true"
|
||||
{{if or (not .IPv4.DHCP.Arping) (deref .IPv4.DHCP.Arping)}}checked{{end}}> Probe for address collision before use</label></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Route preference</th>
|
||||
<td><input class="cfg-input cfg-input-sm" type="number" name="route-preference"
|
||||
{{if .IPv4.DHCP.RoutePreference}}value="{{deref .IPv4.DHCP.RoutePreference}}"{{end}}
|
||||
placeholder="5" min="0" max="255" style="width:6rem"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Save</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</div>
|
||||
</form>
|
||||
<h6 class="cfg-subsection-head" style="margin-top:0.75rem">DHCP Options</h6>
|
||||
{{if .IPv4.DHCP.Options}}
|
||||
<table class="data-table cfg-table" style="margin-bottom:0.5rem">
|
||||
<thead><tr><th>Option</th><th>Value</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .IPv4.DHCP.Options}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{if .Value}}{{.Value}}{{else if .Hex}}<span class="text-muted">hex:{{.Hex}}</span>{{else}}<span class="text-muted">—</span>{{end}}</td>
|
||||
<td style="text-align:right">
|
||||
<button type="button" class="btn-icon btn-icon-danger"
|
||||
hx-delete="/configure/interfaces/{{$.Name}}/ipv4/dhcp/options/{{.ID}}"
|
||||
hx-confirm="Remove option {{.ID}}?"
|
||||
hx-target="closest tr" hx-swap="delete" title="Remove option">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
{{if $.DHCPv4Options}}
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv4/dhcp/options" hx-swap="none"
|
||||
class="user-add-inline">
|
||||
<select class="cfg-input cfg-input-sm" name="id">
|
||||
{{range $.DHCPv4Options}}<option value="{{.Value}}">{{.Label}}</option>{{end}}
|
||||
</select>
|
||||
<input class="cfg-input cfg-input-sm" type="text" name="value"
|
||||
placeholder="value (optional)" style="flex:2">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{{if and .IPv4 .IPv4.Address}}
|
||||
<table class="info-table" style="margin-bottom:0.5rem">
|
||||
{{range .IPv4.Address}}
|
||||
<tr>
|
||||
<td>{{.IP}}/{{.PrefixLength}}</td>
|
||||
<td>{{if .Origin}}<span class="text-muted">{{.Origin}}</span>{{end}}</td>
|
||||
<td class="cfg-reset-col">
|
||||
{{if or (eq .Origin "") (eq .Origin "static")}}
|
||||
<button type="button" class="btn-icon btn-icon-danger"
|
||||
hx-delete="/configure/interfaces/{{$.Name}}/ipv4/{{.IP}}"
|
||||
hx-confirm="Remove {{.IP}}?"
|
||||
hx-target="#ipv4-block-{{$.Name}}" hx-swap="outerHTML" title="Remove address">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{else}}
|
||||
<p class="text-muted" style="font-size:0.85em;margin-bottom:0.5rem">No IPv4 addresses configured.</p>
|
||||
{{end}}
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv4" hx-target="#ipv4-block-{{.Name}}" hx-swap="outerHTML" class="user-add-inline">
|
||||
<input class="cfg-input cfg-input-sm" type="text" name="ip"
|
||||
placeholder="192.168.1.1" style="flex:2">
|
||||
<span style="padding:0 0.25rem">/</span>
|
||||
<input class="cfg-input cfg-input-sm" type="number" name="prefix-length"
|
||||
placeholder="24" min="0" max="32" style="flex:0 0 5rem">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</form>
|
||||
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
<button form="{{$ipv4Form}}" type="submit" class="btn btn-primary btn-sm">Save IPv4 Settings</button>
|
||||
<span class="cfg-save-status" data-cfg-status-for="{{$ipv4Form}}"></span>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "iface-ipv6-block"}}
|
||||
<div id="ipv6-block-{{.Name}}" class="ipv6-block">
|
||||
{{$ipv6Form := printf "ipv6-form-%s" $.Name}}
|
||||
{{$ipv6Fold := printf "ipv6-dhcp-%s" $.Name}}
|
||||
<h5 class="cfg-subsection-head" style="margin-top:0.75rem">IPv6{{template "field-info" (index $.Desc "ipv6-address")}}</h5>
|
||||
<form id="{{$ipv6Form}}" hx-post="/configure/interfaces/{{.Name}}/ipv6/settings" hx-target="#ipv6-block-{{.Name}}" hx-swap="outerHTML">
|
||||
<div style="display:flex;gap:1.5rem;flex-wrap:wrap;margin-bottom:0.5rem">
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="forwarding" value="true"
|
||||
{{if and .IPv6 .IPv6.Forwarding}}checked{{end}}>
|
||||
Forwarding{{template "field-info" (index $.Desc "ipv6-forwarding")}}
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="dhcp" value="true" data-fold-target="{{$ipv6Fold}}"
|
||||
{{if .DHCPv6Enabled}}checked{{end}}>
|
||||
DHCPv6 client{{template "field-info" (index $.Desc "ipv6-dhcp")}}
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="slaac" value="true"
|
||||
{{if and .IPv6 .IPv6.SLAACv6}}checked{{end}}>
|
||||
SLAAC{{template "field-info" (index $.Desc "ipv6-slaac")}}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<details id="{{$ipv6Fold}}" class="cfg-fold" {{if .JustSaved}}open{{end}} {{if not .DHCPv6Enabled}}hidden{{end}}>
|
||||
<summary>DHCPv6 Settings & Options</summary>
|
||||
<div class="cfg-fold-body">
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv6/dhcp/settings" hx-swap="none">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<th>DUID</th>
|
||||
<td><input class="cfg-input cfg-input-sm cfg-input-mono" type="text" name="duid"
|
||||
value="{{.IPv6.DHCPv6.DUID}}" placeholder="default (auto-generated)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Information only</th>
|
||||
<td><label><input type="checkbox" name="information-only" value="true"
|
||||
{{if and .IPv6.DHCPv6.InformationOnly (deref .IPv6.DHCPv6.InformationOnly)}}checked{{end}}> Request config only, no address (requires SLAAC)</label></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Route preference</th>
|
||||
<td><input class="cfg-input cfg-input-sm" type="number" name="route-preference"
|
||||
{{if .IPv6.DHCPv6.RoutePreference}}value="{{deref .IPv6.DHCPv6.RoutePreference}}"{{end}}
|
||||
placeholder="5" min="0" max="255" style="width:6rem"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Save</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</div>
|
||||
</form>
|
||||
<h6 class="cfg-subsection-head" style="margin-top:0.75rem">DHCPv6 Options</h6>
|
||||
{{if .IPv6.DHCPv6.Options}}
|
||||
<table class="data-table cfg-table" style="margin-bottom:0.5rem">
|
||||
<thead><tr><th>Option</th><th>Value</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .IPv6.DHCPv6.Options}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{if .Value}}{{.Value}}{{else if .Hex}}<span class="text-muted">hex:{{.Hex}}</span>{{else}}<span class="text-muted">—</span>{{end}}</td>
|
||||
<td style="text-align:right">
|
||||
<button type="button" class="btn-icon btn-icon-danger"
|
||||
hx-delete="/configure/interfaces/{{$.Name}}/ipv6/dhcp/options/{{.ID}}"
|
||||
hx-confirm="Remove option {{.ID}}?"
|
||||
hx-target="closest tr" hx-swap="delete" title="Remove option">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
{{if $.DHCPv6Options}}
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv6/dhcp/options" hx-swap="none"
|
||||
class="user-add-inline">
|
||||
<select class="cfg-input cfg-input-sm" name="id">
|
||||
{{range $.DHCPv6Options}}<option value="{{.Value}}">{{.Label}}</option>{{end}}
|
||||
</select>
|
||||
<input class="cfg-input cfg-input-sm" type="text" name="value"
|
||||
placeholder="value (optional)" style="flex:2">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{{if and .IPv6 .IPv6.Address}}
|
||||
<table class="info-table" style="margin-bottom:0.5rem">
|
||||
{{range .IPv6.Address}}
|
||||
<tr>
|
||||
<td class="cfg-input-mono">{{.IP}}/{{.PrefixLength}}</td>
|
||||
<td>{{if .Origin}}<span class="text-muted">{{.Origin}}</span>{{end}}</td>
|
||||
<td class="cfg-reset-col">
|
||||
{{if or (eq .Origin "") (eq .Origin "static")}}
|
||||
<button type="button" class="btn-icon btn-icon-danger"
|
||||
hx-delete="/configure/interfaces/{{$.Name}}/ipv6/{{.IP}}"
|
||||
hx-confirm="Remove {{.IP}}?"
|
||||
hx-target="#ipv6-block-{{$.Name}}" hx-swap="outerHTML" title="Remove address">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{else}}
|
||||
<p class="text-muted" style="font-size:0.85em;margin-bottom:0.5rem">No IPv6 addresses configured.</p>
|
||||
{{end}}
|
||||
<form hx-post="/configure/interfaces/{{.Name}}/ipv6" hx-target="#ipv6-block-{{.Name}}" hx-swap="outerHTML" class="user-add-inline">
|
||||
<input class="cfg-input" type="text" name="ip"
|
||||
placeholder="2001:db8::1" style="flex:2">
|
||||
<span style="padding:0 0.25rem">/</span>
|
||||
<input class="cfg-input cfg-input-sm" type="number" name="prefix-length"
|
||||
placeholder="64" min="0" max="128" style="flex:0 0 5rem">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</form>
|
||||
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
<button form="{{$ipv6Form}}" type="submit" class="btn btn-primary btn-sm">Save IPv6 Settings</button>
|
||||
<span class="cfg-save-status" data-cfg-status-for="{{$ipv6Form}}"></span>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user