Compare commits

...

3 Commits

Author SHA1 Message Date
Jeremy Stretch
faf8554d2c Introduce CircuitTerminationPanel to replace generic panel 2026-04-01 17:15:05 -04:00
Jeremy Stretch
623ab55d5b Include permissions in TemplatedAttr context 2026-04-01 16:48:19 -04:00
Jeremy Stretch
c9073aca3c Misc cleanup 2026-04-01 15:49:55 -04:00
10 changed files with 149 additions and 49 deletions

View File

@@ -85,26 +85,6 @@ NetBox also includes a set of panels suite for specific uses, such as display ob
::: 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
@@ -119,9 +99,13 @@ The following classes are available to represent object attributes within an Obj
::: netbox.ui.panels.TemplatePanel
::: netbox.ui.panels.TextCodePanel
::: netbox.ui.panels.ContextTablePanel
::: netbox.ui.panels.PluginContentPanel
## Panel Actions
### 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.
@@ -146,3 +130,60 @@ panels.ObjectsTablePanel(
::: netbox.ui.actions.AddObject
::: netbox.ui.actions.CopyContent
## 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.ChoiceAttr` | A selection from a set of choices |
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
| `netbox.ui.attrs.DateTimeAttr` | A date or datetime value |
| `netbox.ui.attrs.GenericForeignKeyAttr` | A related object via a generic foreign key |
| `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 (includes ancestors) |
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
| `netbox.ui.attrs.RelatedObjectListAttr` | A list of related objects |
| `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.attrs.ObjectAttribute
::: netbox.ui.attrs.AddressAttr
::: netbox.ui.attrs.BooleanAttr
::: netbox.ui.attrs.ChoiceAttr
::: netbox.ui.attrs.ColorAttr
::: netbox.ui.attrs.DateTimeAttr
::: netbox.ui.attrs.GenericForeignKeyAttr
::: netbox.ui.attrs.GPSCoordinatesAttr
::: netbox.ui.attrs.ImageAttr
::: netbox.ui.attrs.NestedObjectAttr
::: netbox.ui.attrs.NumericAttr
::: netbox.ui.attrs.RelatedObjectAttr
::: netbox.ui.attrs.RelatedObjectListAttr
::: netbox.ui.attrs.TemplatedAttr
::: netbox.ui.attrs.TextAttr
::: netbox.ui.attrs.TimezoneAttr
::: netbox.ui.attrs.UtilizationAttr

View File

@@ -58,6 +58,26 @@ class CircuitGroupAssignmentsPanel(panels.ObjectsTablePanel):
)
class CircuitTerminationPanel(panels.ObjectAttributesPanel):
title = _('Circuit Termination')
circuit = attrs.RelatedObjectAttr('circuit', linkify=True)
provider = attrs.RelatedObjectAttr('circuit.provider', linkify=True)
termination = attrs.GenericForeignKeyAttr('termination', linkify=True, label=_('Termination point'))
connection = attrs.TemplatedAttr(
'pk',
template_name='circuits/circuit_termination/attrs/connection.html',
label=_('Connection'),
)
speed = attrs.TemplatedAttr(
'port_speed',
template_name='circuits/circuit_termination/attrs/speed.html',
label=_('Speed'),
)
xconnect_id = attrs.TextAttr('xconnect_id', label=_('Cross-Connect'), style='font-monospace')
pp_info = attrs.TextAttr('pp_info', label=_('Patch Panel/Port'))
description = attrs.TextAttr('description')
class CircuitGroupPanel(panels.OrganizationalObjectPanel):
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')

View File

