Skip to content

Commit 917280d

Browse files
committed
Add plugin dev docs for UI components
1 parent a024012 commit 917280d

File tree

7 files changed

+289
-217
lines changed

7 files changed

+289
-217
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# UI Components
2+
3+
!!! note "New in NetBox v4.5"
4+
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.
5+
6+
!!! danger "Beta Feature"
7+
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.
8+
9+
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.
10+
11+
## Page Layout
12+
13+
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.
14+
15+
```
16+
+-------+-------+-------+
17+
| Col 1 | Col 2 | Col 3 |
18+
+-------+-------+-------+
19+
| Col 4 |
20+
+-----------+-----------+
21+
| Col 5 | Col 6 |
22+
+-----------+-----------+
23+
```
24+
25+
The above layout can be achieved with the following declaration under a view:
26+
27+
```python
28+
from netbox.ui import layout
29+
from netbox.views import generic
30+
31+
class MyView(generic.ObjectView):
32+
layout = layout.Layout(
33+
layout.Row(
34+
layout.Column(),
35+
layout.Column(),
36+
layout.Column(),
37+
),
38+
layout.Row(
39+
layout.Column(),
40+
),
41+
layout.Row(
42+
layout.Column(),
43+
layout.Column(),
44+
),
45+
)
46+
```
47+
48+
!!! note
49+
Currently, layouts are supported only for subclasses of [`generic.ObjectView`](./views.md#netbox.views.generic.ObjectView).
50+
51+
::: netbox.ui.layout.Layout
52+
53+
::: netbox.ui.layout.SimpleLayout
54+
55+
::: netbox.ui.layout.Row
56+
57+
::: netbox.ui.layout.Column
58+
59+
## Panels
60+
61+
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.
62+
63+
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.
64+
65+
```python
66+
from django.utils.translation import gettext_lazy as _
67+
from netbox.ui.panels import Panel
68+
69+
class RecentChangesPanel(Panel):
70+
template_name = 'my_plugin/panels/recent_changes.html'
71+
title = _('Recent Changes')
72+
73+
def get_context(self, context):
74+
return {
75+
**super().get_context(context),
76+
'changes': get_changes()[:10],
77+
}
78+
```
79+
80+
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.
81+
82+
::: netbox.ui.panels.Panel
83+
84+
::: netbox.ui.panels.ObjectPanel
85+
86+
::: netbox.ui.panels.ObjectAttributesPanel
87+
88+
#### Object Attributes
89+
90+
The following classes are available to represent object attributes within an ObjectAttributesPanel. Additionally, plugins can subclass `netbox.ui.attrs.ObjectAttribute` to create custom classes.
91+
92+
| Class | Description |
93+
|--------------------------------------|--------------------------------------------------|
94+
| `netbox.ui.attrs.AddressAttr` | A physical or mailing address. |
95+
| `netbox.ui.attrs.BooleanAttr` | A boolean value |
96+
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
97+
| `netbox.ui.attrs.ChoiceAttr` | A selection from a set of choices |
98+
| `netbox.ui.attrs.GPSCoordinatesAttr` | GPS coordinates (latitude and longitude) |
99+
| `netbox.ui.attrs.ImageAttr` | An attached image (displays the image) |
100+
| `netbox.ui.attrs.NestedObjectAttr` | A related nested object |
101+
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
102+
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
103+
| `netbox.ui.attrs.TemplatedAttr` | Renders an attribute using a custom template |
104+
| `netbox.ui.attrs.TextAttr` | A string (text) value |
105+
| `netbox.ui.attrs.TimezoneAttr` | A timezone with annotated offset |
106+
| `netbox.ui.attrs.UtilizationAttr` | A numeric value expressed as a utilization graph |
107+
108+
::: netbox.ui.panels.OrganizationalObjectPanel
109+
110+
::: netbox.ui.panels.NestedGroupObjectPanel
111+
112+
::: netbox.ui.panels.CommentsPanel
113+
114+
::: netbox.ui.panels.JSONPanel
115+
116+
::: netbox.ui.panels.RelatedObjectsPanel
117+
118+
::: netbox.ui.panels.ObjectsTablePanel
119+
120+
::: netbox.ui.panels.TemplatePanel
121+
122+
::: netbox.ui.panels.PluginContentPanel
123+
124+
## Panel Actions
125+
126+
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.
127+
128+
```python
129+
from django.utils.translation import gettext_lazy as _
130+
from netbox.ui import actions, panels
131+
132+
panels.ObjectsTablePanel(
133+
model='dcim.Region',
134+
title=_('Child Regions'),
135+
filters={'parent_id': lambda ctx: ctx['object'].pk},
136+
actions=[
137+
actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}),
138+
],
139+
),
140+
```
141+
142+
::: netbox.ui.actions.PanelAction
143+
144+
::: netbox.ui.actions.LinkAction
145+
146+
::: netbox.ui.actions.AddObject
147+
148+
::: netbox.ui.actions.CopyContent

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ nav:
143143
- Getting Started: 'plugins/development/index.md'
144144
- Models: 'plugins/development/models.md'
145145
- Views: 'plugins/development/views.md'
146+
- UI Components: 'plugins/development/ui-components.md'
146147
- Navigation: 'plugins/development/navigation.md'
147148
- Templates: 'plugins/development/templates.md'
148149
- Tables: 'plugins/development/tables.md'

netbox/netbox/ui/actions.py

Lines changed: 34 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
__all__ = (
1212
'AddObject',
1313
'CopyContent',
14+
'LinkAction',
1415
'PanelAction',
1516
)
1617

@@ -20,34 +21,28 @@ class PanelAction:
2021
A link (typically a button) within a panel to perform some associated action, such as adding an object.
2122
2223
Attributes:
23-
template_name: The name of the template to render
24-
label: The default human-friendly button text
25-
button_class: Bootstrap CSS class for the button
26-
button_icon: Name of the button's MDI icon
24+
template_name (str): The name of the template to render
25+
26+
Parameters:
27+
label (str): The human-friendly button text
28+
permissions (list): An iterable of permissions required to display the action
29+
button_class (str): Bootstrap CSS class for the button
30+
button_icon (str): Name of the button's MDI icon
2731
"""
2832
template_name = None
29-
label = None
30-
button_class = 'primary'
31-
button_icon = None
32-
33-
def __init__(self, label=None, permissions=None):
34-
"""
35-
Initialize a new PanelAction.
3633

37-
Parameters:
38-
label: The human-friendly button text
39-
permissions: A list of permissions required to display the action
40-
"""
41-
if label is not None:
42-
self.label = label
34+
def __init__(self, label, permissions=None, button_class='primary', button_icon=None):
35+
self.label = label
4336
self.permissions = permissions
37+
self.button_class = button_class
38+
self.button_icon = button_icon
4439

4540
def get_context(self, context):
4641
"""
4742
Return the template context used to render the action element.
4843
4944
Parameters:
50-
context: The template context
45+
context (dict): The template context
5146
"""
5247
return {
5348
'label': self.label,
@@ -60,7 +55,7 @@ def render(self, context):
6055
Render the action as HTML.
6156
6257
Parameters:
63-
context: The template context
58+
context (dict): The template context
6459
"""
6560
# Enforce permissions
6661
user = context['request'].user
@@ -74,26 +69,16 @@ class LinkAction(PanelAction):
7469
"""
7570
A hyperlink (typically a button) within a panel to perform some associated action, such as adding an object.
7671
77-
Attributes:
78-
label: The default human-friendly button text
79-
button_class: Bootstrap CSS class for the button
80-
button_icon: Name of the button's MDI icon
72+
Parameters:
73+
view_name (str): Name of the view to which the action will link
74+
view_kwargs (dict): Additional keyword arguments to pass to `reverse()` when resolving the URL
75+
url_params (dict): A dictionary of arbitrary URL parameters to append to the action's URL. If the value of a key
76+
is a callable, it will be passed the current template context.
8177
"""
8278
template_name = 'ui/actions/link.html'
8379

8480
def __init__(self, view_name, view_kwargs=None, url_params=None, **kwargs):
85-
"""
86-
Initialize a new PanelAction.
87-
88-
Parameters:
89-
view_name: Name of the view to which the action will link
90-
view_kwargs: Additional keyword arguments to pass to the view when resolving its URL
91-
url_params: A dictionary of arbitrary URL parameters to append to the action's URL
92-
permissions: A list of permissions required to display the action
93-
label: The human-friendly button text
94-
"""
9581
super().__init__(**kwargs)
96-
9782
self.view_name = view_name
9883
self.view_kwargs = view_kwargs or {}
9984
self.url_params = url_params or {}
@@ -103,7 +88,7 @@ def get_url(self, context):
10388
Resolve the URL for the action from its view name and kwargs. Append any additional URL parameters.
10489
10590
Parameters:
106-
context: The template context
91+
context (dict): The template context
10792
"""
10893
url = reverse(self.view_name, kwargs=self.view_kwargs)
10994
if self.url_params:
@@ -127,19 +112,12 @@ def get_context(self, context):
127112
class AddObject(LinkAction):
128113
"""
129114
An action to add a new object.
130-
"""
131-
label = _('Add')
132-
button_icon = 'plus-thick'
133-
134-
def __init__(self, model, url_params=None, label=None):
135-
"""
136-
Initialize a new AddObject action.
137115
138-
Parameters:
139-
model: The dotted label of the model to be added (e.g. "dcim.site")
140-
url_params: A dictionary of arbitrary URL parameters to append to the resolved URL
141-
label: The human-friendly button text
142-
"""
116+
Parameters:
117+
model (str): The dotted label of the model to be added (e.g. "dcim.site")
118+
url_params (dict): A dictionary of arbitrary URL parameters to append to the resolved URL
119+
"""
120+
def __init__(self, model, url_params=None, **kwargs):
143121
# Resolve the model class from its app.name label
144122
try:
145123
app_label, model_name = model.split('.')
@@ -148,37 +126,29 @@ def __init__(self, model, url_params=None, label=None):
148126
raise ValueError(f"Invalid model label: {model}")
149127
view_name = get_viewname(model, 'add')
150128

151-
super().__init__(view_name=view_name, url_params=url_params, label=label)
129+
kwargs.setdefault('label', _('Add'))
130+
kwargs.setdefault('button_icon', 'plus-thick')
131+
kwargs.setdefault('permissions', [get_permission_for_model(model, 'add')])
152132

153-
# Require "add" permission on the model
154-
self.permissions = [get_permission_for_model(model, 'add')]
133+
super().__init__(view_name=view_name, url_params=url_params, **kwargs)
155134

156135

157136
class CopyContent(PanelAction):
158137
"""
159138
An action to copy the contents of a panel to the clipboard.
139+
140+
Parameters:
141+
target_id (str): The ID of the target element containing the content to be copied
160142
"""
161143
template_name = 'ui/actions/copy_content.html'
162-
label = _('Copy')
163-
button_icon = 'content-copy'
164144

165145
def __init__(self, target_id, **kwargs):
166-
"""
167-
Instantiate a new CopyContent action.
168-
169-
Parameters:
170-
target_id: The ID of the target element containing the content to be copied
171-
"""
146+
kwargs.setdefault('label', _('Copy'))
147+
kwargs.setdefault('button_icon', 'content-copy')
172148
super().__init__(**kwargs)
173149
self.target_id = target_id
174150

175151
def render(self, context):
176-
"""
177-
Render the action as HTML.
178-
179-
Parameters:
180-
context: The template context
181-
"""
182152
return render_to_string(self.template_name, {
183153
'target_id': self.target_id,
184154
'label': self.label,

0 commit comments

Comments
 (0)