Add plugin dev docs for UI components

This commit is contained in:
Jeremy Stretch 2025-11-07 15:37:20 -05:00
parent a024012abd
commit 917280d1d3
7 changed files with 289 additions and 217 deletions

View File

@ -0,0 +1,148 @@
# UI Components
!!! note "New in NetBox v4.5"
All UI components described here were introduced in NetBox v4.5. Be sure to set the minimum NetBox version to 4.5.0 for your plugin before incorporating any of these resources.
!!! danger "Beta Feature"
UI components are considered a beta feature, and are still under active development. Please be aware that the API for resources on this page is subject to change in future releases.
To simply the process of designing your plugin's user interface, and to encourage a consistent look and feel throughout the entire application, NetBox provides a set of components that enable programmatic UI design. These make it possible to declare complex page layouts with little or no custom HTML.
## Page Layout
A layout defines the general arrangement of content on a page into rows and columns. The layout is defined under the [view](./views.md) and declares a set of rows, each of which may have one or more columns. Below is an example layout.
```
+-------+-------+-------+
| Col 1 | Col 2 | Col 3 |
+-------+-------+-------+
| Col 4 |
+-----------+-----------+
| Col 5 | Col 6 |
+-----------+-----------+
```
The above layout can be achieved with the following declaration under a view:
```python
from netbox.ui import layout
from netbox.views import generic
class MyView(generic.ObjectView):
layout = layout.Layout(
layout.Row(
layout.Column(),
layout.Column(),
layout.Column(),
),
layout.Row(
layout.Column(),
),
layout.Row(
layout.Column(),
layout.Column(),
),
)
```
!!! note
Currently, layouts are supported only for subclasses of [`generic.ObjectView`](./views.md#netbox.views.generic.ObjectView).
::: netbox.ui.layout.Layout
::: netbox.ui.layout.SimpleLayout
::: netbox.ui.layout.Row
::: netbox.ui.layout.Column
## Panels
Within each column, related blocks of content are arranged into panels. Each panel has a title and may have a set of associated actions, but the content within is otherwise arbitrary.
Plugins can define their own panels by inheriting from the base class `netbox.ui.panels.Panel`. Override the `get_context()` method to pass additional context to your custom panel template. An example is provided below.
```python
from django.utils.translation import gettext_lazy as _
from netbox.ui.panels import Panel
class RecentChangesPanel(Panel):
template_name = 'my_plugin/panels/recent_changes.html'
title = _('Recent Changes')
def get_context(self, context):
return {
**super().get_context(context),
'changes': get_changes()[:10],
}
```
NetBox also includes a set of panels suite for specific uses, such as display object details or embedding a table of related objects. These are listed below.
::: netbox.ui.panels.Panel
::: netbox.ui.panels.ObjectPanel
::: netbox.ui.panels.ObjectAttributesPanel
#### Object Attributes
The following classes are available to represent object attributes within an ObjectAttributesPanel. Additionally, plugins can subclass `netbox.ui.attrs.ObjectAttribute` to create custom classes.
| Class | Description |
|--------------------------------------|--------------------------------------------------|
| `netbox.ui.attrs.AddressAttr` | A physical or mailing address. |
| `netbox.ui.attrs.BooleanAttr` | A boolean value |
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
| `netbox.ui.attrs.ChoiceAttr` | A selection from a set of choices |
| `netbox.ui.attrs.GPSCoordinatesAttr` | GPS coordinates (latitude and longitude) |
| `netbox.ui.attrs.ImageAttr` | An attached image (displays the image) |
| `netbox.ui.attrs.NestedObjectAttr` | A related nested object |
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
| `netbox.ui.attrs.TemplatedAttr` | Renders an attribute using a custom template |
| `netbox.ui.attrs.TextAttr` | A string (text) value |
| `netbox.ui.attrs.TimezoneAttr` | A timezone with annotated offset |
| `netbox.ui.attrs.UtilizationAttr` | A numeric value expressed as a utilization graph |
::: netbox.ui.panels.OrganizationalObjectPanel
::: netbox.ui.panels.NestedGroupObjectPanel
::: netbox.ui.panels.CommentsPanel
::: netbox.ui.panels.JSONPanel
::: netbox.ui.panels.RelatedObjectsPanel
::: netbox.ui.panels.ObjectsTablePanel
::: netbox.ui.panels.TemplatePanel
::: netbox.ui.panels.PluginContentPanel
## Panel Actions
Each panel may have actions associated with it. These render as links or buttons within the panel header, opposite the panel's title. For example, a common use case is to include an "Add" action on a panel which displays a list of objects. Below is an example of this.
```python
from django.utils.translation import gettext_lazy as _
from netbox.ui import actions, panels
panels.ObjectsTablePanel(
model='dcim.Region',
title=_('Child Regions'),
filters={'parent_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}),
],
),
```
::: netbox.ui.actions.PanelAction
::: netbox.ui.actions.LinkAction
::: netbox.ui.actions.AddObject
::: netbox.ui.actions.CopyContent

View File

@ -143,6 +143,7 @@ nav:
- Getting Started: 'plugins/development/index.md'
- Models: 'plugins/development/models.md'
- Views: 'plugins/development/views.md'
- UI Components: 'plugins/development/ui-components.md'
- Navigation: 'plugins/development/navigation.md'
- Templates: 'plugins/development/templates.md'
- Tables: 'plugins/development/tables.md'

View File

@ -11,6 +11,7 @@ from utilities.views import get_viewname
__all__ = (
'AddObject',
'CopyContent',
'LinkAction',
'PanelAction',
)
@ -20,34 +21,28 @@ class PanelAction:
A link (typically a button) within a panel to perform some associated action, such as adding an object.
Attributes:
template_name: The name of the template to render
label: The default human-friendly button text
button_class: Bootstrap CSS class for the button
button_icon: Name of the button's MDI icon
template_name (str): The name of the template to render
Parameters:
label (str): The human-friendly button text
permissions (list): An iterable of permissions required to display the action
button_class (str): Bootstrap CSS class for the button
button_icon (str): Name of the button's MDI icon
"""
template_name = None
label = None
button_class = 'primary'
button_icon = None
def __init__(self, label=None, permissions=None):
"""
Initialize a new PanelAction.
Parameters:
label: The human-friendly button text
permissions: A list of permissions required to display the action
"""
if label is not None:
self.label = label
def __init__(self, label, permissions=None, button_class='primary', button_icon=None):
self.label = label
self.permissions = permissions
self.button_class = button_class
self.button_icon = button_icon
def get_context(self, context):
"""
Return the template context used to render the action element.
Parameters:
context: The template context
context (dict): The template context
"""
return {
'label': self.label,
@ -60,7 +55,7 @@ class PanelAction:
Render the action as HTML.
Parameters:
context: The template context
context (dict): The template context
"""
# Enforce permissions
user = context['request'].user
@ -74,26 +69,16 @@ class LinkAction(PanelAction):
"""
A hyperlink (typically a button) within a panel to perform some associated action, such as adding an object.
Attributes:
label: The default human-friendly button text
button_class: Bootstrap CSS class for the button
button_icon: Name of the button's MDI icon
Parameters:
view_name (str): Name of the view to which the action will link
view_kwargs (dict): Additional keyword arguments to pass to `reverse()` when resolving the URL
url_params (dict): A dictionary of arbitrary URL parameters to append to the action's URL. If the value of a key
is a callable, it will be passed the current template context.
"""
template_name = 'ui/actions/link.html'
def __init__(self, view_name, view_kwargs=None, url_params=None, **kwargs):
"""
Initialize a new PanelAction.
Parameters:
view_name: Name of the view to which the action will link
view_kwargs: Additional keyword arguments to pass to the view when resolving its URL
url_params: A dictionary of arbitrary URL parameters to append to the action's URL
permissions: A list of permissions required to display the action
label: The human-friendly button text
"""
super().__init__(**kwargs)
self.view_name = view_name
self.view_kwargs = view_kwargs or {}
self.url_params = url_params or {}
@ -103,7 +88,7 @@ class LinkAction(PanelAction):
Resolve the URL for the action from its view name and kwargs. Append any additional URL parameters.
Parameters:
context: The template context
context (dict): The template context
"""
url = reverse(self.view_name, kwargs=self.view_kwargs)
if self.url_params:
@ -127,19 +112,12 @@ class LinkAction(PanelAction):
class AddObject(LinkAction):
"""
An action to add a new object.
Parameters:
model (str): The dotted label of the model to be added (e.g. "dcim.site")
url_params (dict): A dictionary of arbitrary URL parameters to append to the resolved URL
"""
label = _('Add')
button_icon = 'plus-thick'
def __init__(self, model, url_params=None, label=None):
"""
Initialize a new AddObject action.
Parameters:
model: The dotted label of the model to be added (e.g. "dcim.site")
url_params: A dictionary of arbitrary URL parameters to append to the resolved URL
label: The human-friendly button text
"""
def __init__(self, model, url_params=None, **kwargs):
# Resolve the model class from its app.name label
try:
app_label, model_name = model.split('.')
@ -148,37 +126,29 @@ class AddObject(LinkAction):
raise ValueError(f"Invalid model label: {model}")
view_name = get_viewname(model, 'add')
super().__init__(view_name=view_name, url_params=url_params, label=label)
kwargs.setdefault('label', _('Add'))
kwargs.setdefault('button_icon', 'plus-thick')
kwargs.setdefault('permissions', [get_permission_for_model(model, 'add')])
# Require "add" permission on the model
self.permissions = [get_permission_for_model(model, 'add')]
super().__init__(view_name=view_name, url_params=url_params, **kwargs)
class CopyContent(PanelAction):
"""
An action to copy the contents of a panel to the clipboard.
Parameters:
target_id (str): The ID of the target element containing the content to be copied
"""
template_name = 'ui/actions/copy_content.html'
label = _('Copy')
button_icon = 'content-copy'
def __init__(self, target_id, **kwargs):
"""
Instantiate a new CopyContent action.
Parameters:
target_id: The ID of the target element containing the content to be copied
"""
kwargs.setdefault('label', _('Copy'))
kwargs.setdefault('button_icon', 'content-copy')
super().__init__(**kwargs)
self.target_id = target_id
def render(self, context):
"""
Render the action as HTML.
Parameters:
context: The template context
"""
return render_to_string(self.template_name, {
'target_id': self.target_id,
'label': self.label,

View File

@ -34,22 +34,18 @@ class ObjectAttribute:
Base class for representing an attribute of an object.
Attributes:
template_name: The name of the template to render
label: Human-friendly label for the rendered attribute
placeholder: HTML to render for empty/null values
template_name (str): The name of the template to render
placeholder (str): HTML to render for empty/null values
Parameters:
accessor (str): The dotted path to the attribute being rendered (e.g. "site.region.name")
label (str): Human-friendly label for the rendered attribute
"""
template_name = None
label = None
placeholder = mark_safe(PLACEHOLDER_HTML)
def __init__(self, accessor, label=None):
"""
Instantiate a new ObjectAttribute.
Parameters:
accessor: The dotted path to the attribute being rendered (e.g. "site.region.name")
label: Human-friendly label for the rendered attribute
"""
self.accessor = accessor
if label is not None:
self.label = label
@ -59,7 +55,7 @@ class ObjectAttribute:
Return the value of the attribute.
Parameters:
obj: The object for which the attribute is being rendered
obj (object): The object for which the attribute is being rendered
"""
return resolve_attr_path(obj, self.accessor)
@ -68,8 +64,8 @@ class ObjectAttribute:
Return any additional template context used to render the attribute value.
Parameters:
obj: The object for which the attribute is being rendered
context: The root template context
obj (object): The object for which the attribute is being rendered
context (dict): The root template context
"""
return {}
@ -90,21 +86,15 @@ class ObjectAttribute:
class TextAttr(ObjectAttribute):
"""
A text attribute.
Parameters:
style (str): CSS class to apply to the rendered attribute
format_string (str): If specified, the value will be formatted using this string when rendering
copy_button (bool): Set to True to include a copy-to-clipboard button
"""
template_name = 'ui/attrs/text.html'
def __init__(self, *args, style=None, format_string=None, copy_button=False, **kwargs):
"""
Instantiate a new TextAttr.
Parameters:
accessor: The dotted path to the attribute being rendered (e.g. "site.region.name")
label: Human-friendly label for the rendered attribute
template_name: The name of the template to render
style: CSS class to apply to the rendered attribute
format_string: If specified, the value will be formatted using this string when rendering
copy_button: Set to True to include a copy-to-clipboard button
"""
super().__init__(*args, **kwargs)
self.style = style
self.format_string = format_string
@ -127,20 +117,14 @@ class TextAttr(ObjectAttribute):
class NumericAttr(ObjectAttribute):
"""
An integer or float attribute.
Parameters:
unit_accessor (str): Accessor for the unit of measurement to display alongside the value (if any)
copy_button (bool): Set to True to include a copy-to-clipboard button
"""
template_name = 'ui/attrs/numeric.html'
def __init__(self, *args, unit_accessor=None, copy_button=False, **kwargs):
"""
Instantiate a new NumericAttr.
Parameters:
accessor: The dotted path to the attribute being rendered (e.g. "site.region.name")
unit_accessor: Accessor for the unit of measurement to display alongside the value (if any)
copy_button: Set to True to include a copy-to-clipboard button
label: Human-friendly label for the rendered attribute
template_name: The name of the template to render
"""
super().__init__(*args, **kwargs)
self.unit_accessor = unit_accessor
self.copy_button = copy_button
@ -181,19 +165,13 @@ class ChoiceAttr(ObjectAttribute):
class BooleanAttr(ObjectAttribute):
"""
A boolean attribute.
Parameters:
display_false (bool): If False, a placeholder will be rendered instead of the "False" indication
"""
template_name = 'ui/attrs/boolean.html'
def __init__(self, *args, display_false=True, **kwargs):
"""
Instantiate a new BooleanAttr.
Parameters:
accessor: The dotted path to the attribute being rendered (e.g. "site.region.name")
display_false: If False, a placeholder will be rendered instead of the "False" indication
label: Human-friendly label for the rendered attribute
template_name: The name of the template to render
"""
super().__init__(*args, **kwargs)
self.display_false = display_false
@ -222,21 +200,15 @@ class ImageAttr(ObjectAttribute):
class RelatedObjectAttr(ObjectAttribute):
"""
An attribute representing a related object.
Parameters:
linkify (bool): If True, the rendered value will be hyperlinked to the related object's detail view
grouped_by (str): A second-order object to annotate alongside the related object; for example, an attribute
representing the dcim.Site model might specify grouped_by="region"
"""
template_name = 'ui/attrs/object.html'
def __init__(self, *args, linkify=None, grouped_by=None, **kwargs):
"""
Instantiate a new RelatedObjectAttr.
Parameters:
accessor: The dotted path to the attribute being rendered (e.g. "site.region.name")
linkify: If True, the rendered value will be hyperlinked to the related object's detail view
grouped_by: A second-order object to annotate alongside the related object; for example, an attribute
representing the dcim.Site model might specify grouped_by="region"
label: Human-friendly label for the rendered attribute
template_name: The name of the template to render
"""
super().__init__(*args, **kwargs)
self.linkify = linkify
self.grouped_by = grouped_by
@ -254,20 +226,14 @@ class NestedObjectAttr(ObjectAttribute):
"""
An attribute representing a related nested object. Similar to `RelatedObjectAttr`, but includes the ancestors of the
related object in the rendered output.
Parameters:
linkify (bool): If True, the rendered value will be hyperlinked to the related object's detail view
max_depth (int): Maximum number of ancestors to display (default: all)
"""
template_name = 'ui/attrs/nested_object.html'
def __init__(self, *args, linkify=None, max_depth=None, **kwargs):
"""
Instantiate a new NestedObjectAttr. Shows a related object as well as its ancestors.
Parameters:
accessor: The dotted path to the attribute being rendered (e.g. "site.region.name")
linkify: If True, the rendered value will be hyperlinked to the related object's detail view
max_depth: Maximum number of ancestors to display (default: all)
label: Human-friendly label for the rendered attribute
template_name: The name of the template to render
"""
super().__init__(*args, **kwargs)
self.linkify = linkify
self.max_depth = max_depth
@ -286,19 +252,13 @@ class NestedObjectAttr(ObjectAttribute):
class AddressAttr(ObjectAttribute):
"""
A physical or mailing address.
Parameters:
map_url (bool): If true, the address will render as a hyperlink using settings.MAPS_URL
"""
template_name = 'ui/attrs/address.html'
def __init__(self, *args, map_url=True, **kwargs):
"""
Instantiate a new AddressAttr.
Parameters:
accessor: The dotted path to the attribute being rendered (e.g. "site.region.name")
map_url: If true, the address will render as a hyperlink using settings.MAPS_URL
label: Human-friendly label for the rendered attribute
template_name: The name of the template to render
"""
super().__init__(*args, **kwargs)
if map_url is True:
self.map_url = get_config().MAPS_URL
@ -316,21 +276,16 @@ class AddressAttr(ObjectAttribute):
class GPSCoordinatesAttr(ObjectAttribute):
"""
A GPS coordinates pair comprising latitude and longitude values.
Parameters:
latitude_attr (float): The name of the field containing the latitude value
longitude_attr (float): The name of the field containing the longitude value
map_url (bool): If true, the address will render as a hyperlink using settings.MAPS_URL
"""
template_name = 'ui/attrs/gps_coordinates.html'
label = _('GPS coordinates')
def __init__(self, latitude_attr='latitude', longitude_attr='longitude', map_url=True, **kwargs):
"""
Instantiate a new GPSCoordinatesAttr.
Parameters:
latitude_attr: The name of the field containing the latitude value
longitude_attr: The name of the field containing the longitude value
map_url: If true, the address will render as a hyperlink using settings.MAPS_URL
label: Human-friendly label for the rendered attribute
template_name: The name of the template to render
"""
super().__init__(accessor=None, **kwargs)
self.latitude_attr = latitude_attr
self.longitude_attr = longitude_attr
@ -365,18 +320,12 @@ class TimezoneAttr(ObjectAttribute):
class TemplatedAttr(ObjectAttribute):
"""
Renders an attribute using a custom template.
Parameters:
template_name (str): The name of the template to render
context (dict): Additional context to pass to the template when rendering
"""
def __init__(self, *args, template_name, context=None, **kwargs):
"""
Instantiate a new TemplatedAttr.
Parameters:
accessor: The dotted path to the attribute being rendered (e.g. "site.region.name")
template_name: The name of the template to render
context: Additional context to pass to the template when rendering
label: Human-friendly label for the rendered attribute
template_name: The name of the template to render
"""
super().__init__(*args, **kwargs)
self.template_name = template_name
self.context = context or {}

View File

@ -15,6 +15,9 @@ __all__ = (
class Layout:
"""
A collection of rows and columns comprising the layout of content within the user interface.
Parameters:
*rows: One or more Row instances
"""
def __init__(self, *rows):
for i, row in enumerate(rows):
@ -26,6 +29,9 @@ class Layout:
class Row:
"""
A collection of columns arranged horizontally.
Parameters:
*columns: One or more Column instances
"""
def __init__(self, *columns):
for i, column in enumerate(columns):
@ -37,6 +43,9 @@ class Row:
class Column:
"""
A collection of panels arranged vertically.
Parameters:
*panels: One or more Panel instances
"""
def __init__(self, *panels):
for i, panel in enumerate(panels):
@ -51,13 +60,23 @@ class Column:
class SimpleLayout(Layout):
"""
A layout with one row of two columns and a second row with one column. Includes registered plugin content.
A layout with one row of two columns and a second row with one column.
+------+------+
| col1 | col2 |
+------+------+
| col3 |
+-------------+
Plugin content registered for `left_page`, `right_page`, or `full_width_path` is included automatically. Most object
views in NetBox utilize this layout.
```
+-------+-------+
| Col 1 | Col 2 |
+-------+-------+
| Col 3 |
+---------------+
```
Parameters:
left_panels: Panel instances to be rendered in the top lefthand column
right_panels: Panel instances to be rendered in the top righthand column
bottom_panels: Panel instances to be rendered in the bottom row
"""
def __init__(self, left_panels=None, right_panels=None, bottom_panels=None):
left_panels = left_panels or []

View File

@ -36,19 +36,19 @@ class Panel:
Panels are arranged within rows and columns, (generally) render as discrete "cards" within the user interface. Each
panel has a title and may have one or more actions associated with it, which will be rendered as hyperlinks in the
top right corner of the card.
Attributes:
template_name (str): The name of the template used to render the panel
Parameters:
title (str): The human-friendly title of the panel
actions (list): An iterable of PanelActions to include in the panel header
"""
template_name = None
title = None
actions = None
def __init__(self, title=None, actions=None):
"""
Instantiate a new Panel.
Parameters:
title: The human-friendly title of the panel
actions: An iterable of PanelActions to include in the panel header
"""
if title is not None:
self.title = title
self.actions = actions or self.actions or []
@ -58,7 +58,7 @@ class Panel:
Return the context data to be used when rendering the panel.
Parameters:
context: The template context
context (dict): The template context
"""
return {
'request': context.get('request'),
@ -72,7 +72,7 @@ class Panel:
Render the panel as HTML.
Parameters:
context: The template context
context (dict): The template context
"""
return render_to_string(self.template_name, self.get_context(context))
@ -84,16 +84,13 @@ class Panel:
class ObjectPanel(Panel):
"""
Base class for object-specific panels.
Parameters:
accessor (str): The dotted path in context data to the object being rendered (default: "object")
"""
accessor = 'object'
def __init__(self, accessor=None, **kwargs):
"""
Instantiate a new ObjectPanel.
Parameters:
accessor: The dotted path in context data to the object being rendered (default: "object")
"""
super().__init__(**kwargs)
if accessor is not None:
@ -141,17 +138,16 @@ class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta):
Attributes are added to the panel by declaring ObjectAttribute instances in the class body (similar to fields on
a Django form). Attributes are displayed in the order they are declared.
Note that the `only` and `exclude` parameters are mutually exclusive.
Parameters:
only (list): If specified, only attributes in this list will be displayed
exclude (list): If specified, attributes in this list will be excluded from display
"""
template_name = 'ui/panels/object_attributes.html'
def __init__(self, only=None, exclude=None, **kwargs):
"""
Instantiate a new ObjectPanel.
Parameters:
only: If specified, only attributes in this list will be displayed
exclude: If specified, attributes in this list will be excluded from display
"""
super().__init__(**kwargs)
# Set included/excluded attributes
@ -192,7 +188,7 @@ class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta):
class OrganizationalObjectPanel(ObjectAttributesPanel, metaclass=ObjectAttributesPanelMeta):
"""
An ObjectPanel with attributes common to OrganizationalModels. Includes name and description.
An ObjectPanel with attributes common to OrganizationalModels. Includes `name` and `description` attributes.
"""
name = attrs.TextAttr('name', label=_('Name'))
description = attrs.TextAttr('description', label=_('Description'))
@ -200,25 +196,24 @@ class OrganizationalObjectPanel(ObjectAttributesPanel, metaclass=ObjectAttribute
class NestedGroupObjectPanel(ObjectAttributesPanel, metaclass=ObjectAttributesPanelMeta):
"""
An ObjectPanel with attributes common to NestedGroupObjects. Includes the parent object.
An ObjectPanel with attributes common to NestedGroupObjects. Includes the `parent` attribute.
"""
name = attrs.TextAttr('name', label=_('Name'))
parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)
description = attrs.TextAttr('description', label=_('Description'))
class CommentsPanel(ObjectPanel):
"""
A panel which displays comments associated with an object.
Parameters:
field_name (str): The name of the comment field on the object (default: "comments")
"""
template_name = 'ui/panels/comments.html'
title = _('Comments')
def __init__(self, field_name='comments', **kwargs):
"""
Instantiate a new CommentsPanel.
Parameters:
field_name: The name of the comment field on the object (default: "comments")
"""
super().__init__(**kwargs)
self.field_name = field_name
@ -232,17 +227,14 @@ class CommentsPanel(ObjectPanel):
class JSONPanel(ObjectPanel):
"""
A panel which renders formatted JSON data from an object's JSONField.
Parameters:
field_name (str): The name of the JSON field on the object
copy_button (bool): Set to True (default) to include a copy-to-clipboard button
"""
template_name = 'ui/panels/json.html'
def __init__(self, field_name, copy_button=True, **kwargs):
"""
Instantiate a new JSONPanel.
Parameters:
field_name: The name of the JSON field on the object
copy_button: Set to True (default) to include a copy-to-clipboard button
"""
super().__init__(**kwargs)
self.field_name = field_name
@ -278,18 +270,16 @@ class RelatedObjectsPanel(Panel):
class ObjectsTablePanel(Panel):
"""
A panel which displays a table of objects (rendered via HTMX).
Parameters:
model (str): The dotted label of the model to be added (e.g. "dcim.site")
filters (dict): A dictionary of arbitrary URL parameters to append to the table's URL. If the value of a key is
a callable, it will be passed the current template context.
"""
template_name = 'ui/panels/objects_table.html'
title = None
def __init__(self, model, filters=None, **kwargs):
"""
Instantiate a new ObjectsTablePanel.
Parameters:
model: The dotted label of the model to be added (e.g. "dcim.site")
filters: A dictionary of arbitrary URL parameters to append to the table's URL
"""
super().__init__(**kwargs)
# Resolve the model class from its app.name label
@ -321,14 +311,11 @@ class ObjectsTablePanel(Panel):
class TemplatePanel(Panel):
"""
A panel which renders custom content using an HTML template.
Parameters:
template_name (str): The name of the template to render
"""
def __init__(self, template_name, **kwargs):
"""
Instantiate a new TemplatePanel.
Parameters:
template_name: The name of the template to render
"""
super().__init__(**kwargs)
self.template_name = template_name
@ -342,7 +329,7 @@ class PluginContentPanel(Panel):
A panel which displays embedded plugin content.
Parameters:
method: The name of the plugin method to render (e.g. "left_page")
method (str): The name of the plugin method to render (e.g. "left_page")
"""
def __init__(self, method, **kwargs):
super().__init__(**kwargs)

View File

@ -44,6 +44,7 @@ class ObjectView(ActionsMixin, BaseObjectView):
Note: If `template_name` is not specified, it will be determined automatically based on the queryset model.
Attributes:
layout: An instance of `netbox.ui.layout.Layout` which defines the page layout (overrides HTML template)
tab: A ViewTab instance for the view
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
"""
@ -59,9 +60,6 @@ class ObjectView(ActionsMixin, BaseObjectView):
Return self.template_name if defined. Otherwise, dynamically resolve the template name using the queryset
model's `app_label` and `model_name`.
"""
# TODO: Temporarily allow layout to override template_name
if self.layout is not None:
return 'generic/object.html'
if self.template_name is not None:
return self.template_name
model_opts = self.queryset.model._meta