1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

#11625: Employ HTMX form rendering for device & VM interfaces

This commit is contained in:
jeremystretch
2023-02-18 15:32:26 -05:00
parent 368e774ceb
commit c84f0de8f8
13 changed files with 75 additions and 274 deletions

View File

@ -5,7 +5,7 @@ from django import forms
from core.models import *
from netbox.forms import NetBoxModelForm
from netbox.registry import registry
from utilities.forms import CommentField
from utilities.forms import CommentField, get_field_value
__all__ = (
'DataSourceForm',
@ -44,7 +44,7 @@ class DataSourceForm(NetBoxModelForm):
]
if self.backend_fields:
fieldsets.append(
('Backend', self.backend_fields)
('Backend Parameters', self.backend_fields)
)
return fieldsets
@ -52,16 +52,11 @@ class DataSourceForm(NetBoxModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
backend_classes = registry['data_backends']
if self.is_bound and self.data.get('type') in backend_classes:
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_)
# Determine the selected backend type
backend_type = get_field_value(self, 'type')
backend = registry['data_backends'].get(backend_type)
# Add backend-specific form fields
self.backend_fields = []
for name, form_field in backend.parameters.items():
field_name = f'backend_{name}'

View File

@ -3,6 +3,7 @@ from django.utils.translation import gettext as _
from dcim.choices import *
from dcim.constants import *
from utilities.forms.utils import get_field_value
__all__ = (
'InterfaceCommonForm',
@ -23,6 +24,20 @@ class InterfaceCommonForm(forms.Form):
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):
super().clean()

View File

@ -1367,6 +1367,13 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
]
widgets = {
'speed': SelectSpeedWidget(),
'mode': forms.Select(
attrs={
'hx-get': '.',
'hx-include': '#form_fields input',
'hx-target': '#form_fields',
}
),
}
labels = {
'mode': '802.1Q Mode',

View File

@ -431,6 +431,12 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
form = self.initialize_form(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, {
'object': instance,
'form': form,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,9 @@
import { initFormElements } from './elements';
import { initSpeedSelector } from './speedSelector';
import { initScopeSelector } from './scopeSelector';
import { initVlanTags } from './vlanTags';
export function initForms(): void {
for (const func of [initFormElements, initSpeedSelector, initScopeSelector, initVlanTags]) {
for (const func of [initFormElements, initSpeedSelector, initScopeSelector]) {
func();
}
}

View File

@ -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);
}
}

View File

@ -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 %}

View File

@ -17,7 +17,7 @@
{% endif %}
{% for name in fields %}
{% with field=form|getfield:name %}
{% if not field.field.widget.is_hidden %}
{% if field and not field.field.widget.is_hidden %}
{% render_field field %}
{% endif %}
{% endwith %}

View File

@ -12,6 +12,7 @@ __all__ = (
'expand_alphanumeric_pattern',
'expand_ipaddress_pattern',
'form_from_model',
'get_field_value',
'get_selected_values',
'parse_alphanumeric_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])
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):
"""
Return the list of selected human-friendly values for a form field

View File

@ -11,9 +11,12 @@ register = template.Library()
@register.filter()
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')

View File

@ -349,6 +349,15 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
labels = {
'mode': '802.1Q Mode',
}
widgets = {
'mode': forms.Select(
attrs={
'hx-get': '.',
'hx-include': '#form_fields input',
'hx-target': '#form_fields',
}
),
}
help_texts = {
'mode': INTERFACE_MODE_HELP_TEXT,
}