diff --git a/netbox/extras/ui/panels.py b/netbox/extras/ui/panels.py
index 4e8532a64..d347089e0 100644
--- a/netbox/extras/ui/panels.py
+++ b/netbox/extras/ui/panels.py
@@ -2,16 +2,55 @@ from django.contrib.contenttypes.models import ContentType
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
-from netbox.ui import actions, panels
+from netbox.ui import actions, attrs, panels
from utilities.data import resolve_attr_path
__all__ = (
+ 'ConfigContextAssignmentPanel',
+ 'ConfigContextPanel',
+ 'ConfigContextProfilePanel',
+ 'ConfigTemplatePanel',
+ 'CustomFieldBehaviorPanel',
+ 'CustomFieldChoiceSetChoicesPanel',
+ 'CustomFieldChoiceSetPanel',
+ 'CustomFieldObjectTypesPanel',
+ 'CustomFieldPanel',
+ 'CustomFieldRelatedObjectsPanel',
+ 'CustomFieldValidationPanel',
'CustomFieldsPanel',
+ 'CustomLinkPanel',
+ 'EventRuleActionPanel',
+ 'EventRuleEventTypesPanel',
+ 'EventRulePanel',
+ 'ExportTemplatePanel',
+ 'ImageAttachmentFilePanel',
+ 'ImageAttachmentImagePanel',
+ 'ImageAttachmentPanel',
'ImageAttachmentsPanel',
+ 'JournalEntryPanel',
+ 'NotificationGroupGroupsPanel',
+ 'NotificationGroupPanel',
+ 'NotificationGroupUsersPanel',
+ 'ObjectTypesPanel',
+ 'SavedFilterObjectTypesPanel',
+ 'SavedFilterPanel',
+ 'TableConfigColumnsPanel',
+ 'TableConfigOrderingPanel',
+ 'TableConfigPanel',
+ 'TagItemTypesPanel',
+ 'TagObjectTypesPanel',
+ 'TagPanel',
'TagsPanel',
+ 'WebhookHTTPPanel',
+ 'WebhookPanel',
+ 'WebhookSSLPanel',
)
+#
+# Generic panels
+#
+
class CustomFieldsPanel(panels.ObjectPanel):
"""
A panel showing the value of all custom fields defined on an object.
@@ -73,3 +112,403 @@ class TagsPanel(panels.ObjectPanel):
**super().get_context(context),
'object': resolve_attr_path(context, self.accessor),
}
+
+
+class ObjectTypesPanel(panels.ObjectPanel):
+ """
+ A panel listing the object types assigned to the object.
+ """
+ template_name = 'extras/panels/object_types.html'
+ title = _('Object Types')
+
+
+#
+# CustomField panels
+#
+
+class CustomFieldPanel(panels.ObjectAttributesPanel):
+ title = _('Custom Field')
+
+ name = attrs.TextAttr('name')
+ type = attrs.TemplatedAttr('type', label=_('Type'), template_name='extras/customfield/attrs/type.html')
+ label = attrs.TextAttr('label')
+ group_name = attrs.TextAttr('group_name', label=_('Group name'))
+ description = attrs.TextAttr('description')
+ required = attrs.BooleanAttr('required')
+ unique = attrs.BooleanAttr('unique', label=_('Must be unique'))
+ is_cloneable = attrs.BooleanAttr('is_cloneable', label=_('Cloneable'))
+ choice_set = attrs.TemplatedAttr(
+ 'choice_set',
+ template_name='extras/customfield/attrs/choice_set.html',
+ )
+ default = attrs.TextAttr('default', label=_('Default value'))
+ related_object_filter = attrs.TemplatedAttr(
+ 'related_object_filter',
+ template_name='extras/customfield/attrs/related_object_filter.html',
+ )
+
+
+class CustomFieldBehaviorPanel(panels.ObjectAttributesPanel):
+ title = _('Behavior')
+
+ search_weight = attrs.TemplatedAttr(
+ 'search_weight',
+ template_name='extras/customfield/attrs/search_weight.html',
+ )
+ filter_logic = attrs.ChoiceAttr('filter_logic')
+ weight = attrs.NumericAttr('weight', label=_('Display weight'))
+ ui_visible = attrs.ChoiceAttr('ui_visible', label=_('UI visible'))
+ ui_editable = attrs.ChoiceAttr('ui_editable', label=_('UI editable'))
+
+
+class CustomFieldValidationPanel(panels.ObjectAttributesPanel):
+ title = _('Validation Rules')
+
+ validation_minimum = attrs.NumericAttr('validation_minimum', label=_('Minimum value'))
+ validation_maximum = attrs.NumericAttr('validation_maximum', label=_('Maximum value'))
+ validation_regex = attrs.TextAttr(
+ 'validation_regex',
+ label=_('Regular expression'),
+ style='font-monospace',
+ )
+
+
+class CustomFieldObjectTypesPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/object_types.html'
+ title = _('Object Types')
+
+
+class CustomFieldRelatedObjectsPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/customfield_related_objects.html'
+ title = _('Related Objects')
+
+ def get_context(self, context):
+ return {
+ **super().get_context(context),
+ 'related_models': context.get('related_models'),
+ }
+
+
+#
+# CustomFieldChoiceSet panels
+#
+
+class CustomFieldChoiceSetPanel(panels.ObjectAttributesPanel):
+ title = _('Custom Field Choice Set')
+
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+ base_choices = attrs.ChoiceAttr('base_choices')
+ order_alphabetically = attrs.BooleanAttr('order_alphabetically')
+ choices_for = attrs.RelatedObjectListAttr('choices_for', linkify=True, label=_('Used by'))
+
+
+class CustomFieldChoiceSetChoicesPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/customfieldchoiceset_choices.html'
+
+ def get_context(self, context):
+ obj = context.get('object')
+ total = len(obj.choices) if obj else 0
+ return {
+ **super().get_context(context),
+ 'title': f'{_("Choices")} ({total})',
+ 'choices': context.get('choices'),
+ }
+
+
+#
+# CustomLink panels
+#
+
+class CustomLinkPanel(panels.ObjectAttributesPanel):
+ title = _('Custom Link')
+
+ name = attrs.TextAttr('name')
+ enabled = attrs.BooleanAttr('enabled')
+ group_name = attrs.TextAttr('group_name')
+ weight = attrs.NumericAttr('weight')
+ button_class = attrs.ChoiceAttr('button_class')
+ new_window = attrs.BooleanAttr('new_window')
+
+
+#
+# ExportTemplate panels
+#
+
+class ExportTemplatePanel(panels.ObjectAttributesPanel):
+ title = _('Export Template')
+
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+ mime_type = attrs.TextAttr('mime_type', label=_('MIME type'))
+ file_name = attrs.TextAttr('file_name')
+ file_extension = attrs.TextAttr('file_extension')
+ as_attachment = attrs.BooleanAttr('as_attachment', label=_('Attachment'))
+
+
+#
+# SavedFilter panels
+#
+
+class SavedFilterPanel(panels.ObjectAttributesPanel):
+ title = _('Saved Filter')
+
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+ user = attrs.TextAttr('user')
+ enabled = attrs.BooleanAttr('enabled')
+ shared = attrs.BooleanAttr('shared')
+ weight = attrs.NumericAttr('weight')
+
+
+class SavedFilterObjectTypesPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/savedfilter_object_types.html'
+ title = _('Assigned Models')
+
+
+#
+# TableConfig panels
+#
+
+class TableConfigPanel(panels.ObjectAttributesPanel):
+ title = _('Table Config')
+
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+ object_type = attrs.TextAttr('object_type')
+ table = attrs.TextAttr('table')
+ user = attrs.TextAttr('user')
+ enabled = attrs.BooleanAttr('enabled')
+ shared = attrs.BooleanAttr('shared')
+ weight = attrs.NumericAttr('weight')
+
+
+class TableConfigColumnsPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/tableconfig_columns.html'
+ title = _('Columns Displayed')
+
+ def get_context(self, context):
+ return {
+ **super().get_context(context),
+ 'columns': context.get('columns'),
+ }
+
+
+class TableConfigOrderingPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/tableconfig_ordering.html'
+ title = _('Ordering')
+
+ def get_context(self, context):
+ return {
+ **super().get_context(context),
+ 'columns': context.get('columns'),
+ }
+
+
+#
+# NotificationGroup panels
+#
+
+class NotificationGroupPanel(panels.ObjectAttributesPanel):
+ title = _('Notification Group')
+
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+
+
+class NotificationGroupGroupsPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/notificationgroup_groups.html'
+ title = _('Groups')
+
+
+class NotificationGroupUsersPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/notificationgroup_users.html'
+ title = _('Users')
+
+
+#
+# Webhook panels
+#
+
+class WebhookPanel(panels.ObjectAttributesPanel):
+ title = _('Webhook')
+
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+
+
+class WebhookHTTPPanel(panels.ObjectAttributesPanel):
+ title = _('HTTP Request')
+
+ http_method = attrs.ChoiceAttr('http_method', label=_('HTTP method'))
+ payload_url = attrs.TextAttr('payload_url', label=_('Payload URL'), style='font-monospace')
+ http_content_type = attrs.TextAttr('http_content_type', label=_('HTTP content type'))
+ secret = attrs.TextAttr('secret')
+
+
+class WebhookSSLPanel(panels.ObjectAttributesPanel):
+ title = _('SSL')
+
+ ssl_verification = attrs.BooleanAttr('ssl_verification', label=_('SSL verification'))
+ ca_file_path = attrs.TextAttr('ca_file_path', label=_('CA file path'))
+
+
+#
+# EventRule panels
+#
+
+class EventRulePanel(panels.ObjectAttributesPanel):
+ title = _('Event Rule')
+
+ name = attrs.TextAttr('name')
+ enabled = attrs.BooleanAttr('enabled')
+ description = attrs.TextAttr('description')
+
+
+class EventRuleEventTypesPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/eventrule_event_types.html'
+ title = _('Event Types')
+
+ def get_context(self, context):
+ return {
+ **super().get_context(context),
+ 'registry': context.get('registry'),
+ }
+
+
+class EventRuleActionPanel(panels.ObjectAttributesPanel):
+ title = _('Action')
+
+ action_type = attrs.ChoiceAttr('action_type', label=_('Type'))
+ action_object = attrs.RelatedObjectAttr('action_object', linkify=True, label=_('Object'))
+ action_data = attrs.TemplatedAttr(
+ 'action_data',
+ label=_('Data'),
+ template_name='extras/eventrule/attrs/action_data.html',
+ )
+
+
+#
+# Tag panels
+#
+
+class TagPanel(panels.ObjectAttributesPanel):
+ title = _('Tag')
+
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+ color = attrs.ColorAttr('color')
+ weight = attrs.NumericAttr('weight')
+ tagged_items = attrs.TemplatedAttr(
+ 'extras_taggeditem_items',
+ template_name='extras/tag/attrs/tagged_item_count.html',
+ )
+
+
+class TagObjectTypesPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/tag_object_types.html'
+ title = _('Allowed Object Types')
+
+
+class TagItemTypesPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/tag_item_types.html'
+ title = _('Tagged Item Types')
+
+ def get_context(self, context):
+ return {
+ **super().get_context(context),
+ 'object_types': context.get('object_types'),
+ }
+
+
+#
+# ConfigContextProfile panels
+#
+
+class ConfigContextProfilePanel(panels.ObjectAttributesPanel):
+ title = _('Config Context Profile')
+
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+
+
+#
+# ConfigContext panels
+#
+
+class ConfigContextPanel(panels.ObjectAttributesPanel):
+ title = _('Config Context')
+
+ name = attrs.TextAttr('name')
+ weight = attrs.NumericAttr('weight')
+ profile = attrs.RelatedObjectAttr('profile', linkify=True)
+ description = attrs.TextAttr('description')
+ is_active = attrs.BooleanAttr('is_active', label=_('Active'))
+
+
+class ConfigContextAssignmentPanel(panels.ObjectPanel):
+ template_name = 'extras/panels/configcontext_assignment.html'
+ title = _('Assignment')
+
+ def get_context(self, context):
+ return {
+ **super().get_context(context),
+ 'assigned_objects': context.get('assigned_objects'),
+ }
+
+
+#
+# ConfigTemplate panels
+#
+
+class ConfigTemplatePanel(panels.ObjectAttributesPanel):
+ title = _('Config Template')
+
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+ mime_type = attrs.TextAttr('mime_type', label=_('MIME type'))
+ file_name = attrs.TextAttr('file_name')
+ file_extension = attrs.TextAttr('file_extension')
+ as_attachment = attrs.BooleanAttr('as_attachment', label=_('Attachment'))
+ data_source = attrs.RelatedObjectAttr('data_source', linkify=True)
+ data_file = attrs.TemplatedAttr(
+ 'data_path',
+ template_name='extras/configtemplate/attrs/data_file.html',
+ )
+ data_synced = attrs.DateTimeAttr('data_synced')
+ auto_sync_enabled = attrs.BooleanAttr('auto_sync_enabled')
+
+
+#
+# ImageAttachment panels
+#
+
+class ImageAttachmentPanel(panels.ObjectAttributesPanel):
+ title = _('Image Attachment')
+
+ parent = attrs.RelatedObjectAttr('parent', linkify=True, label=_('Parent object'))
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+
+
+class ImageAttachmentFilePanel(panels.ObjectPanel):
+ template_name = 'extras/panels/imageattachment_file.html'
+ title = _('File')
+
+
+class ImageAttachmentImagePanel(panels.ObjectPanel):
+ template_name = 'extras/panels/imageattachment_image.html'
+ title = _('Image')
+
+
+#
+# JournalEntry panels
+#
+
+class JournalEntryPanel(panels.ObjectAttributesPanel):
+ title = _('Journal Entry')
+
+ assigned_object = attrs.RelatedObjectAttr('assigned_object', linkify=True, label=_('Object'))
+ created = attrs.DateTimeAttr('created', spec='minutes')
+ created_by = attrs.TextAttr('created_by')
+ kind = attrs.ChoiceAttr('kind')
diff --git a/netbox/extras/views.py b/netbox/extras/views.py
index 05a3dbb13..99e8d976a 100644
--- a/netbox/extras/views.py
+++ b/netbox/extras/views.py
@@ -10,7 +10,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.utils.module_loading import import_string
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
from django.views.generic import View
from jinja2.exceptions import TemplateError
@@ -23,6 +23,14 @@ from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class
from extras.utils import SharedObjectViewMixin
from netbox.object_actions import *
+from netbox.ui import layout
+from netbox.ui.panels import (
+ CommentsPanel,
+ ContextTablePanel,
+ JSONPanel,
+ TemplatePanel,
+ TextCodePanel,
+)
from netbox.views import generic
from netbox.views.generic.mixins import TableMixin
from utilities.forms import ConfirmationForm, get_field_value
@@ -40,6 +48,7 @@ from . import filtersets, forms, tables
from .constants import LOG_LEVEL_RANK
from .models import *
from .tables import ReportResultsTable, ScriptJobTable, ScriptResultsTable
+from .ui import panels
#
# Custom fields
@@ -57,6 +66,18 @@ class CustomFieldListView(generic.ObjectListView):
@register_model_view(CustomField)
class CustomFieldView(generic.ObjectView):
queryset = CustomField.objects.select_related('choice_set')
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.CustomFieldPanel(),
+ panels.CustomFieldBehaviorPanel(),
+ CommentsPanel(),
+ ],
+ right_panels=[
+ panels.CustomFieldObjectTypesPanel(),
+ panels.CustomFieldValidationPanel(),
+ panels.CustomFieldRelatedObjectsPanel(),
+ ],
+ )
def get_extra_context(self, request, instance):
related_models = ()
@@ -128,6 +149,14 @@ class CustomFieldChoiceSetListView(generic.ObjectListView):
@register_model_view(CustomFieldChoiceSet)
class CustomFieldChoiceSetView(generic.ObjectView):
queryset = CustomFieldChoiceSet.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.CustomFieldChoiceSetPanel(),
+ ],
+ right_panels=[
+ panels.CustomFieldChoiceSetChoicesPanel(),
+ ],
+ )
def get_extra_context(self, request, instance):
@@ -203,6 +232,16 @@ class CustomLinkListView(generic.ObjectListView):
@register_model_view(CustomLink)
class CustomLinkView(generic.ObjectView):
queryset = CustomLink.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.CustomLinkPanel(),
+ panels.ObjectTypesPanel(title=_('Assigned Models')),
+ ],
+ right_panels=[
+ TextCodePanel('link_text', title=_('Link Text')),
+ TextCodePanel('link_url', title=_('Link URL')),
+ ],
+ )
@register_model_view(CustomLink, 'add', detail=False)
@@ -260,6 +299,19 @@ class ExportTemplateListView(generic.ObjectListView):
@register_model_view(ExportTemplate)
class ExportTemplateView(generic.ObjectView):
queryset = ExportTemplate.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.ExportTemplatePanel(),
+ TemplatePanel('core/inc/datafile_panel.html'),
+ ],
+ right_panels=[
+ panels.ObjectTypesPanel(title=_('Assigned Models')),
+ JSONPanel('environment_params', title=_('Environment Parameters')),
+ ],
+ bottom_panels=[
+ TextCodePanel('template_code', title=_('Template'), show_sync_warning=True),
+ ],
+ )
@register_model_view(ExportTemplate, 'add', detail=False)
@@ -321,6 +373,15 @@ class SavedFilterListView(SharedObjectViewMixin, generic.ObjectListView):
@register_model_view(SavedFilter)
class SavedFilterView(SharedObjectViewMixin, generic.ObjectView):
queryset = SavedFilter.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.SavedFilterPanel(),
+ panels.SavedFilterObjectTypesPanel(),
+ ],
+ right_panels=[
+ JSONPanel('parameters', title=_('Parameters')),
+ ],
+ )
@register_model_view(SavedFilter, 'add', detail=False)
@@ -383,6 +444,15 @@ class TableConfigListView(SharedObjectViewMixin, generic.ObjectListView):
@register_model_view(TableConfig)
class TableConfigView(SharedObjectViewMixin, generic.ObjectView):
queryset = TableConfig.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.TableConfigPanel(),
+ ],
+ right_panels=[
+ panels.TableConfigColumnsPanel(),
+ panels.TableConfigOrderingPanel(),
+ ],
+ )
def get_extra_context(self, request, instance):
table = instance.table_class([])
@@ -476,6 +546,15 @@ class NotificationGroupListView(generic.ObjectListView):
@register_model_view(NotificationGroup)
class NotificationGroupView(generic.ObjectView):
queryset = NotificationGroup.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.NotificationGroupPanel(),
+ ],
+ right_panels=[
+ panels.NotificationGroupGroupsPanel(),
+ panels.NotificationGroupUsersPanel(),
+ ],
+ )
@register_model_view(NotificationGroup, 'add', detail=False)
@@ -660,6 +739,19 @@ class WebhookListView(generic.ObjectListView):
@register_model_view(Webhook)
class WebhookView(generic.ObjectView):
queryset = Webhook.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.WebhookPanel(),
+ panels.WebhookHTTPPanel(),
+ panels.WebhookSSLPanel(),
+ ],
+ right_panels=[
+ TextCodePanel('additional_headers', title=_('Additional Headers')),
+ TextCodePanel('body_template', title=_('Body Template')),
+ panels.CustomFieldsPanel(),
+ panels.TagsPanel(),
+ ],
+ )
@register_model_view(Webhook, 'add', detail=False)
@@ -716,6 +808,19 @@ class EventRuleListView(generic.ObjectListView):
@register_model_view(EventRule)
class EventRuleView(generic.ObjectView):
queryset = EventRule.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.EventRulePanel(),
+ panels.ObjectTypesPanel(),
+ panels.EventRuleEventTypesPanel(),
+ ],
+ right_panels=[
+ JSONPanel('conditions', title=_('Conditions')),
+ panels.EventRuleActionPanel(),
+ panels.CustomFieldsPanel(),
+ panels.TagsPanel(),
+ ],
+ )
@register_model_view(EventRule, 'add', detail=False)
@@ -774,6 +879,18 @@ class TagListView(generic.ObjectListView):
@register_model_view(Tag)
class TagView(generic.ObjectView):
queryset = Tag.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.TagPanel(),
+ ],
+ right_panels=[
+ panels.TagObjectTypesPanel(),
+ panels.TagItemTypesPanel(),
+ ],
+ bottom_panels=[
+ ContextTablePanel('taggeditem_table', title=_('Tagged Objects')),
+ ],
+ )
def get_extra_context(self, request, instance):
tagged_items = TaggedItem.objects.filter(tag=instance)
@@ -853,6 +970,18 @@ class ConfigContextProfileListView(generic.ObjectListView):
@register_model_view(ConfigContextProfile)
class ConfigContextProfileView(generic.ObjectView):
queryset = ConfigContextProfile.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.ConfigContextProfilePanel(),
+ TemplatePanel('core/inc/datafile_panel.html'),
+ panels.CustomFieldsPanel(),
+ panels.TagsPanel(),
+ CommentsPanel(),
+ ],
+ right_panels=[
+ JSONPanel('schema', title=_('JSON Schema')),
+ ],
+ )
@register_model_view(ConfigContextProfile, 'add', detail=False)
@@ -915,6 +1044,16 @@ class ConfigContextListView(generic.ObjectListView):
@register_model_view(ConfigContext)
class ConfigContextView(generic.ObjectView):
queryset = ConfigContext.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.ConfigContextPanel(),
+ TemplatePanel('core/inc/datafile_panel.html'),
+ panels.ConfigContextAssignmentPanel(),
+ ],
+ right_panels=[
+ TemplatePanel('extras/panels/configcontext_data.html'),
+ ],
+ )
def get_extra_context(self, request, instance):
# Gather assigned objects for parsing in the template
@@ -1034,6 +1173,18 @@ class ConfigTemplateListView(generic.ObjectListView):
@register_model_view(ConfigTemplate)
class ConfigTemplateView(generic.ObjectView):
queryset = ConfigTemplate.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.ConfigTemplatePanel(),
+ panels.TagsPanel(),
+ ],
+ right_panels=[
+ JSONPanel('environment_params', title=_('Environment Parameters')),
+ ],
+ bottom_panels=[
+ TextCodePanel('template_code', title=_('Template'), show_sync_warning=True),
+ ],
+ )
@register_model_view(ConfigTemplate, 'add', detail=False)
@@ -1151,6 +1302,17 @@ class ImageAttachmentListView(generic.ObjectListView):
@register_model_view(ImageAttachment)
class ImageAttachmentView(generic.ObjectView):
queryset = ImageAttachment.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.ImageAttachmentPanel(),
+ ],
+ right_panels=[
+ panels.ImageAttachmentFilePanel(),
+ ],
+ bottom_panels=[
+ panels.ImageAttachmentImagePanel(),
+ ],
+ )
@register_model_view(ImageAttachment, 'add', detail=False)
@@ -1215,6 +1377,16 @@ class JournalEntryListView(generic.ObjectListView):
@register_model_view(JournalEntry)
class JournalEntryView(generic.ObjectView):
queryset = JournalEntry.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.JournalEntryPanel(),
+ panels.CustomFieldsPanel(),
+ panels.TagsPanel(),
+ ],
+ right_panels=[
+ CommentsPanel(),
+ ],
+ )
@register_model_view(JournalEntry, 'add', detail=False)
diff --git a/netbox/netbox/ui/panels.py b/netbox/netbox/ui/panels.py
index b1ab1e411..f58e98449 100644
--- a/netbox/netbox/ui/panels.py
+++ b/netbox/netbox/ui/panels.py
@@ -23,6 +23,7 @@ __all__ = (
'PluginContentPanel',
'RelatedObjectsPanel',
'TemplatePanel',
+ 'TextCodePanel',
)
@@ -329,6 +330,25 @@ class TemplatePanel(Panel):
return render_to_string(self.template_name, context.flatten())
+class TextCodePanel(ObjectPanel):
+ """
+ A panel displaying a text field as a pre-formatted code block.
+ """
+ template_name = 'ui/panels/text_code.html'
+
+ def __init__(self, field_name, show_sync_warning=False, **kwargs):
+ super().__init__(**kwargs)
+ self.field_name = field_name
+ self.show_sync_warning = show_sync_warning
+
+ def get_context(self, context):
+ return {
+ **super().get_context(context),
+ 'show_sync_warning': self.show_sync_warning,
+ 'value': getattr(context.get('object'), self.field_name, None),
+ }
+
+
class PluginContentPanel(Panel):
"""
A panel which displays embedded plugin content.
diff --git a/netbox/templates/extras/configcontext.html b/netbox/templates/extras/configcontext.html
index 7afe73553..f15e1d050 100644
--- a/netbox/templates/extras/configcontext.html
+++ b/netbox/templates/extras/configcontext.html
@@ -1,62 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load static %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Weight" %} |
- {{ object.weight }} |
-
-
- | {% trans "Profile" %} |
- {{ object.profile|linkify|placeholder }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "Active" %} |
- {% checkmark object.is_active %} |
-
-
-
- {% include 'core/inc/datafile_panel.html' %}
-
-
-
- {% for title, objects in assigned_objects %}
-
- | {{ title }} |
-
-
- {% for object in objects %}
- - {{ object|linkify }}
- {% empty %}
- - {% trans "None" %}
- {% endfor %}
-
- |
-
- {% endfor %}
-
-
-
-
- {% include 'inc/sync_warning.html' %}
-
- {% include 'extras/inc/configcontext_data.html' with title="Data" data=object.data format=format copyid="data" %}
-
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/configcontextprofile.html b/netbox/templates/extras/configcontextprofile.html
index 658724196..f15e1d050 100644
--- a/netbox/templates/extras/configcontextprofile.html
+++ b/netbox/templates/extras/configcontextprofile.html
@@ -1,39 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load static %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
- {% include 'core/inc/datafile_panel.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/comments.html' %}
-
-
-
-
-
{{ object.schema|json }}
-
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/configtemplate.html b/netbox/templates/extras/configtemplate.html
index a2e31baed..f15e1d050 100644
--- a/netbox/templates/extras/configtemplate.html
+++ b/netbox/templates/extras/configtemplate.html
@@ -1,100 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "MIME Type" %} |
- {{ object.mime_type|placeholder }} |
-
-
- | {% trans "File Name" %} |
- {{ object.file_name|placeholder }} |
-
-
- | {% trans "File Extension" %} |
- {{ object.file_extension|placeholder }} |
-
-
- | {% trans "Attachment" %} |
- {% checkmark object.as_attachment %} |
-
-
- | {% trans "Data Source" %} |
-
- {% if object.data_source %}
- {{ object.data_source }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- | {% trans "Data File" %} |
-
- {% if object.data_file %}
- {{ object.data_file }}
- {% elif object.data_path %}
-
-
-
- {{ object.data_path }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- | {% trans "Data Synced" %} |
- {{ object.data_synced|placeholder }} |
-
-
- | {% trans "Auto Sync Enabled" %} |
- {% checkmark object.auto_sync_enabled %} |
-
-
-
- {% include 'inc/panels/tags.html' %}
- {% plugin_left_page object %}
-
-
-
-
-
- {% if object.environment_params %}
-
{{ object.environment_params|json }}
- {% else %}
-
{% trans "None" %}
- {% endif %}
-
-
- {% plugin_right_page object %}
-
-
-
-
-
-
-
- {% include 'inc/sync_warning.html' %}
-
{{ object.template_code }}
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/configtemplate/attrs/data_file.html b/netbox/templates/extras/configtemplate/attrs/data_file.html
new file mode 100644
index 000000000..7d1a79937
--- /dev/null
+++ b/netbox/templates/extras/configtemplate/attrs/data_file.html
@@ -0,0 +1,9 @@
+{% load i18n %}
+{% if object.data_file %}
+ {{ object.data_file }}
+{% else %}
+
+
+
+ {{ value }}
+{% endif %}
diff --git a/netbox/templates/extras/customfield.html b/netbox/templates/extras/customfield.html
index c2ddff355..f15e1d050 100644
--- a/netbox/templates/extras/customfield.html
+++ b/netbox/templates/extras/customfield.html
@@ -1,163 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | Type |
-
- {{ object.get_type_display }}
- {% if object.related_object_type %}
- ({{ object.related_object_type.model|bettertitle }})
- {% endif %}
- |
-
-
- | {% trans "Label" %} |
- {{ object.label|placeholder }} |
-
-
- | {% trans "Group Name" %} |
- {{ object.group_name|placeholder }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|markdown|placeholder }} |
-
-
- | {% trans "Required" %} |
- {% checkmark object.required %} |
-
-
- | {% trans "Must be Unique" %} |
- {% checkmark object.unique %} |
-
-
- | {% trans "Cloneable" %} |
- {% checkmark object.is_cloneable %} |
-
- {% if object.choice_set %}
-
- | {% trans "Choice Set" %} |
- {{ object.choice_set|linkify }} ({{ object.choice_set.choices|length }} choices) |
-
- {% endif %}
-
- | {% trans "Default Value" %} |
- {{ object.default }} |
-
-
- | {% trans "Related object filter" %} |
- {% if object.related_object_filter %}
- {{ object.related_object_filter|json }} |
- {% else %}
- {{ ''|placeholder }} |
- {% endif %}
-
-
-
-
-
-
-
- | {% trans "Search Weight" %} |
-
- {% if object.search_weight %}
- {{ object.search_weight }}
- {% else %}
- {% trans "Disabled" %}
- {% endif %}
- |
-
-
- | {% trans "Filter Logic" %} |
- {{ object.get_filter_logic_display }} |
-
-
- | {% trans "Display Weight" %} |
- {{ object.weight }} |
-
-
- | {% trans "UI Visible" %} |
- {{ object.get_ui_visible_display }} |
-
-
- | {% trans "UI Editable" %} |
- {{ object.get_ui_editable_display }} |
-
-
-
- {% include 'inc/panels/comments.html' %}
- {% plugin_left_page object %}
-
-
-
-
-
- {% for ct in object.object_types.all %}
-
- | {{ ct }} |
-
- {% endfor %}
-
-
-
-
-
-
- | {% trans "Minimum Value" %} |
- {{ object.validation_minimum|placeholder }} |
-
-
- | {% trans "Maximum Value" %} |
- {{ object.validation_maximum|placeholder }} |
-
-
- | {% trans "Regular Expression" %} |
-
- {% if object.validation_regex %}
- {{ object.validation_regex }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
-
-
-
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/customfield/attrs/choice_set.html b/netbox/templates/extras/customfield/attrs/choice_set.html
new file mode 100644
index 000000000..9cc2d2ff7
--- /dev/null
+++ b/netbox/templates/extras/customfield/attrs/choice_set.html
@@ -0,0 +1 @@
+{% load helpers i18n %}{{ value|linkify }} ({{ value.choices|length }} {% trans "choices" %})
diff --git a/netbox/templates/extras/customfield/attrs/related_object_filter.html b/netbox/templates/extras/customfield/attrs/related_object_filter.html
new file mode 100644
index 000000000..074b80489
--- /dev/null
+++ b/netbox/templates/extras/customfield/attrs/related_object_filter.html
@@ -0,0 +1 @@
+{% load helpers %}{{ value|json }}
diff --git a/netbox/templates/extras/customfield/attrs/search_weight.html b/netbox/templates/extras/customfield/attrs/search_weight.html
new file mode 100644
index 000000000..7061e5310
--- /dev/null
+++ b/netbox/templates/extras/customfield/attrs/search_weight.html
@@ -0,0 +1 @@
+{% load i18n %}{% if value %}{{ value }}{% else %}{% trans "Disabled" %}{% endif %}
diff --git a/netbox/templates/extras/customfield/attrs/type.html b/netbox/templates/extras/customfield/attrs/type.html
new file mode 100644
index 000000000..b6f9ff73c
--- /dev/null
+++ b/netbox/templates/extras/customfield/attrs/type.html
@@ -0,0 +1 @@
+{% load helpers %}{{ object.get_type_display }}{% if object.related_object_type %} ({{ object.related_object_type.model|bettertitle }}){% endif %}
diff --git a/netbox/templates/extras/customfieldchoiceset.html b/netbox/templates/extras/customfieldchoiceset.html
index 85f642603..f15e1d050 100644
--- a/netbox/templates/extras/customfieldchoiceset.html
+++ b/netbox/templates/extras/customfieldchoiceset.html
@@ -1,72 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-
-{% block content %}
-
-
-
-
-
-
- | Name |
- {{ object.name }} |
-
-
- | Description |
- {{ object.description|placeholder }} |
-
-
- | Base Choices |
- {{ object.get_base_choices_display|placeholder }} |
-
-
- | Choices |
- {{ object.choices|length }} |
-
-
- | Order Alphabetically |
- {% checkmark object.order_alphabetically %} |
-
-
- | Used by |
-
-
- {% for cf in object.choices_for.all %}
- - {{ cf|linkify }}
- {% endfor %}
-
- |
-
-
-
- {% plugin_left_page object %}
-
-
-
-
-
-
-
- | Value |
- Label |
-
-
- {% for value, label in choices %}
-
- | {{ value }} |
- {{ label }} |
-
- {% endfor %}
-
- {% include 'inc/paginator.html' with page=choices %}
-
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/customlink.html b/netbox/templates/extras/customlink.html
index 7e9740402..f15e1d050 100644
--- a/netbox/templates/extras/customlink.html
+++ b/netbox/templates/extras/customlink.html
@@ -1,71 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Enabled" %} |
- {% checkmark object.enabled %} |
-
-
- | {% trans "Group Name" %} |
- {{ object.group_name|placeholder }} |
-
-
- | {% trans "Weight" %} |
- {{ object.weight }} |
-
-
- | {% trans "Button Class" %} |
- {{ object.get_button_class_display }} |
-
-
- | {% trans "New Window" %} |
- {% checkmark object.new_window %} |
-
-
-
-
-
-
- {% for ct in object.object_types.all %}
-
- | {{ ct }} |
-
- {% endfor %}
-
-
- {% plugin_left_page object %}
-
-
-
-
-
-
{{ object.link_text }}
-
-
-
-
-
-
{{ object.link_url }}
-
-
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/eventrule.html b/netbox/templates/extras/eventrule.html
index 2b0bebe8f..f15e1d050 100644
--- a/netbox/templates/extras/eventrule.html
+++ b/netbox/templates/extras/eventrule.html
@@ -1,105 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Enabled" %} |
- {% checkmark object.enabled %} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
-
-
-
- {% for object_type in object.object_types.all %}
-
- | {{ object_type }} |
-
- {% endfor %}
-
-
-
-
-
- {% for name, event in registry.event_types.items %}
- -
-
-
- {% if name in object.event_types %}
- {% checkmark True %}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
-
-
- {{ event }}
-
-
-
- {% endfor %}
-
-
- {% plugin_left_page object %}
-
-
-
-
-
- {% if object.conditions %}
-
{{ object.conditions|json }}
- {% else %}
-
{% trans "None" %}
- {% endif %}
-
-
-
-
-
-
- | {% trans "Type" %} |
- {{ object.get_action_type_display }} |
-
-
- | {% trans "Object" %} |
-
- {{ object.action_object|linkify }}
- |
-
-
- | {% trans "Data" %} |
-
- {% if object.action_data %}
- {{ object.action_data|json }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/eventrule/attrs/action_data.html b/netbox/templates/extras/eventrule/attrs/action_data.html
new file mode 100644
index 000000000..ee6f5233b
--- /dev/null
+++ b/netbox/templates/extras/eventrule/attrs/action_data.html
@@ -0,0 +1 @@
+{% load helpers %}{% if value %}{{ value|json }}{% else %}—{% endif %}
diff --git a/netbox/templates/extras/exporttemplate.html b/netbox/templates/extras/exporttemplate.html
index eb8ec9135..f15e1d050 100644
--- a/netbox/templates/extras/exporttemplate.html
+++ b/netbox/templates/extras/exporttemplate.html
@@ -1,79 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block title %}{{ object.name }}{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "MIME Type" %} |
- {{ object.mime_type|placeholder }} |
-
-
- | {% trans "File Name" %} |
- {{ object.file_name|placeholder }} |
-
-
- | {% trans "File Extension" %} |
- {{ object.file_extension|placeholder }} |
-
-
- | {% trans "Attachment" %} |
- {% checkmark object.as_attachment %} |
-
-
-
- {% include 'core/inc/datafile_panel.html' %}
- {% plugin_left_page object %}
-
-
-
-
-
- {% for object_type in object.object_types.all %}
-
- | {{ object_type }} |
-
- {% endfor %}
-
-
-
-
-
- {% if object.environment_params %}
-
{{ object.environment_params|json }}
- {% else %}
-
{% trans "None" %}
- {% endif %}
-
-
- {% plugin_right_page object %}
-
-
-
-
-
-
-
- {% include 'inc/sync_warning.html' %}
-
{{ object.template_code }}
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/imageattachment.html b/netbox/templates/extras/imageattachment.html
index 8aefd095b..f15e1d050 100644
--- a/netbox/templates/extras/imageattachment.html
+++ b/netbox/templates/extras/imageattachment.html
@@ -1,67 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Parent Object" %} |
- {{ object.parent|linkify }} |
-
-
- | {% trans "Name" %} |
- {{ object.name|placeholder }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
- {% plugin_left_page object %}
-
-
-
-
-
-
- | {% trans "Filename" %} |
-
- {{ object.filename }}
-
- |
-
-
- | {% trans "Dimensions" %} |
- {{ object.image_width }} × {{ object.image_height }} |
-
-
- | {% trans "Size" %} |
-
- {{ object.size|filesizeformat }}
- |
-
-
-
- {% plugin_right_page object %}
-
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/journalentry.html b/netbox/templates/extras/journalentry.html
index 23aa881ff..358172d03 100644
--- a/netbox/templates/extras/journalentry.html
+++ b/netbox/templates/extras/journalentry.html
@@ -1,42 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
-{% load static %}
-{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
{{ object.assigned_object }}
{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Object" %} |
- {{ object.assigned_object|linkify }} |
-
-
- | {% trans "Created" %} |
- {{ object.created|isodatetime:"minutes" }} |
-
-
- | {% trans "Created By" %} |
- {{ object.created_by }} |
-
-
- | {% trans "Kind" %} |
- {% badge object.get_kind_display bg_color=object.get_kind_color %} |
-
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
-
-
- {% include 'inc/panels/comments.html' %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/notificationgroup.html b/netbox/templates/extras/notificationgroup.html
index 36ceef308..f15e1d050 100644
--- a/netbox/templates/extras/notificationgroup.html
+++ b/netbox/templates/extras/notificationgroup.html
@@ -1,57 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
-
- {{ object.name }}
- |
-
-
- | {% trans "Description" %} |
-
- {{ object.description|placeholder }}
- |
-
-
-
- {% plugin_left_page object %}
-
-
-
-
-
- {% for group in object.groups.all %}
-
{{ group }}
- {% empty %}
-
{% trans "None assigned" %}
- {% endfor %}
-
-
-
-
-
- {% for user in object.users.all %}
-
{{ user }}
- {% empty %}
-
{% trans "None assigned" %}
- {% endfor %}
-
-
- {% plugin_right_page object %}
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/panels/configcontext_assignment.html b/netbox/templates/extras/panels/configcontext_assignment.html
new file mode 100644
index 000000000..fbd7cf4b4
--- /dev/null
+++ b/netbox/templates/extras/panels/configcontext_assignment.html
@@ -0,0 +1,21 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers i18n %}
+
+{% block panel_content %}
+
+ {% for title, objects in assigned_objects %}
+
+ | {{ title }} |
+
+
+ {% for obj in objects %}
+ - {{ obj|linkify }}
+ {% empty %}
+ - {% trans "None" %}
+ {% endfor %}
+
+ |
+
+ {% endfor %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/configcontext_data.html b/netbox/templates/extras/panels/configcontext_data.html
new file mode 100644
index 000000000..3a92e96ae
--- /dev/null
+++ b/netbox/templates/extras/panels/configcontext_data.html
@@ -0,0 +1,5 @@
+{% load helpers i18n %}
+{% include 'inc/sync_warning.html' %}
+
+ {% include 'extras/inc/configcontext_data.html' with title="Data" data=object.data format=format copyid="data" %}
+
diff --git a/netbox/templates/extras/panels/configcontextprofile_schema.html b/netbox/templates/extras/panels/configcontextprofile_schema.html
new file mode 100644
index 000000000..a86aa4344
--- /dev/null
+++ b/netbox/templates/extras/panels/configcontextprofile_schema.html
@@ -0,0 +1,6 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers %}
+
+{% block panel_content %}
+ {{ object.schema|json }}
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/customfield_related_objects.html b/netbox/templates/extras/panels/customfield_related_objects.html
new file mode 100644
index 000000000..9dc8ed662
--- /dev/null
+++ b/netbox/templates/extras/panels/customfield_related_objects.html
@@ -0,0 +1,21 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers i18n %}
+
+{% block panel_content %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/customfieldchoiceset_choices.html b/netbox/templates/extras/panels/customfieldchoiceset_choices.html
new file mode 100644
index 000000000..db5be1520
--- /dev/null
+++ b/netbox/templates/extras/panels/customfieldchoiceset_choices.html
@@ -0,0 +1,22 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+
+
+
+ | {% trans "Value" %} |
+ {% trans "Label" %} |
+
+
+
+ {% for value, label in choices %}
+
+ | {{ value }} |
+ {{ label }} |
+
+ {% endfor %}
+
+
+ {% include 'inc/paginator.html' with page=choices %}
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/eventrule_event_types.html b/netbox/templates/extras/panels/eventrule_event_types.html
new file mode 100644
index 000000000..cc3d62806
--- /dev/null
+++ b/netbox/templates/extras/panels/eventrule_event_types.html
@@ -0,0 +1,23 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers %}
+
+{% block panel_content %}
+
+ {% for name, event in registry.event_types.items %}
+ -
+
+
+ {% if name in object.event_types %}
+ {% checkmark True %}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+
+
+ {{ event }}
+
+
+
+ {% endfor %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/imageattachment_file.html b/netbox/templates/extras/panels/imageattachment_file.html
new file mode 100644
index 000000000..0a50ad11a
--- /dev/null
+++ b/netbox/templates/extras/panels/imageattachment_file.html
@@ -0,0 +1,24 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+
+
+ | {% trans "Filename" %} |
+
+ {{ object.filename }}
+
+ |
+
+
+ | {% trans "Dimensions" %} |
+ {{ object.image_width }} × {{ object.image_height }} |
+
+
+ | {% trans "Size" %} |
+
+ {{ object.size|filesizeformat }}
+ |
+
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/imageattachment_image.html b/netbox/templates/extras/panels/imageattachment_image.html
new file mode 100644
index 000000000..dfeea5040
--- /dev/null
+++ b/netbox/templates/extras/panels/imageattachment_image.html
@@ -0,0 +1,9 @@
+{% extends "ui/panels/_base.html" %}
+
+{% block panel_content %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/notificationgroup_groups.html b/netbox/templates/extras/panels/notificationgroup_groups.html
new file mode 100644
index 000000000..2e8a14da9
--- /dev/null
+++ b/netbox/templates/extras/panels/notificationgroup_groups.html
@@ -0,0 +1,12 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+
+ {% for group in object.groups.all %}
+
{{ group }}
+ {% empty %}
+
{% trans "None assigned" %}
+ {% endfor %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/notificationgroup_users.html b/netbox/templates/extras/panels/notificationgroup_users.html
new file mode 100644
index 000000000..5ee74a8cb
--- /dev/null
+++ b/netbox/templates/extras/panels/notificationgroup_users.html
@@ -0,0 +1,12 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+
+ {% for user in object.users.all %}
+
{{ user }}
+ {% empty %}
+
{% trans "None assigned" %}
+ {% endfor %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/object_types.html b/netbox/templates/extras/panels/object_types.html
new file mode 100644
index 000000000..c57556e42
--- /dev/null
+++ b/netbox/templates/extras/panels/object_types.html
@@ -0,0 +1,11 @@
+{% extends "ui/panels/_base.html" %}
+
+{% block panel_content %}
+
+ {% for ct in object.object_types.all %}
+
+ | {{ ct }} |
+
+ {% endfor %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/savedfilter_object_types.html b/netbox/templates/extras/panels/savedfilter_object_types.html
new file mode 100644
index 000000000..8b7a053c6
--- /dev/null
+++ b/netbox/templates/extras/panels/savedfilter_object_types.html
@@ -0,0 +1,16 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers i18n %}
+
+{% block panel_content %}
+
+ {% for object_type in object.object_types.all %}
+ {% with object_type.model_class|validated_viewname:"list" as viewname %}
+ {% if viewname %}
+
{{ object_type }}
+ {% else %}
+
{{ object_type }}
+ {% endif %}
+ {% endwith %}
+ {% endfor %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/tableconfig_columns.html b/netbox/templates/extras/panels/tableconfig_columns.html
new file mode 100644
index 000000000..33b4f052f
--- /dev/null
+++ b/netbox/templates/extras/panels/tableconfig_columns.html
@@ -0,0 +1,15 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers %}
+{% load i18n %}
+
+{% block panel_content %}
+
+ {% for name in object.columns %}
+ -
+ {% with column=columns|get_key:name %}
+ {{ column.verbose_name }}
+ {% endwith %}
+
+ {% endfor %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/tableconfig_ordering.html b/netbox/templates/extras/panels/tableconfig_ordering.html
new file mode 100644
index 000000000..2c9f3afb7
--- /dev/null
+++ b/netbox/templates/extras/panels/tableconfig_ordering.html
@@ -0,0 +1,22 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers %}
+{% load i18n %}
+
+{% block panel_content %}
+
+ {% for column_name, ascending in object.ordering_items %}
+ -
+ {% with column=columns|get_key:column_name %}
+ {% if ascending %}
+
+ {% else %}
+
+ {% endif %}
+ {{ column.verbose_name }}
+ {% endwith %}
+
+ {% empty %}
+ - {% trans "Default" %}
+ {% endfor %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/tag_item_types.html b/netbox/templates/extras/panels/tag_item_types.html
new file mode 100644
index 000000000..8cc2b7920
--- /dev/null
+++ b/netbox/templates/extras/panels/tag_item_types.html
@@ -0,0 +1,21 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers i18n %}
+
+{% block panel_content %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/panels/tag_object_types.html b/netbox/templates/extras/panels/tag_object_types.html
new file mode 100644
index 000000000..6f20d2ee7
--- /dev/null
+++ b/netbox/templates/extras/panels/tag_object_types.html
@@ -0,0 +1,16 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+
+ {% for ct in object.object_types.all %}
+
+ | {{ ct }} |
+
+ {% empty %}
+
+ | {% trans "Any" %} |
+
+ {% endfor %}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/extras/savedfilter.html b/netbox/templates/extras/savedfilter.html
index 93d917f3f..f15e1d050 100644
--- a/netbox/templates/extras/savedfilter.html
+++ b/netbox/templates/extras/savedfilter.html
@@ -1,69 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "User" %} |
- {{ object.user|placeholder }} |
-
-
- | {% trans "Enabled" %} |
- {% checkmark object.enabled %} |
-
-
- | {% trans "Shared" %} |
- {% checkmark object.shared %} |
-
-
- | {% trans "Weight" %} |
- {{ object.weight }} |
-
-
-
-
-
-
- {% for object_type in object.object_types.all %}
- {% with object_type.model_class|validated_viewname:"list" as viewname %}
- {% if viewname %}
-
{{ object_type }}
- {% else %}
-
{{ object_type }}
- {% endif %}
- {% endwith %}
- {% endfor %}
-
-
- {% plugin_left_page object %}
-
-
-
-
-
-
{{ object.parameters|json }}
-
-
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/tableconfig.html b/netbox/templates/extras/tableconfig.html
index a08b58ec9..f15e1d050 100644
--- a/netbox/templates/extras/tableconfig.html
+++ b/netbox/templates/extras/tableconfig.html
@@ -1,88 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "Object Type" %} |
- {{ object.object_type }} |
-
-
- | {% trans "Table" %} |
- {{ object.table }} |
-
-
- | {% trans "User" %} |
- {{ object.user|placeholder }} |
-
-
- | {% trans "Enabled" %} |
- {% checkmark object.enabled %} |
-
-
- | {% trans "Shared" %} |
- {% checkmark object.shared %} |
-
-
- | {% trans "Weight" %} |
- {{ object.weight }} |
-
-
-
- {% plugin_left_page object %}
-
-
-
-
-
- {% for name in object.columns %}
- -
- {% with column=columns|get_key:name %}
- {{ column.verbose_name }}
- {% endwith %}
-
- {% endfor %}
-
-
-
-
-
- {% for column, ascending in object.ordering_items %}
- -
- {% with column=columns|get_key:column %}
- {% if ascending %}
-
- {% else %}
-
- {% endif %}
- {{ column.verbose_name }}
- {% endwith %}
-
- {% empty %}
- - {% trans "Default" %}
- {% endfor %}
-
-
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/tag.html b/netbox/templates/extras/tag.html
index 61d130322..f15e1d050 100644
--- a/netbox/templates/extras/tag.html
+++ b/netbox/templates/extras/tag.html
@@ -1,94 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
-
- {{ object.name }}
- |
-
-
- | {% trans "Description" %} |
-
- {{ object.description|placeholder }}
- |
-
-
- | {% trans "Color" %} |
-
-
- |
-
-
- | {% trans "Weight" %} |
- {{ object.weight }} |
-
-
- | {% trans "Tagged Items" %} |
-
- {{ taggeditem_table.rows|length }}
- |
-
-
-
- {% plugin_left_page object %}
-
-
-
-
-
- {% for ct in object.object_types.all %}
-
- | {{ ct }} |
-
- {% empty %}
-
- | {% trans "Any" %} |
-
- {% endfor %}
-
-
-
- {% plugin_right_page object %}
-
-
-
-
-
-
-
- {% render_table taggeditem_table 'inc/table.html' %}
- {% include 'inc/paginator.html' with paginator=taggeditem_table.paginator page=taggeditem_table.page %}
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/extras/tag/attrs/tagged_item_count.html b/netbox/templates/extras/tag/attrs/tagged_item_count.html
new file mode 100644
index 000000000..5a051633a
--- /dev/null
+++ b/netbox/templates/extras/tag/attrs/tagged_item_count.html
@@ -0,0 +1 @@
+{{ value.count }}
diff --git a/netbox/templates/extras/webhook.html b/netbox/templates/extras/webhook.html
index 6ec1d3942..f15e1d050 100644
--- a/netbox/templates/extras/webhook.html
+++ b/netbox/templates/extras/webhook.html
@@ -1,89 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
-
-
-
-
- | {% trans "HTTP Method" %} |
- {{ object.get_http_method_display }} |
-
-
- | {% trans "Payload URL" %} |
- {{ object.payload_url }} |
-
-
- | {% trans "HTTP Content Type" %} |
- {{ object.http_content_type }} |
-
-
- | {% trans "Secret" %} |
- {{ object.secret|placeholder }} |
-
-
-
-
-
-
-
- | {% trans "SSL Verification" %} |
- {% checkmark object.ssl_verification %} |
-
-
- | {% trans "CA File Path" %} |
- {{ object.ca_file_path|placeholder }} |
-
-
-
- {% plugin_left_page object %}
-
-
-
-
-
- {% if object.additional_headers %}
-
{{ object.additional_headers }}
- {% else %}
-
{% trans "None" %}
- {% endif %}
-
-
-
-
-
- {% if object.body_template %}
-
{{ object.body_template }}
- {% else %}
-
{% trans "None" %}
- {% endif %}
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/ui/panels/text_code.html b/netbox/templates/ui/panels/text_code.html
new file mode 100644
index 000000000..01182340f
--- /dev/null
+++ b/netbox/templates/ui/panels/text_code.html
@@ -0,0 +1,15 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+
+ {% if value %}
+
{{ value }}
+ {% else %}
+ {% if show_sync_warning %}
+ {% include 'inc/sync_warning.html' %}
+ {% endif %}
+
{% trans "None" %}
+ {% endif %}
+
+{% endblock panel_content %}