diff --git a/netbox/dcim/ui/panels.py b/netbox/dcim/ui/panels.py
index d26dfda45..d6309c600 100644
--- a/netbox/dcim/ui/panels.py
+++ b/netbox/dcim/ui/panels.py
@@ -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'))
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 3d2dad903..051d2867d 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -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)
diff --git a/netbox/netbox/ui/actions.py b/netbox/netbox/ui/actions.py
index 8a3d7ecb1..a94520bdc 100644
--- a/netbox/netbox/ui/actions.py
+++ b/netbox/netbox/ui/actions.py
@@ -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,
+ })
diff --git a/netbox/netbox/ui/attrs.py b/netbox/netbox/ui/attrs.py
index 4df8f64e1..72c5dba5f 100644
--- a/netbox/netbox/ui/attrs.py
+++ b/netbox/netbox/ui/attrs.py
@@ -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'
diff --git a/netbox/netbox/ui/panels.py b/netbox/netbox/ui/panels.py
index b2f7ad2eb..eefbde5b4 100644
--- a/netbox/netbox/ui/panels.py
+++ b/netbox/netbox/ui/panels.py
@@ -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.
diff --git a/netbox/templates/ui/actions/copy_content.html b/netbox/templates/ui/actions/copy_content.html
new file mode 100644
index 000000000..67f54354b
--- /dev/null
+++ b/netbox/templates/ui/actions/copy_content.html
@@ -0,0 +1,7 @@
+{% load i18n %}
+
+ {% if button_icon %}
+
+ {% endif %}
+ {{ label }}
+
diff --git a/netbox/templates/ui/action.html b/netbox/templates/ui/actions/link.html
similarity index 56%
rename from netbox/templates/ui/action.html
rename to netbox/templates/ui/actions/link.html
index c61357312..11c6b6da9 100644
--- a/netbox/templates/ui/action.html
+++ b/netbox/templates/ui/actions/link.html
@@ -1,4 +1,4 @@
-
+
{% if button_icon %}
{% endif %}
diff --git a/netbox/templates/ui/attrs/boolean.html b/netbox/templates/ui/attrs/boolean.html
index a724d687b..a7087c94f 100644
--- a/netbox/templates/ui/attrs/boolean.html
+++ b/netbox/templates/ui/attrs/boolean.html
@@ -1 +1 @@
-{% checkmark object.desc_units %}
+{% checkmark value %}
diff --git a/netbox/templates/ui/attrs/image.html b/netbox/templates/ui/attrs/image.html
new file mode 100644
index 000000000..3c10113c4
--- /dev/null
+++ b/netbox/templates/ui/attrs/image.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/netbox/templates/ui/panels/json.html b/netbox/templates/ui/panels/json.html
new file mode 100644
index 000000000..36d3d4d1a
--- /dev/null
+++ b/netbox/templates/ui/panels/json.html
@@ -0,0 +1,5 @@
+{% extends "ui/panels/_base.html" %}
+
+{% block panel_content %}
+
{{ data|json }}
+{% endblock panel_content %}