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:
@ -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}'
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
14
netbox/project-static/dist/netbox.js
vendored
14
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
4
netbox/project-static/dist/netbox.js.map
vendored
4
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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 %}
|
||||
{% 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 %}
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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,
|
||||
}
|
||||
|
Reference in New Issue
Block a user