Browse Source

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

jeremystretch 3 years ago
parent
commit
c84f0de8f8

+ 6 - 11
netbox/core/forms/model_forms.py

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

+ 15 - 0
netbox/dcim/forms/common.py

@@ -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()
 

+ 7 - 0
netbox/dcim/forms/model_forms.py

@@ -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',

+ 6 - 0
netbox/netbox/views/generic/object_views.py

@@ -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 it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 1 - 2
netbox/project-static/src/forms/index.ts

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

+ 0 - 148
netbox/project-static/src/forms/vlanTags.ts

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

+ 0 - 101
netbox/templates/dcim/interface_edit.html

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

+ 1 - 1
netbox/templates/htmx/form.html

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

+ 16 - 0
netbox/utilities/forms/utils.py

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

+ 5 - 2
netbox/utilities/templatetags/form_helpers.py

@@ -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')

+ 9 - 0
netbox/virtualization/forms/model_forms.py

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

Some files were not shown because too many files changed in this diff