-
Notifications
You must be signed in to change notification settings - Fork 3k
Description
NetBox version
v4.5.2
Feature type
Change to existing functionality
Proposed functionality
Add support for rendering description annotations (subtitle text below each option label) in static <select> dropdowns, matching the existing behavior of DynamicModelChoiceField's AJAX-powered dropdowns.
Currently, NetBox's TomSelect integration renders descriptions only for dynamic (API-powered) selects via DynamicTomSelect in dynamic.ts. Static selects initialized by initStaticSelects() in static.ts use NetBoxTomSelect without a custom render function, so there is no way to display descriptions.
Proposed implementation:
- New widget or enhancement to
APISelect: Add aDescriptionSelectwidget (or extend the existingSelectwidget) that accepts a description mapping and rendersdata-descriptionattributes on<option>elements:
# Example widget
class DescriptionSelect(forms.Select):
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
option = super().create_option(name, value, label, selected, index, subindex, attrs)
if description := self.descriptions.get(value, ''):
option['attrs']['data-description'] = description
return option- Update
initStaticSelects()instatic.ts: Add a custom render function that checks fordata-descriptionon<option>elements and renders them as<br /><small class="text-secondary">...</small>— the same HTML pattern already used indynamic.ts:
// In static.ts — detect and render descriptions for static selects
function renderOption(data: TomOption, escape: typeof escape_html) {
const option = select.querySelector(`option[value="${data.value}"]`);
const description = option?.dataset.description;
let html = `<div>${escape(data.text)}`;
if (description) {
html += `<br /><small class="text-secondary">${escape(description)}</small>`;
}
html += `</div>`;
return html;
}- Optional: A
ModelChoiceFieldsubclass (e.g.,DescriptionModelChoiceField) that automatically populates the widget's description mapping from the queryset's.descriptionfield, similar to howDynamicModelChoiceFieldworks with the API.
Use case
Plugin developers who need filtered dropdowns with description annotations currently have no clean path to achieve this. The core issue is that DynamicModelChoiceField ignores the .queryset set in __init__() for the dropdown UI — it fetches all options from the REST API via AJAX. The queryset only affects server-side validation on form submission.
This means:
DynamicModelChoiceFieldwith a filtered queryset: dropdown shows all objects (queryset ignored for UI), descriptions workModelChoiceFieldwith a filtered queryset: dropdown correctly filters, but no description annotations
Concrete example: Our plugin restricts which DeviceRole values are available per tenant hierarchy. The filtering logic (tenant group ancestry, cross-tenant intersection) cannot be expressed via query_params on the standard API endpoint. We need a ModelChoiceField with a custom queryset — but then we lose the description subtitle styling.
Current workaround: Build a custom REST API endpoint that duplicates the filtering logic, register a URL for it, and use DynamicModelChoiceField with widget=APISelect(api_url='/api/plugins/.../custom-endpoint/'). This works but requires significant boilerplate:
- A custom DRF
APIViewreturning{count, results}withid,display,descriptionfields - URL registration for the endpoint
- Using
query_paramswith$fieldreferences for dynamic dependencies - Redundant server-side validation in
clean()as a safety net
With native static description support, all of this would be replaced by a ModelChoiceField with a DescriptionSelect widget — a single line change.
Database changes
None. This is a frontend/widget change only.
External dependencies
None. Uses existing TomSelect library already bundled with NetBox.