mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
#11625: Employ HTMX form rendering for device & VM interfaces
This commit is contained in:
parent
368e774ceb
commit
c84f0de8f8
@ -5,7 +5,7 @@ from django import forms
|
|||||||
from core.models import *
|
from core.models import *
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from utilities.forms import CommentField
|
from utilities.forms import CommentField, get_field_value
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'DataSourceForm',
|
'DataSourceForm',
|
||||||
@ -44,7 +44,7 @@ class DataSourceForm(NetBoxModelForm):
|
|||||||
]
|
]
|
||||||
if self.backend_fields:
|
if self.backend_fields:
|
||||||
fieldsets.append(
|
fieldsets.append(
|
||||||
('Backend', self.backend_fields)
|
('Backend Parameters', self.backend_fields)
|
||||||
)
|
)
|
||||||
|
|
||||||
return fieldsets
|
return fieldsets
|
||||||
@ -52,16 +52,11 @@ class DataSourceForm(NetBoxModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
backend_classes = registry['data_backends']
|
# Determine the selected backend type
|
||||||
|
backend_type = get_field_value(self, 'type')
|
||||||
if self.is_bound and self.data.get('type') in backend_classes:
|
backend = registry['data_backends'].get(backend_type)
|
||||||
type_ = self.data['type']
|
|
||||||
elif self.initial and self.initial.get('type') in backend_classes:
|
|
||||||
type_ = self.initial['type']
|
|
||||||
else:
|
|
||||||
type_ = self.fields['type'].initial
|
|
||||||
backend = backend_classes.get(type_)
|
|
||||||
|
|
||||||
|
# Add backend-specific form fields
|
||||||
self.backend_fields = []
|
self.backend_fields = []
|
||||||
for name, form_field in backend.parameters.items():
|
for name, form_field in backend.parameters.items():
|
||||||
field_name = f'backend_{name}'
|
field_name = f'backend_{name}'
|
||||||
|
@ -3,6 +3,7 @@ from django.utils.translation import gettext as _
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
|
from utilities.forms.utils import get_field_value
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'InterfaceCommonForm',
|
'InterfaceCommonForm',
|
||||||
@ -23,6 +24,20 @@ class InterfaceCommonForm(forms.Form):
|
|||||||
label=_('MTU')
|
label=_('MTU')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Determine the selected 802.1Q mode
|
||||||
|
interface_mode = get_field_value(self, 'mode')
|
||||||
|
|
||||||
|
# Delete VLAN tagging fields which are not relevant for the selected mode
|
||||||
|
if interface_mode in (InterfaceModeChoices.MODE_ACCESS, InterfaceModeChoices.MODE_TAGGED_ALL):
|
||||||
|
del self.fields['tagged_vlans']
|
||||||
|
elif not interface_mode:
|
||||||
|
del self.fields['vlan_group']
|
||||||
|
del self.fields['untagged_vlan']
|
||||||
|
del self.fields['tagged_vlans']
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
@ -1367,6 +1367,13 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
|||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'speed': SelectSpeedWidget(),
|
'speed': SelectSpeedWidget(),
|
||||||
|
'mode': forms.Select(
|
||||||
|
attrs={
|
||||||
|
'hx-get': '.',
|
||||||
|
'hx-include': '#form_fields input',
|
||||||
|
'hx-target': '#form_fields',
|
||||||
|
}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'mode': '802.1Q Mode',
|
'mode': '802.1Q Mode',
|
||||||
|
@ -431,6 +431,12 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
|
|||||||
form = self.initialize_form(request)
|
form = self.initialize_form(request)
|
||||||
instance = self.alter_object(self.queryset.model(), request)
|
instance = self.alter_object(self.queryset.model(), request)
|
||||||
|
|
||||||
|
# If this is an HTMX request, return only the rendered form HTML
|
||||||
|
if is_htmx(request):
|
||||||
|
return render(request, 'htmx/form.html', {
|
||||||
|
'form': form,
|
||||||
|
})
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'object': instance,
|
'object': instance,
|
||||||
'form': form,
|
'form': form,
|
||||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -1,10 +1,9 @@
|
|||||||
import { initFormElements } from './elements';
|
import { initFormElements } from './elements';
|
||||||
import { initSpeedSelector } from './speedSelector';
|
import { initSpeedSelector } from './speedSelector';
|
||||||
import { initScopeSelector } from './scopeSelector';
|
import { initScopeSelector } from './scopeSelector';
|
||||||
import { initVlanTags } from './vlanTags';
|
|
||||||
|
|
||||||
export function initForms(): void {
|
export function initForms(): void {
|
||||||
for (const func of [initFormElements, initSpeedSelector, initScopeSelector, initVlanTags]) {
|
for (const func of [initFormElements, initSpeedSelector, initScopeSelector]) {
|
||||||
func();
|
func();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,148 +0,0 @@
|
|||||||
import { all, getElement, resetSelect, toggleVisibility as _toggleVisibility } from '../util';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a select element's containing `.row` element.
|
|
||||||
*
|
|
||||||
* @param element Select element.
|
|
||||||
* @returns Containing row element.
|
|
||||||
*/
|
|
||||||
function fieldContainer(element: Nullable<HTMLSelectElement>): Nullable<HTMLElement> {
|
|
||||||
const container = element?.parentElement?.parentElement ?? null;
|
|
||||||
if (container !== null && container.classList.contains('row')) {
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle visibility of the select element's container and disable the select element itself.
|
|
||||||
*
|
|
||||||
* @param element Select element.
|
|
||||||
* @param action 'show' or 'hide'
|
|
||||||
*/
|
|
||||||
function toggleVisibility<E extends Nullable<HTMLSelectElement>>(
|
|
||||||
element: E,
|
|
||||||
action: 'show' | 'hide',
|
|
||||||
): void {
|
|
||||||
// Find the select element's containing element.
|
|
||||||
const parent = fieldContainer(element);
|
|
||||||
if (element !== null && parent !== null) {
|
|
||||||
// Toggle container visibility to visually remove it from the form.
|
|
||||||
_toggleVisibility(parent, action);
|
|
||||||
// Create a new event so that the APISelect instance properly handles the enable/disable
|
|
||||||
// action.
|
|
||||||
const event = new Event(`netbox.select.disabled.${element.name}`);
|
|
||||||
switch (action) {
|
|
||||||
case 'hide':
|
|
||||||
// Disable the native select element and dispatch the event APISelect is listening for.
|
|
||||||
element.disabled = true;
|
|
||||||
element.dispatchEvent(event);
|
|
||||||
break;
|
|
||||||
case 'show':
|
|
||||||
// Enable the native select element and dispatch the event APISelect is listening for.
|
|
||||||
element.disabled = false;
|
|
||||||
element.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle element visibility when the mode field does not have a value.
|
|
||||||
*/
|
|
||||||
function handleModeNone(): void {
|
|
||||||
const elements = [
|
|
||||||
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
|
||||||
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
|
||||||
getElement<HTMLSelectElement>('id_vlan_group'),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (all(elements)) {
|
|
||||||
const [taggedVlans, untaggedVlan] = elements;
|
|
||||||
resetSelect(untaggedVlan);
|
|
||||||
resetSelect(taggedVlans);
|
|
||||||
for (const element of elements) {
|
|
||||||
toggleVisibility(element, 'hide');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle element visibility when the mode field's value is Access.
|
|
||||||
*/
|
|
||||||
function handleModeAccess(): void {
|
|
||||||
const elements = [
|
|
||||||
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
|
||||||
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
|
||||||
getElement<HTMLSelectElement>('id_vlan_group'),
|
|
||||||
];
|
|
||||||
if (all(elements)) {
|
|
||||||
const [taggedVlans, untaggedVlan, vlanGroup] = elements;
|
|
||||||
resetSelect(taggedVlans);
|
|
||||||
toggleVisibility(vlanGroup, 'show');
|
|
||||||
toggleVisibility(untaggedVlan, 'show');
|
|
||||||
toggleVisibility(taggedVlans, 'hide');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle element visibility when the mode field's value is Tagged.
|
|
||||||
*/
|
|
||||||
function handleModeTagged(): void {
|
|
||||||
const elements = [
|
|
||||||
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
|
||||||
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
|
||||||
getElement<HTMLSelectElement>('id_vlan_group'),
|
|
||||||
];
|
|
||||||
if (all(elements)) {
|
|
||||||
const [taggedVlans, untaggedVlan, vlanGroup] = elements;
|
|
||||||
toggleVisibility(taggedVlans, 'show');
|
|
||||||
toggleVisibility(vlanGroup, 'show');
|
|
||||||
toggleVisibility(untaggedVlan, 'show');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle element visibility when the mode field's value is Tagged (All).
|
|
||||||
*/
|
|
||||||
function handleModeTaggedAll(): void {
|
|
||||||
const elements = [
|
|
||||||
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
|
||||||
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
|
||||||
getElement<HTMLSelectElement>('id_vlan_group'),
|
|
||||||
];
|
|
||||||
if (all(elements)) {
|
|
||||||
const [taggedVlans, untaggedVlan, vlanGroup] = elements;
|
|
||||||
resetSelect(taggedVlans);
|
|
||||||
toggleVisibility(vlanGroup, 'show');
|
|
||||||
toggleVisibility(untaggedVlan, 'show');
|
|
||||||
toggleVisibility(taggedVlans, 'hide');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset field visibility when the mode field's value changes.
|
|
||||||
*/
|
|
||||||
function handleModeChange(element: HTMLSelectElement): void {
|
|
||||||
switch (element.value) {
|
|
||||||
case 'access':
|
|
||||||
handleModeAccess();
|
|
||||||
break;
|
|
||||||
case 'tagged':
|
|
||||||
handleModeTagged();
|
|
||||||
break;
|
|
||||||
case 'tagged-all':
|
|
||||||
handleModeTaggedAll();
|
|
||||||
break;
|
|
||||||
case '':
|
|
||||||
handleModeNone();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initVlanTags(): void {
|
|
||||||
const element = getElement<HTMLSelectElement>('id_mode');
|
|
||||||
if (element !== null) {
|
|
||||||
element.addEventListener('change', () => handleModeChange(element));
|
|
||||||
handleModeChange(element);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
{% extends 'generic/object_edit.html' %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block form %}
|
|
||||||
{# Render hidden fields #}
|
|
||||||
{% for field in form.hidden_fields %}
|
|
||||||
{{ field }}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class="field-group my-5">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Interface</h5>
|
|
||||||
</div>
|
|
||||||
{% if form.instance.device %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label text-lg-end">Device</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ form.instance.device }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% render_field form.module %}
|
|
||||||
{% render_field form.name %}
|
|
||||||
{% render_field form.type %}
|
|
||||||
{% render_field form.speed %}
|
|
||||||
{% render_field form.duplex %}
|
|
||||||
{% render_field form.label %}
|
|
||||||
{% render_field form.description %}
|
|
||||||
{% render_field form.tags %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group my-5">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Addressing</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.vrf %}
|
|
||||||
{% render_field form.mac_address %}
|
|
||||||
{% render_field form.wwn %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group my-5">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Operation</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.mtu %}
|
|
||||||
{% render_field form.tx_power %}
|
|
||||||
{% render_field form.enabled %}
|
|
||||||
{% render_field form.mgmt_only %}
|
|
||||||
{% render_field form.mark_connected %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group my-5">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Related Interfaces</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.parent %}
|
|
||||||
{% render_field form.bridge %}
|
|
||||||
{% render_field form.lag %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if form.instance.is_wireless %}
|
|
||||||
<div class="field-group my-5">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Wireless</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.rf_role %}
|
|
||||||
{% render_field form.rf_channel %}
|
|
||||||
{% render_field form.rf_channel_frequency %}
|
|
||||||
{% render_field form.rf_channel_width %}
|
|
||||||
{% render_field form.wireless_lan_group %}
|
|
||||||
{% render_field form.wireless_lans %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="field-group my-5">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Power over Ethernet (PoE)</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.poe_mode %}
|
|
||||||
{% render_field form.poe_type %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group my-5">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">802.1Q Switching</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.mode %}
|
|
||||||
{% render_field form.vlan_group %}
|
|
||||||
{% render_field form.untagged_vlan %}
|
|
||||||
{% render_field form.tagged_vlans %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if form.custom_fields %}
|
|
||||||
<div class="field-group my-5">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
|
||||||
</div>
|
|
||||||
{% render_custom_fields form %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
@ -17,7 +17,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% for name in fields %}
|
{% for name in fields %}
|
||||||
{% with field=form|getfield:name %}
|
{% with field=form|getfield:name %}
|
||||||
{% if not field.field.widget.is_hidden %}
|
{% if field and not field.field.widget.is_hidden %}
|
||||||
{% render_field field %}
|
{% render_field field %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -12,6 +12,7 @@ __all__ = (
|
|||||||
'expand_alphanumeric_pattern',
|
'expand_alphanumeric_pattern',
|
||||||
'expand_ipaddress_pattern',
|
'expand_ipaddress_pattern',
|
||||||
'form_from_model',
|
'form_from_model',
|
||||||
|
'get_field_value',
|
||||||
'get_selected_values',
|
'get_selected_values',
|
||||||
'parse_alphanumeric_range',
|
'parse_alphanumeric_range',
|
||||||
'parse_numeric_range',
|
'parse_numeric_range',
|
||||||
@ -113,6 +114,21 @@ def expand_ipaddress_pattern(string, family):
|
|||||||
yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), remnant])
|
yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), remnant])
|
||||||
|
|
||||||
|
|
||||||
|
def get_field_value(form, field_name):
|
||||||
|
"""
|
||||||
|
Return the current bound or initial value associated with a form field, prior to calling
|
||||||
|
clean() for the form.
|
||||||
|
"""
|
||||||
|
field = form.fields[field_name]
|
||||||
|
|
||||||
|
if form.is_bound:
|
||||||
|
if data := form.data.get(field_name):
|
||||||
|
if field.valid_value(data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
return form.get_initial_for_field(field, field_name)
|
||||||
|
|
||||||
|
|
||||||
def get_selected_values(form, field_name):
|
def get_selected_values(form, field_name):
|
||||||
"""
|
"""
|
||||||
Return the list of selected human-friendly values for a form field
|
Return the list of selected human-friendly values for a form field
|
||||||
|
@ -11,9 +11,12 @@ register = template.Library()
|
|||||||
@register.filter()
|
@register.filter()
|
||||||
def getfield(form, fieldname):
|
def getfield(form, fieldname):
|
||||||
"""
|
"""
|
||||||
Return the specified field of a Form.
|
Return the specified bound field of a Form.
|
||||||
"""
|
"""
|
||||||
return form[fieldname]
|
try:
|
||||||
|
return form[fieldname]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='widget_type')
|
@register.filter(name='widget_type')
|
||||||
|
@ -349,6 +349,15 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
|
|||||||
labels = {
|
labels = {
|
||||||
'mode': '802.1Q Mode',
|
'mode': '802.1Q Mode',
|
||||||
}
|
}
|
||||||
|
widgets = {
|
||||||
|
'mode': forms.Select(
|
||||||
|
attrs={
|
||||||
|
'hx-get': '.',
|
||||||
|
'hx-include': '#form_fields input',
|
||||||
|
'hx-target': '#form_fields',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'mode': INTERFACE_MODE_HELP_TEXT,
|
'mode': INTERFACE_MODE_HELP_TEXT,
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user