From 917280d1d3a392f29a1151faff4ebe2e9dd21d44 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 7 Nov 2025 15:37:20 -0500 Subject: [PATCH] Add plugin dev docs for UI components --- docs/plugins/development/ui-components.md | 148 ++++++++++++++++++++ mkdocs.yml | 1 + netbox/netbox/ui/actions.py | 98 +++++-------- netbox/netbox/ui/attrs.py | 135 ++++++------------ netbox/netbox/ui/layout.py | 31 +++- netbox/netbox/ui/panels.py | 89 +++++------- netbox/netbox/views/generic/object_views.py | 4 +- 7 files changed, 289 insertions(+), 217 deletions(-) create mode 100644 docs/plugins/development/ui-components.md diff --git a/docs/plugins/development/ui-components.md b/docs/plugins/development/ui-components.md new file mode 100644 index 000000000..a8fe2eff0 --- /dev/null +++ b/docs/plugins/development/ui-components.md @@ -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 diff --git a/mkdocs.yml b/mkdocs.yml index 078fc5e50..07628e775 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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' diff --git a/netbox/netbox/ui/actions.py b/netbox/netbox/ui/actions.py index 2dd8482b2..7579e7b93 100644 --- a/netbox/netbox/ui/actions.py +++ b/netbox/netbox/ui/actions.py @@ -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, diff --git a/netbox/netbox/ui/attrs.py b/netbox/netbox/ui/attrs.py index 53e295246..bf55e3f3c 100644 --- a/netbox/netbox/ui/attrs.py +++ b/netbox/netbox/ui/attrs.py @@ -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 {} diff --git a/netbox/netbox/ui/layout.py b/netbox/netbox/ui/layout.py index d3fc69535..b59fd7b34 100644 --- a/netbox/netbox/ui/layout.py +++ b/netbox/netbox/ui/layout.py @@ -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 [] diff --git a/netbox/netbox/ui/panels.py b/netbox/netbox/ui/panels.py index 149b48563..5699c64e9 100644 --- a/netbox/netbox/ui/panels.py +++ b/netbox/netbox/ui/panels.py @@ -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) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index eb1a4d3a9..88a3456f7 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -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