@@ -8,7 +8,6 @@ from netbox.ui import actions, layout
from netbox.ui.panels import (
CommentsPanel,
ObjectsTablePanel,
Panel,
RelatedObjectsPanel,
)
from netbox.views import generic
@@ -508,10 +507,7 @@ class CircuitTerminationView(generic.ObjectView):
queryset = CircuitTermination.objects.all()
layout = layout.SimpleLayout(
left_panels=[
Panel(
template_name='circuits/panels/circuit_termination.html',
title=_('Circuit Termination'),
)
panels.CircuitTerminationPanel(),
],
right_panels=[
CustomFieldsPanel(),

View File

@@ -59,7 +59,7 @@ class PanelAction:
"""
# Enforce permissions
user = context['request'].user
if not user.has_perms(self.permissions):
if self.permissions and not user.has_perms(self.permissions):
return ''
return render_to_string(self.template_name, self.get_context(context))

View File

@@ -506,6 +506,7 @@ class TemplatedAttr(ObjectAttribute):
def get_context(self, obj, context):
return {
**context,
**self.context,
'object': obj,
}

View File

@@ -62,7 +62,7 @@ class SimpleLayout(Layout):
"""
A layout with one row of two columns and a second row with one column.
Plugin content registered for `left_page`, `right_page`, or `full_width_path` is included automatically. Most object
Plugin content registered for `left_page`, `right_page`, or `full_width_page` is included automatically. Most object
views in NetBox utilize this layout.
```

View File

@@ -187,7 +187,7 @@ class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta):
'attrs': [
{
'label': attr.label or self._name_to_label(name),
'value': attr.render(ctx['object'], {'name': name}),
'value': attr.render(ctx['object'], {'name': name, 'perms': ctx['perms']}),
} for name, attr in self._attrs.items() if name in attr_names
],
}
@@ -225,9 +225,10 @@ class CommentsPanel(ObjectPanel):
self.field_name = field_name
def get_context(self, context):
ctx = super().get_context(context)
return {
**super().get_context(context),
'comments': getattr(context['object'], self.field_name),
**ctx,
'comments': getattr(ctx['object'], self.field_name),
}
@@ -249,9 +250,10 @@ class JSONPanel(ObjectPanel):
self.actions.append(CopyContent(f'panel_{field_name}'))
def get_context(self, context):
ctx = super().get_context(context)
return {
**super().get_context(context),
'data': getattr(context['object'], self.field_name),
**ctx,
'data': getattr(ctx['object'], self.field_name),
'field_name': self.field_name,
}

View File

@@ -0,0 +1,48 @@
{% load helpers i18n %}
{% if object.mark_connected %}
<div>
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
<span class="text-muted">{% trans "Marked as connected" %}</span>
</div>
{% elif object.cable %}
<div>
<a class="d-block d-md-inline" href="{{ object.cable.get_absolute_url }}">{{ object.cable }}</a> {% trans "to" %}
{% for peer in object.link_peers %}
{% if peer.device %}
{{ peer.device|linkify }}<br/>
{% elif peer.circuit %}
{{ peer.circuit|linkify }}<br/>
{% endif %}
{{ peer|linkify }}{% if not forloop.last %},{% endif %}
{% endfor %}
</div>
<div class="mt-1">
<a href="{% url 'circuits:circuittermination_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> {% trans "Trace" %}
</a>
{% if perms.dcim.change_cable %}
<a href="{% url 'dcim:cable_edit' pk=object.cable.pk %}?return_url={{ object.circuit.get_absolute_url }}" title="{% trans "Edit cable" %}" class="btn btn-sm btn-warning">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Edit" %}
</a>
{% endif %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=object.cable.pk %}?return_url={{ object.circuit.get_absolute_url }}" title="{% trans "Remove cable" %}" class="btn btn-sm btn-danger">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {% trans "Disconnect" %}
</a>
{% endif %}
</div>
{% elif perms.dcim.add_cable %}
<div class="dropdown">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">{% trans "Interface" %}</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">{% trans "Front Port" %}</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">{% trans "Rear Port" %}</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">{% trans "Circuit Termination" %}</a></li>
</ul>
</div>
{% else %}
{{ ''|placeholder }}
{% endif %}

View File

@@ -0,0 +1,8 @@
{% load helpers i18n %}
{% if object.upstream_speed %}
<i class="mdi mdi-arrow-down-bold" title="{% trans "Downstream" %}"></i> {{ object.port_speed|humanize_speed }}
<i class="mdi mdi-slash-forward"></i>
<i class="mdi mdi-arrow-up-bold" title="{% trans "Upstream" %}"></i> {{ object.upstream_speed|humanize_speed }}
{% else %}
{{ object.port_speed|humanize_speed }}
{% endif %}

View File

@@ -1,16 +0,0 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Circuit" %}</th>
<td>{{ object.circuit|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Provider" %}</th>
<td>{{ object.circuit.provider|linkify|placeholder }}</td>
</tr>
{% include 'circuits/inc/circuit_termination_fields.html' with termination=object %}
</table>
{% endblock panel_content %}