Add layouts for DeviceType & ModuleTypeProfile
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run

This commit is contained in:
Jeremy Stretch 2025-11-04 20:06:18 -05:00
parent d5cec3723e
commit 1de41b4964
10 changed files with 162 additions and 7 deletions

View File

@ -112,3 +112,24 @@ class DeviceManagementPanel(panels.ObjectPanel):
label=_('Out-of-band IP'),
template_name='dcim/device/attrs/ipaddress.html',
)
class DeviceTypePanel(panels.ObjectPanel):
manufacturer = attrs.ObjectAttr('manufacturer', label=_('Manufacturer'), linkify=True)
model = attrs.TextAttr('model', label=_('Model'))
part_number = attrs.TextAttr('part_number', label=_('Part number'))
default_platform = attrs.ObjectAttr('default_platform', label=_('Default platform'), linkify=True)
description = attrs.TextAttr('description', label=_('Description'))
u_height = attrs.TextAttr('u_height', format_string='{}U', label=_('Height'))
exclude_from_utilization = attrs.BooleanAttr('exclude_from_utilization', label=_('Exclude from utilization'))
full_depth = attrs.BooleanAttr('is_full_depth', label=_('Full depth'))
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display', label=_('Weight'))
subdevice_role = attrs.ChoiceAttr('subdevice_role', label=_('Parent/child'))
airflow = attrs.ChoiceAttr('airflow', label=_('Airflow'))
front_image = attrs.ImageAttr('front_image', label=_('Front image'))
rear_image = attrs.ImageAttr('rear_image', label=_('Rear image'))
class ModuleTypeProfilePanel(panels.ObjectPanel):
name = attrs.TextAttr('name', label=_('Name'))
description = attrs.TextAttr('description', label=_('Description'))

View File

@ -21,7 +21,7 @@ from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
from netbox.object_actions import *
from netbox.ui import actions, layout
from netbox.ui.panels import (
CommentsPanel, NestedGroupObjectPanel, ObjectsTablePanel, OrganizationalObjectPanel, RelatedObjectsPanel,
CommentsPanel, JSONPanel, NestedGroupObjectPanel, ObjectsTablePanel, OrganizationalObjectPanel, RelatedObjectsPanel,
TemplatePanel,
)
from netbox.views import generic
@ -1308,6 +1308,18 @@ class DeviceTypeListView(generic.ObjectListView):
@register_model_view(DeviceType)
class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DeviceType.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.DeviceTypePanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
ImageAttachmentsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@ -1559,6 +1571,34 @@ class ModuleTypeProfileListView(generic.ObjectListView):
@register_model_view(ModuleTypeProfile)
class ModuleTypeProfileView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ModuleTypeProfile.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ModuleTypeProfilePanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
JSONPanel(field_name='schema', title=_('Schema')),
CustomFieldsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='dcim.ModuleType',
title=_('Module Types'),
filters={
'profile_id': lambda ctx: ctx['object'].pk,
},
actions=[
actions.AddObject(
'dcim.ModuleType',
url_params={
'profile': lambda ctx: ctx['object'].pk,
}
),
],
),
]
)
@register_model_view(ModuleTypeProfile, 'add', detail=False)

View File

@ -24,11 +24,12 @@ class PanelAction:
button_class: Bootstrap CSS class for the button
button_icon: Name of the button's MDI icon
"""
template_name = 'ui/action.html'
template_name = 'ui/actions/link.html'
label = None
button_class = 'primary'
button_icon = None
# TODO: Refactor URL parameters to AddObject
def __init__(self, view_name, view_kwargs=None, url_params=None, permissions=None, label=None):
"""
Initialize a new PanelAction.
@ -114,3 +115,30 @@ class AddObject(PanelAction):
# Require "add" permission on the model
self.permissions = [get_permission_for_model(model, 'add')]
class CopyContent:
"""
An action to copy the contents of a panel to the clipboard.
"""
template_name = 'ui/actions/copy_content.html'
label = _('Copy')
button_class = 'primary'
button_icon = 'content-copy'
def __init__(self, target_id):
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,
'button_class': self.button_class,
'button_icon': self.button_icon,
})

View File

@ -135,6 +135,20 @@ class ColorAttr(Attr):
})
class ImageAttr(Attr):
template_name = 'ui/attrs/image.html'
def render(self, obj, context=None):
context = context or {}
value = self._resolve_attr(obj, self.accessor)
if value in (None, ''):
return self.placeholder
return render_to_string(self.template_name, {
**context,
'value': value,
})
class ObjectAttr(Attr):
template_name = 'ui/attrs/object.html'

View File

@ -5,6 +5,7 @@ from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from netbox.ui import attrs
from netbox.ui.actions import CopyContent
from utilities.querydict import dict_to_querydict
from utilities.string import title
from utilities.templatetags.plugins import _get_registered_content
@ -12,6 +13,7 @@ from utilities.views import get_viewname
__all__ = (
'CommentsPanel',
'JSONPanel',
'NestedGroupObjectPanel',
'ObjectPanel',
'ObjectsTablePanel',
@ -34,7 +36,7 @@ class Panel(ABC):
"""
template_name = None
title = None
actions = []
actions = None
def __init__(self, title=None, actions=None):
"""
@ -46,8 +48,7 @@ class Panel(ABC):
"""
if title is not None:
self.title = title
if actions is not None:
self.actions = actions
self.actions = actions or []
def get_context(self, context):
"""
@ -251,6 +252,42 @@ class ObjectsTablePanel(Panel):
}
class JSONPanel(Panel):
"""
A panel which renders formatted JSON data.
"""
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
if copy_button:
self.actions.append(
CopyContent(f'panel_{field_name}'),
)
def get_context(self, context):
"""
Return the context data to be used when rendering the panel.
Parameters:
context: The template context
"""
return {
**super().get_context(context),
'data': getattr(context['object'], self.field_name),
'field_name': self.field_name,
}
class TemplatePanel(Panel):
"""
A panel which renders content using an HTML template.

View File

@ -0,0 +1,7 @@
{% load i18n %}
<a class="btn btn-ghost-{{ button_class }} btn-sm copy-content" data-clipboard-target="#{{ target_id }}" title="{% trans "Copy to clipboard" %}">
{% if button_icon %}
<i class="mdi mdi-{{ button_icon }}" aria-hidden="true"></i>
{% endif %}
{{ label }}
</a>

View File

@ -1,4 +1,4 @@
<a href="{{ url }}" class="btn btn-ghost-{{ button_class }} btn-sm">
<a {% if url %}href="{{ url }}" {% endif %}class="btn btn-ghost-{{ button_class }} btn-sm">
{% if button_icon %}
<i class="mdi mdi-{{ button_icon }}" aria-hidden="true"></i>
{% endif %}

View File

@ -1 +1 @@
{% checkmark object.desc_units %}
{% checkmark value %}

View File

@ -0,0 +1,3 @@
<a href="{{ value.url }}">
<img src="{{ value.url }}" alt="{{ value.name }}" class="img-fluid" />
</a>

View File

@ -0,0 +1,5 @@
{% extends "ui/panels/_base.html" %}
{% block panel_content %}
<pre id="panel_{{ field_name }}">{{ data|json }}</pre>
{% endblock panel_content %}