Browse Source

#20923 - Convert extras to new declarative UI layout (#21765)

bctiemann 20 hours ago
parent
commit
b8b12f3f90
42 changed files with 919 additions and 1178 deletions
  1. 440 1
      netbox/extras/ui/panels.py
  2. 173 1
      netbox/extras/views.py
  3. 20 0
      netbox/netbox/ui/panels.py
  4. 0 61
      netbox/templates/extras/configcontext.html
  5. 0 38
      netbox/templates/extras/configcontextprofile.html
  6. 0 99
      netbox/templates/extras/configtemplate.html
  7. 9 0
      netbox/templates/extras/configtemplate/attrs/data_file.html
  8. 0 162
      netbox/templates/extras/customfield.html
  9. 1 0
      netbox/templates/extras/customfield/attrs/choice_set.html
  10. 1 0
      netbox/templates/extras/customfield/attrs/related_object_filter.html
  11. 1 0
      netbox/templates/extras/customfield/attrs/search_weight.html
  12. 1 0
      netbox/templates/extras/customfield/attrs/type.html
  13. 0 71
      netbox/templates/extras/customfieldchoiceset.html
  14. 0 70
      netbox/templates/extras/customlink.html
  15. 0 104
      netbox/templates/extras/eventrule.html
  16. 1 0
      netbox/templates/extras/eventrule/attrs/action_data.html
  17. 0 78
      netbox/templates/extras/exporttemplate.html
  18. 0 66
      netbox/templates/extras/imageattachment.html
  19. 0 35
      netbox/templates/extras/journalentry.html
  20. 0 56
      netbox/templates/extras/notificationgroup.html
  21. 21 0
      netbox/templates/extras/panels/configcontext_assignment.html
  22. 5 0
      netbox/templates/extras/panels/configcontext_data.html
  23. 6 0
      netbox/templates/extras/panels/configcontextprofile_schema.html
  24. 21 0
      netbox/templates/extras/panels/customfield_related_objects.html
  25. 22 0
      netbox/templates/extras/panels/customfieldchoiceset_choices.html
  26. 23 0
      netbox/templates/extras/panels/eventrule_event_types.html
  27. 24 0
      netbox/templates/extras/panels/imageattachment_file.html
  28. 9 0
      netbox/templates/extras/panels/imageattachment_image.html
  29. 12 0
      netbox/templates/extras/panels/notificationgroup_groups.html
  30. 12 0
      netbox/templates/extras/panels/notificationgroup_users.html
  31. 11 0
      netbox/templates/extras/panels/object_types.html
  32. 16 0
      netbox/templates/extras/panels/savedfilter_object_types.html
  33. 15 0
      netbox/templates/extras/panels/tableconfig_columns.html
  34. 22 0
      netbox/templates/extras/panels/tableconfig_ordering.html
  35. 21 0
      netbox/templates/extras/panels/tag_item_types.html
  36. 16 0
      netbox/templates/extras/panels/tag_object_types.html
  37. 0 68
      netbox/templates/extras/savedfilter.html
  38. 0 87
      netbox/templates/extras/tableconfig.html
  39. 0 93
      netbox/templates/extras/tag.html
  40. 1 0
      netbox/templates/extras/tag/attrs/tagged_item_count.html
  41. 0 88
      netbox/templates/extras/webhook.html
  42. 15 0
      netbox/templates/ui/panels/text_code.html

+ 440 - 1
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')

+ 173 - 1
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)

+ 20 - 0
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.

+ 0 - 61
netbox/templates/extras/configcontext.html

@@ -1,62 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load static %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-12 col-md-5">
-      <div class="card">
-        <h2 class="card-header">{% trans "Config Context" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Weight" %}</th>
-            <td>{{ object.weight }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Profile" %}</th>
-            <td>{{ object.profile|linkify|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Active" %}</th>
-            <td>{% checkmark object.is_active %}</td>
-          </tr>
-        </table>
-      </div>
-      {% include 'core/inc/datafile_panel.html' %}
-      <div class="card">
-        <h2 class="card-header">{% trans "Assignment" %}</h2>
-        <table class="table table-hover attr-table">
-          {% for title, objects in assigned_objects %}
-            <tr>
-              <th scope="row">{{ title }}</th>
-              <td>
-                <ul class="list-unstyled mb-0">
-                  {% for object in objects %}
-                    <li>{{ object|linkify }}</li>
-                  {% empty %}
-                    <li class="text-muted">{% trans "None" %}</li>
-                  {% endfor %}
-                </ul>
-              </td>
-            </tr>
-          {% endfor %}
-        </table>
-      </div>
-    </div>
-    <div class="col col-12 col-md-7">
-      {% include 'inc/sync_warning.html' %}
-      <div class="card">
-        {% include 'extras/inc/configcontext_data.html' with title="Data" data=object.data format=format copyid="data" %}
-      </div>
-    </div>
-  </div>
-{% endblock %}

+ 0 - 38
netbox/templates/extras/configcontextprofile.html

@@ -1,39 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load static %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Config Context Profile" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-      {% include 'core/inc/datafile_panel.html' %}
-      {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/tags.html' %}
-      {% include 'inc/panels/comments.html' %}
-    </div>
-    <div class="col col-6">
-      <div class="card">
-        <h2 class="card-header d-flex justify-content-between">
-          {% trans "JSON Schema" %}
-          <div>
-            {% copy_content "schema" %}
-          </div>
-        </h2>
-        <pre class="card-body rendered-context-data m-0" id="schema">{{ object.schema|json }}</pre>
-      </div>
-    </div>
-  </div>
-{% endblock %}

+ 0 - 99
netbox/templates/extras/configtemplate.html

@@ -1,100 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Config Template" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "MIME Type" %}</th>
-            <td>{{ object.mime_type|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "File Name" %}</th>
-            <td>{{ object.file_name|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "File Extension" %}</th>
-            <td>{{ object.file_extension|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Attachment" %}</th>
-            <td>{% checkmark object.as_attachment %}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Data Source" %}</th>
-            <td>
-              {% if object.data_source %}
-                <a href="{{ object.data_source.get_absolute_url }}">{{ object.data_source }}</a>
-              {% else %}
-                {{ ''|placeholder }}
-              {% endif %}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Data File" %}</th>
-            <td>
-              {% if object.data_file %}
-                <a href="{{ object.data_file.get_absolute_url }}">{{ object.data_file }}</a>
-              {% elif object.data_path %}
-                <div class="float-end text-warning">
-                  <i class="mdi mdi-alert" title="{% trans "The data file associated with this object has been deleted" %}."></i>
-                </div>
-                {{ object.data_path }}
-              {% else %}
-                {{ ''|placeholder }}
-              {% endif %}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Data Synced" %}</th>
-            <td>{{ object.data_synced|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Auto Sync Enabled" %}</th>
-            <td>{% checkmark object.auto_sync_enabled %}</td>
-          </tr>
-        </table>
-      </div>
-      {% include 'inc/panels/tags.html' %}
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Environment Parameters" %}</h2>
-        <div class="card-body">
-          {% if object.environment_params %}
-            <pre>{{ object.environment_params|json }}</pre>
-          {% else %}
-            <span class="text-muted">{% trans "None" %}</span>
-          {% endif %}
-        </div>
-      </div>
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      <div class="card">
-        <h2 class="card-header">{% trans "Template" %}</h2>
-        <div class="card-body">
-          {% include 'inc/sync_warning.html' %}
-          <pre>{{ object.template_code }}</pre>
-        </div>
-      </div>
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 9 - 0
netbox/templates/extras/configtemplate/attrs/data_file.html

@@ -0,0 +1,9 @@
+{% load i18n %}
+{% if object.data_file %}
+  <a href="{{ object.data_file.get_absolute_url }}">{{ object.data_file }}</a>
+{% else %}
+  <div class="float-end text-warning">
+    <i class="mdi mdi-alert" title="{% trans "The data file associated with this object has been deleted" %}."></i>
+  </div>
+  {{ value }}
+{% endif %}

+ 0 - 162
netbox/templates/extras/customfield.html

@@ -1,163 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-<div class="row mb-3">
-	<div class="col col-12 col-md-6">
-    <div class="card">
-      <h2 class="card-header">{% trans "Custom Field" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "Name" %}</th>
-          <td>{{ object.name }}</td>
-        </tr>
-        <tr>
-          <th scope="row">Type</th>
-          <td>
-            {{ object.get_type_display }}
-            {% if object.related_object_type %}
-              ({{ object.related_object_type.model|bettertitle }})
-            {% endif %}
-          </td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Label" %}</th>
-          <td>{{ object.label|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Group Name" %}</th>
-          <td>{{ object.group_name|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Description" %}</th>
-          <td>{{ object.description|markdown|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Required" %}</th>
-          <td>{% checkmark object.required %}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Must be Unique" %}</th>
-          <td>{% checkmark object.unique %}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Cloneable" %}</th>
-          <td>{% checkmark object.is_cloneable %}</td>
-        </tr>
-        {% if object.choice_set %}
-          <tr>
-            <th scope="row">{% trans "Choice Set" %}</th>
-            <td>{{ object.choice_set|linkify }} ({{ object.choice_set.choices|length }} choices)</td>
-          </tr>
-        {% endif %}
-        <tr>
-          <th scope="row">{% trans "Default Value" %}</th>
-          <td>{{ object.default }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Related object filter" %}</th>
-          {% if object.related_object_filter %}
-            <td><pre>{{ object.related_object_filter|json }}</pre></td>
-          {% else %}
-            <td>{{ ''|placeholder }}</td>
-          {% endif %}
-        </tr>
-      </table>
-    </div>
-    <div class="card">
-      <h2 class="card-header">{% trans "Behavior" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "Search Weight" %}</th>
-          <td>
-            {% if object.search_weight %}
-              {{ object.search_weight }}
-            {% else %}
-              <span class="text-muted">{% trans "Disabled" %}</span>
-            {% endif %}
-          </td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Filter Logic" %}</th>
-          <td>{{ object.get_filter_logic_display }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Display Weight" %}</th>
-          <td>{{ object.weight }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "UI Visible" %}</th>
-          <td>{{ object.get_ui_visible_display }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "UI Editable" %}</th>
-          <td>{{ object.get_ui_editable_display }}</td>
-        </tr>
-      </table>
-    </div>
-    {% include 'inc/panels/comments.html' %}
-    {% plugin_left_page object %}
-	</div>
-	<div class="col col-12 col-md-6">
-    <div class="card">
-      <h2 class="card-header">{% trans "Object Types" %}</h2>
-      <table class="table table-hover attr-table">
-        {% for ct in object.object_types.all %}
-          <tr>
-            <td>{{ ct }}</td>
-          </tr>
-        {% endfor %}
-      </table>
-    </div>
-    <div class="card">
-      <h2 class="card-header">{% trans "Validation Rules" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "Minimum Value" %}</th>
-          <td>{{ object.validation_minimum|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Maximum Value" %}</th>
-          <td>{{ object.validation_maximum|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Regular Expression" %}</th>
-          <td>
-            {% if object.validation_regex %}
-              <code>{{ object.validation_regex }}</code>
-            {% else %}
-              {{ ''|placeholder }}
-            {% endif %}
-          </td>
-        </tr>
-      </table>
-    </div>
-    <div class="card">
-      <h2 class="card-header">Related Objects</h2>
-      <ul class="list-group list-group-flush" role="presentation">
-        {% for qs in related_models %}
-          <a class="list-group-item list-group-item-action d-flex justify-content-between">
-            {{ qs.model|meta:"verbose_name_plural"|bettertitle }}
-            {% with count=qs.count %}
-              {% if count %}
-                <span class="badge text-bg-primary rounded-pill">{{ count }}</span>
-              {% else %}
-                <span class="badge text-bg-light rounded-pill">&mdash;</span>
-              {% endif %}
-            {% endwith %}
-          </a>
-        {% endfor %}
-      </ul>
-    </div>
-
-    {% plugin_right_page object %}
-  </div>
-</div>
-<div class="row">
-  <div class="col col-md-12">
-    {% plugin_full_width_page object %}
-  </div>
-</div>
-{% endblock %}

+ 1 - 0
netbox/templates/extras/customfield/attrs/choice_set.html

@@ -0,0 +1 @@
+{% load helpers i18n %}{{ value|linkify }} ({{ value.choices|length }} {% trans "choices" %})

+ 1 - 0
netbox/templates/extras/customfield/attrs/related_object_filter.html

@@ -0,0 +1 @@
+{% load helpers %}<pre>{{ value|json }}</pre>

+ 1 - 0
netbox/templates/extras/customfield/attrs/search_weight.html

@@ -0,0 +1 @@
+{% load i18n %}{% if value %}{{ value }}{% else %}<span class="text-muted">{% trans "Disabled" %}</span>{% endif %}

+ 1 - 0
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 %}

+ 0 - 71
netbox/templates/extras/customfieldchoiceset.html

@@ -1,72 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-
-{% block content %}
-  <div class="row mb-3">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">Custom Field Choice Set</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">Name</th>
-            <td>{{ object.name }}</td>
-          </tr>
-          <tr>
-            <th scope="row">Description</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">Base Choices</th>
-            <td>{{ object.get_base_choices_display|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">Choices</th>
-            <td>{{ object.choices|length }}</td>
-          </tr>
-          <tr>
-            <th scope="row">Order Alphabetically</th>
-          <td>{% checkmark object.order_alphabetically %}</td>
-          </tr>
-          <tr>
-            <th scope="row">Used by</th>
-            <td>
-              <ul class="list-unstyled mb-0">
-                {% for cf in object.choices_for.all %}
-                  <li>{{ cf|linkify }}</li>
-                {% endfor %}
-              </ul>
-            </td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">Choices ({{ object.choices|length }})</h2>
-        <table class="table table-hover">
-          <thead>
-            <tr>
-              <th>Value</th>
-              <th>Label</th>
-            </tr>
-          </thead>
-          {% for value, label in choices %}
-            <tr>
-              <td>{{ value }}</td>
-              <td>{{ label }}</td>
-            </tr>
-          {% endfor %}
-        </table>
-        {% include 'inc/paginator.html' with page=choices %}
-      </div>
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 0 - 70
netbox/templates/extras/customlink.html

@@ -1,71 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-<div class="row mb-3">
-	<div class="col col-12 col-md-5">
-    <div class="card">
-      <h2 class="card-header">{% trans "Custom Link" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "Name" %}</th>
-          <td>{{ object.name }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Enabled" %}</th>
-          <td>{% checkmark object.enabled %}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Group Name" %}</th>
-          <td>{{ object.group_name|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Weight" %}</th>
-          <td>{{ object.weight }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Button Class" %}</th>
-          <td>{{ object.get_button_class_display }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "New Window" %}</th>
-          <td>{% checkmark object.new_window %}</td>
-        </tr>
-      </table>
-    </div>
-    <div class="card">
-      <h2 class="card-header">{% trans "Assigned Models" %}</h2>
-      <table class="table table-hover attr-table">
-        {% for ct in object.object_types.all %}
-          <tr>
-            <td>{{ ct }}</td>
-          </tr>
-        {% endfor %}
-      </table>
-    </div>
-    {% plugin_left_page object %}
-	</div>
-	<div class="col col-12 col-md-7">
-    <div class="card">
-      <h2 class="card-header">{% trans "Link Text" %}</h2>
-      <div class="card-body">
-        <pre>{{ object.link_text }}</pre>
-      </div>
-    </div>
-    <div class="card">
-      <h2 class="card-header">{% trans "Link URL" %}</h2>
-      <div class="card-body">
-        <pre>{{ object.link_url }}</pre>
-      </div>
-    </div>
-    {% plugin_right_page object %}
-  </div>
-</div>
-<div class="row">
-    <div class="col col-md-12">
-        {% plugin_full_width_page object %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 104
netbox/templates/extras/eventrule.html

@@ -1,105 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row mb-3">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Event Rule" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Enabled" %}</th>
-            <td>{% checkmark object.enabled %}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-      <div class="card">
-        <h2 class="card-header">{% trans "Object Types" %}</h2>
-        <table class="table table-hover attr-table">
-          {% for object_type in object.object_types.all %}
-            <tr>
-              <td>{{ object_type }}</td>
-            </tr>
-          {% endfor %}
-        </table>
-      </div>
-      <div class="card">
-        <h2 class="card-header">{% trans "Event Types" %}</h2>
-        <ul class="list-group list-group-flush">
-          {% for name, event in registry.event_types.items %}
-            <li class="list-group-item">
-              <div class="row align-items-center">
-                <div class="col-auto">
-                  {% if name in object.event_types %}
-                    {% checkmark True %}
-                  {% else %}
-                    {{ ''|placeholder }}
-                  {% endif %}
-                </div>
-                <div class="col">
-                  {{ event }}
-                </div>
-              </div>
-            </li>
-          {% endfor %}
-        </ul>
-      </div>
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Conditions" %}</h2>
-        <div class="card-body">
-          {% if object.conditions %}
-            <pre>{{ object.conditions|json }}</pre>
-          {% else %}
-            <p class="text-muted">{% trans "None" %}</p>
-          {% endif %}
-        </div>
-      </div>
-      <div class="card">
-        <h2 class="card-header">{% trans "Action" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Type" %}</th>
-            <td>{{ object.get_action_type_display }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Object" %}</th>
-            <td>
-                {{ object.action_object|linkify }}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Data" %}</th>
-            <td>
-              {% if object.action_data %}
-                <pre>{{ object.action_data|json }}</pre>
-              {% else %}
-                {{ ''|placeholder }}
-              {% endif %}
-            </td>
-          </tr>
-        </table>
-      </div>
-      {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/tags.html' %}
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 1 - 0
netbox/templates/extras/eventrule/attrs/action_data.html

@@ -0,0 +1 @@
+{% load helpers %}{% if value %}<pre>{{ value|json }}</pre>{% else %}<span class="text-muted">&mdash;</span>{% endif %}

+ 0 - 78
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 %}
-  <div class="row">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Export Template" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "MIME Type" %}</th>
-            <td>{{ object.mime_type|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "File Name" %}</th>
-            <td>{{ object.file_name|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "File Extension" %}</th>
-            <td>{{ object.file_extension|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Attachment" %}</th>
-            <td>{% checkmark object.as_attachment %}</td>
-          </tr>
-        </table>
-      </div>
-      {% include 'core/inc/datafile_panel.html' %}
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Assigned Models" %}</h2>
-        <table class="table table-hover attr-table">
-          {% for object_type in object.object_types.all %}
-            <tr>
-              <td>{{ object_type }}</td>
-            </tr>
-          {% endfor %}
-        </table>
-      </div>
-      <div class="card">
-        <h2 class="card-header">{% trans "Environment Parameters" %}</h2>
-        <div class="card-body">
-          {% if object.environment_params %}
-            <pre>{{ object.environment_params|json }}</pre>
-          {% else %}
-            <span class="text-muted">{% trans "None" %}</span>
-          {% endif %}
-        </div>
-      </div>
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      <div class="card">
-        <h2 class="card-header">{% trans "Template" %}</h2>
-        <div class="card-body">
-          {% include 'inc/sync_warning.html' %}
-          <pre>{{ object.template_code }}</pre>
-        </div>
-      </div>
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 0 - 66
netbox/templates/extras/imageattachment.html

@@ -1,67 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Image Attachment" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Parent Object" %}</th>
-            <td>{{ object.parent|linkify }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "File" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Filename" %}</th>
-            <td>
-              <a href="{{ object.image.url }}" target="_blank">{{ object.filename }}</a>
-              <i class="mdi mdi-open-in-new"></i>
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Dimensions" %}</th>
-            <td>{{ object.image_width }} × {{ object.image_height }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Size" %}</th>
-            <td>
-              <span title="{{ object.size }} {% trans "bytes" %}">{{ object.size|filesizeformat }}</span>
-            </td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      <div class="card">
-        <h2 class="card-header">{% trans "Image" %}</h2>
-        <div class="card-body">
-          <a href="{{ object.image.url }}" title="{{ object.name }}">
-            {{ object.html_tag }}
-          </a>
-        </div>
-      </div>
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 0 - 35
netbox/templates/extras/journalentry.html

@@ -1,42 +1,7 @@
 {% extends 'generic/object.html' %}
 {% load helpers %}
-{% load static %}
-{% load i18n %}
 
 {% block breadcrumbs %}
   {{ block.super }}
   <li class="breadcrumb-item"><a href="{% action_url object.assigned_object 'journal' pk=object.assigned_object.pk %}">{{ object.assigned_object }}</a></li>
 {% endblock %}
-
-{% block content %}
-    <div class="row mb-3">
-        <div class="col col-md-5">
-            <div class="card">
-                <h2 class="card-header">{% trans "Journal Entry" %}</h2>
-                <table class="table table-hover attr-table">
-                    <tr>
-                        <th scope="row">{% trans "Object" %}</th>
-                        <td>{{ object.assigned_object|linkify }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">{% trans "Created" %}</th>
-                        <td>{{ object.created|isodatetime:"minutes" }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">{% trans "Created By" %}</th>
-                        <td>{{ object.created_by }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">{% trans "Kind" %}</th>
-                        <td>{% badge object.get_kind_display bg_color=object.get_kind_color %}</td>
-                    </tr>
-                </table>
-            </div>
-            {% include 'inc/panels/custom_fields.html' %}
-            {% include 'inc/panels/tags.html' %}
-        </div>
-        <div class="col col-md-7">
-            {% include 'inc/panels/comments.html' %}
-        </div>
-    </div>
-{% endblock %}

+ 0 - 56
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 %}
-  <div class="row">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Notification Group" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>
-              {{ object.name }}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>
-                {{ object.description|placeholder }}
-            </td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Groups" %}</h2>
-        <div class="list-group list-group-flush">
-          {% for group in object.groups.all %}
-            <a href="{{ group.get_absolute_url }}" class="list-group-item list-group-item-action">{{ group }}</a>
-          {% empty %}
-            <div class="list-group-item text-muted">{% trans "None assigned" %}</div>
-          {% endfor %}
-        </div>
-      </div>
-      <div class="card">
-        <h2 class="card-header">{% trans "Users" %}</h2>
-        <div class="list-group list-group-flush">
-          {% for user in object.users.all %}
-            <a href="{{ user.get_absolute_url }}" class="list-group-item list-group-item-action">{{ user }}</a>
-          {% empty %}
-            <div class="list-group-item text-muted">{% trans "None assigned" %}</div>
-          {% endfor %}
-        </div>
-      </div>
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 21 - 0
netbox/templates/extras/panels/configcontext_assignment.html

@@ -0,0 +1,21 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers i18n %}
+
+{% block panel_content %}
+  <table class="table table-hover attr-table">
+    {% for title, objects in assigned_objects %}
+      <tr>
+        <th scope="row">{{ title }}</th>
+        <td>
+          <ul class="list-unstyled mb-0">
+            {% for obj in objects %}
+              <li>{{ obj|linkify }}</li>
+            {% empty %}
+              <li class="text-muted">{% trans "None" %}</li>
+            {% endfor %}
+          </ul>
+        </td>
+      </tr>
+    {% endfor %}
+  </table>
+{% endblock panel_content %}

+ 5 - 0
netbox/templates/extras/panels/configcontext_data.html

@@ -0,0 +1,5 @@
+{% load helpers i18n %}
+{% include 'inc/sync_warning.html' %}
+<div class="card">
+  {% include 'extras/inc/configcontext_data.html' with title="Data" data=object.data format=format copyid="data" %}
+</div>

+ 6 - 0
netbox/templates/extras/panels/configcontextprofile_schema.html

@@ -0,0 +1,6 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers %}
+
+{% block panel_content %}
+  <pre class="card-body rendered-context-data m-0" id="schema">{{ object.schema|json }}</pre>
+{% endblock panel_content %}

+ 21 - 0
netbox/templates/extras/panels/customfield_related_objects.html

@@ -0,0 +1,21 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers i18n %}
+
+{% block panel_content %}
+  <ul class="list-group list-group-flush" role="presentation">
+    {% for qs in related_models %}
+      <a class="list-group-item list-group-item-action d-flex justify-content-between">
+        {{ qs.model|meta:"verbose_name_plural"|bettertitle }}
+        {% with count=qs.count %}
+          {% if count %}
+            <span class="badge text-bg-primary rounded-pill">{{ count }}</span>
+          {% else %}
+            <span class="badge text-bg-light rounded-pill">&mdash;</span>
+          {% endif %}
+        {% endwith %}
+      </a>
+    {% empty %}
+      <span class="list-group-item text-muted">{% trans "None" %}</span>
+    {% endfor %}
+  </ul>
+{% endblock panel_content %}

+ 22 - 0
netbox/templates/extras/panels/customfieldchoiceset_choices.html

@@ -0,0 +1,22 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+  <table class="table table-hover">
+    <thead>
+      <tr>
+        <th>{% trans "Value" %}</th>
+        <th>{% trans "Label" %}</th>
+      </tr>
+    </thead>
+    <tbody>
+      {% for value, label in choices %}
+        <tr>
+          <td>{{ value }}</td>
+          <td>{{ label }}</td>
+        </tr>
+      {% endfor %}
+    </tbody>
+  </table>
+  {% include 'inc/paginator.html' with page=choices %}
+{% endblock panel_content %}

+ 23 - 0
netbox/templates/extras/panels/eventrule_event_types.html

@@ -0,0 +1,23 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers %}
+
+{% block panel_content %}
+  <ul class="list-group list-group-flush">
+    {% for name, event in registry.event_types.items %}
+      <li class="list-group-item">
+        <div class="row align-items-center">
+          <div class="col-auto">
+            {% if name in object.event_types %}
+              {% checkmark True %}
+            {% else %}
+              {{ ''|placeholder }}
+            {% endif %}
+          </div>
+          <div class="col">
+            {{ event }}
+          </div>
+        </div>
+      </li>
+    {% endfor %}
+  </ul>
+{% endblock panel_content %}

+ 24 - 0
netbox/templates/extras/panels/imageattachment_file.html

@@ -0,0 +1,24 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+  <table class="table table-hover attr-table">
+    <tr>
+      <th scope="row">{% trans "Filename" %}</th>
+      <td>
+        <a href="{{ object.image.url }}" target="_blank">{{ object.filename }}</a>
+        <i class="mdi mdi-open-in-new"></i>
+      </td>
+    </tr>
+    <tr>
+      <th scope="row">{% trans "Dimensions" %}</th>
+      <td>{{ object.image_width }} &times; {{ object.image_height }}</td>
+    </tr>
+    <tr>
+      <th scope="row">{% trans "Size" %}</th>
+      <td>
+        <span title="{{ object.size }} {% trans "bytes" %}">{{ object.size|filesizeformat }}</span>
+      </td>
+    </tr>
+  </table>
+{% endblock panel_content %}

+ 9 - 0
netbox/templates/extras/panels/imageattachment_image.html

@@ -0,0 +1,9 @@
+{% extends "ui/panels/_base.html" %}
+
+{% block panel_content %}
+  <div class="card-body">
+    <a href="{{ object.image.url }}" title="{{ object.name }}">
+      {{ object.html_tag }}
+    </a>
+  </div>
+{% endblock panel_content %}

+ 12 - 0
netbox/templates/extras/panels/notificationgroup_groups.html

@@ -0,0 +1,12 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+  <div class="list-group list-group-flush">
+    {% for group in object.groups.all %}
+      <a href="{{ group.get_absolute_url }}" class="list-group-item list-group-item-action">{{ group }}</a>
+    {% empty %}
+      <div class="list-group-item text-muted">{% trans "None assigned" %}</div>
+    {% endfor %}
+  </div>
+{% endblock panel_content %}

+ 12 - 0
netbox/templates/extras/panels/notificationgroup_users.html

@@ -0,0 +1,12 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+  <div class="list-group list-group-flush">
+    {% for user in object.users.all %}
+      <a href="{{ user.get_absolute_url }}" class="list-group-item list-group-item-action">{{ user }}</a>
+    {% empty %}
+      <div class="list-group-item text-muted">{% trans "None assigned" %}</div>
+    {% endfor %}
+  </div>
+{% endblock panel_content %}

+ 11 - 0
netbox/templates/extras/panels/object_types.html

@@ -0,0 +1,11 @@
+{% extends "ui/panels/_base.html" %}
+
+{% block panel_content %}
+  <table class="table table-hover attr-table">
+    {% for ct in object.object_types.all %}
+      <tr>
+        <td>{{ ct }}</td>
+      </tr>
+    {% endfor %}
+  </table>
+{% endblock panel_content %}

+ 16 - 0
netbox/templates/extras/panels/savedfilter_object_types.html

@@ -0,0 +1,16 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers i18n %}
+
+{% block panel_content %}
+  <div class="list-group list-group-flush" role="presentation">
+    {% for object_type in object.object_types.all %}
+      {% with object_type.model_class|validated_viewname:"list" as viewname %}
+        {% if viewname %}
+          <a href="{% url viewname %}?{{ object.url_params }}" class="list-group-item list-group-item-action">{{ object_type }}</a>
+        {% else %}
+          <div class="list-group-item list-group-item-action">{{ object_type }}</div>
+        {% endif %}
+      {% endwith %}
+    {% endfor %}
+  </div>
+{% endblock panel_content %}

+ 15 - 0
netbox/templates/extras/panels/tableconfig_columns.html

@@ -0,0 +1,15 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers %}
+{% load i18n %}
+
+{% block panel_content %}
+  <ul class="list-group list-group-flush" role="presentation">
+    {% for name in object.columns %}
+      <li class="list-group-item list-group-item-action">
+        {% with column=columns|get_key:name %}
+          {{ column.verbose_name }}
+        {% endwith %}
+      </li>
+    {% endfor %}
+  </ul>
+{% endblock panel_content %}

+ 22 - 0
netbox/templates/extras/panels/tableconfig_ordering.html

@@ -0,0 +1,22 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers %}
+{% load i18n %}
+
+{% block panel_content %}
+  <ul class="list-group list-group-flush" role="presentation">
+    {% for column_name, ascending in object.ordering_items %}
+      <li class="list-group-item">
+        {% with column=columns|get_key:column_name %}
+          {% if ascending %}
+            <i class="mdi mdi-arrow-down-thick"></i>
+          {% else %}
+            <i class="mdi mdi-arrow-up-thick"></i>
+          {% endif %}
+          {{ column.verbose_name }}
+        {% endwith %}
+      </li>
+    {% empty %}
+      <li class="list-group-item text-muted">{% trans "Default" %}</li>
+    {% endfor %}
+  </ul>
+{% endblock panel_content %}

+ 21 - 0
netbox/templates/extras/panels/tag_item_types.html

@@ -0,0 +1,21 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers i18n %}
+
+{% block panel_content %}
+  <ul class="list-group list-group-flush" role="presentation">
+    {% for object_type in object_types %}
+      {% action_url object_type.content_type.model_class 'list' as list_url %}
+      {% if list_url %}
+        <a href="{{ list_url }}?tag={{ object.slug }}" class="list-group-item list-group-item-action d-flex justify-content-between">
+          {{ object_type.content_type.name|bettertitle }}
+          <span class="badge text-bg-primary rounded-pill">{{ object_type.item_count }}</span>
+        </a>
+      {% else %}
+        <li class="list-group-item list-group-item-action d-flex justify-content-between">
+          {{ object_type.content_type.name|bettertitle }}
+          <span class="badge text-bg-primary rounded-pill">{{ object_type.item_count }}</span>
+        </li>
+      {% endif %}
+    {% endfor %}
+  </ul>
+{% endblock panel_content %}

+ 16 - 0
netbox/templates/extras/panels/tag_object_types.html

@@ -0,0 +1,16 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+  <table class="table table-hover attr-table">
+    {% for ct in object.object_types.all %}
+      <tr>
+        <td>{{ ct }}</td>
+      </tr>
+    {% empty %}
+      <tr>
+        <td class="text-muted">{% trans "Any" %}</td>
+      </tr>
+    {% endfor %}
+  </table>
+{% endblock panel_content %}

+ 0 - 68
netbox/templates/extras/savedfilter.html

@@ -1,69 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-<div class="row mb-3">
-	<div class="col col-12 col-md-5">
-    <div class="card">
-      <h2 class="card-header">{% trans "Saved Filter" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "Name" %}</th>
-          <td>{{ object.name }}</td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-        </tr>
-        <tr>
-            <th scope="row">{% trans "User" %}</th>
-            <td>{{ object.user|placeholder }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Enabled" %}</th>
-          <td>{% checkmark object.enabled %}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Shared" %}</th>
-          <td>{% checkmark object.shared %}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Weight" %}</th>
-          <td>{{ object.weight }}</td>
-        </tr>
-      </table>
-    </div>
-    <div class="card">
-      <h2 class="card-header">{% trans "Assigned Models" %}</h2>
-      <div class="list-group list-group-flush" role="presentation">
-        {% for object_type in object.object_types.all %}
-          {% with object_type.model_class|validated_viewname:"list" as viewname %}
-            {% if viewname %}
-              <a href="{% url viewname %}?{{ object.url_params }}" class="list-group-item list-group-item-action">{{ object_type }}</a>
-            {% else %}
-              <div class="list-group-item list-group-item-action">{{ object_type }}</div>
-            {% endif %}
-          {% endwith %}
-        {% endfor %}
-      </div>
-    </div>
-    {% plugin_left_page object %}
-	</div>
-	<div class="col col-12 col-md-7">
-    <div class="card">
-      <h2 class="card-header">{% trans "Parameters" %}</h2>
-      <div class="card-body p-0">
-        <pre>{{ object.parameters|json }}</pre>
-      </div>
-    </div>
-    {% plugin_right_page object %}
-  </div>
-</div>
-<div class="row">
-    <div class="col col-md-12">
-        {% plugin_full_width_page object %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 87
netbox/templates/extras/tableconfig.html

@@ -1,88 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-  <div class="row mb-3">
-    <div class="col col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Table Config" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Object Type" %}</th>
-            <td>{{ object.object_type }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Table" %}</th>
-            <td>{{ object.table }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "User" %}</th>
-            <td>{{ object.user|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Enabled" %}</th>
-            <td>{% checkmark object.enabled %}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Shared" %}</th>
-            <td>{% checkmark object.shared %}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Weight" %}</th>
-            <td>{{ object.weight }}</td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Columns Displayed" %}</h2>
-        <ul class="list-group list-group-flush" role="presentation">
-          {% for name in object.columns %}
-            <li class="list-group-item list-group-item-action">
-              {% with column=columns|get_key:name %}
-                {{ column.verbose_name }}
-              {% endwith %}
-            </li>
-          {% endfor %}
-        </ul>
-      </div>
-      <div class="card">
-        <h2 class="card-header">{% trans "Ordering" %}</h2>
-        <ul class="list-group list-group-flush" role="presentation">
-          {% for column, ascending in object.ordering_items %}
-            <li class="list-group-item">
-              {% with column=columns|get_key:column %}
-                {% if ascending %}
-                  <i class="mdi mdi-arrow-down-thick"></i>
-                {% else %}
-                  <i class="mdi mdi-arrow-up-thick"></i>
-                {% endif %}
-                {{ column.verbose_name }}
-              {% endwith %}
-            </li>
-          {% empty %}
-            <li class="list-group-item text-muted">{% trans "Default" %}</li>
-          {% endfor %}
-        </ul>
-      </div>
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 0 - 93
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 %}
-  <div class="row">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Tag" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>
-              {{ object.name }}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>
-                {{ object.description|placeholder }}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Color" %}</th>
-            <td>
-                <span class="color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Weight" %}</th>
-            <td>{{ object.weight }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Tagged Items" %}</th>
-            <td>
-                {{ taggeditem_table.rows|length }}
-            </td>
-          </tr>
-        </table>
-      </div>
-      {% plugin_left_page object %}
-    </div>
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Allowed Object Types" %}</h2>
-        <table class="table table-hover attr-table">
-          {% for ct in object.object_types.all %}
-            <tr>
-              <td>{{ ct }}</td>
-            </tr>
-          {% empty %}
-            <tr>
-              <td class="text-muted">{% trans "Any" %}</td>
-            </tr>
-          {% endfor %}
-        </table>
-      </div>
-      <div class="card">
-        <h2 class="card-header">{% trans "Tagged Item Types" %}</h2>
-        <ul class="list-group list-group-flush" role="presentation">
-          {% for object_type in object_types %}
-            {% action_url object_type.content_type.model_class 'list' as list_url %}
-            {% if list_url %}
-              <a href="{{ list_url }}?tag={{ object.slug }}" class="list-group-item list-group-item-action d-flex justify-content-between">
-                {{ object_type.content_type.name|bettertitle }}
-                <span class="badge text-bg-primary rounded-pill">{{ object_type.item_count }}</span>
-              </a>
-            {% else %}
-              <li class="list-group-item list-group-item-action d-flex justify-content-between">
-                {{ object_type.content_type.name|bettertitle }}
-                <span class="badge text-bg-primary rounded-pill">{{ object_type.item_count }}</span>
-              </li>
-            {% endif %}
-          {% endfor %}
-        </ul>
-      </div>
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-md-12">
-      <div class="card">
-        <h2 class="card-header">{% trans "Tagged Objects" %}</h2>
-        <div class="table-responsive">
-          {% render_table taggeditem_table 'inc/table.html' %}
-          {% include 'inc/paginator.html' with paginator=taggeditem_table.paginator page=taggeditem_table.page %}
-        </div>
-      </div>
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 1 - 0
netbox/templates/extras/tag/attrs/tagged_item_count.html

@@ -0,0 +1 @@
+{{ value.count }}

+ 0 - 88
netbox/templates/extras/webhook.html

@@ -1,89 +1 @@
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-<div class="row mb-3">
-	<div class="col col-12 col-md-6">
-    <div class="card">
-      <h2 class="card-header">{% trans "Webhook" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "Name" %}</th>
-          <td>{{ object.name }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Description" %}</th>
-          <td>{{ object.description|placeholder }}</td>
-        </tr>
-      </table>
-    </div>
-    <div class="card">
-      <h2 class="card-header">{% trans "HTTP Request" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "HTTP Method" %}</th>
-          <td>{{ object.get_http_method_display }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Payload URL" %}</th>
-          <td class="font-monospace">{{ object.payload_url }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "HTTP Content Type" %}</th>
-          <td>{{ object.http_content_type }}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "Secret" %}</th>
-          <td>{{ object.secret|placeholder }}</td>
-        </tr>
-      </table>
-    </div>
-    <div class="card">
-      <h2 class="card-header">{% trans "SSL" %}</h2>
-      <table class="table table-hover attr-table">
-        <tr>
-          <th scope="row">{% trans "SSL Verification" %}</th>
-          <td>{% checkmark object.ssl_verification %}</td>
-        </tr>
-        <tr>
-          <th scope="row">{% trans "CA File Path" %}</th>
-          <td>{{ object.ca_file_path|placeholder }}</td>
-        </tr>
-      </table>
-    </div>
-    {% plugin_left_page object %}
-	</div>
-	<div class="col col-12 col-md-6">
-    <div class="card">
-      <h2 class="card-header">{% trans "Additional Headers" %}</h2>
-      <div class="card-body">
-        {% if object.additional_headers %}
-          <pre>{{ object.additional_headers }}</pre>
-        {% else %}
-          <span class="text-muted">{% trans "None" %}</span>
-        {% endif %}
-      </div>
-    </div>
-    <div class="card">
-      <h2 class="card-header">{% trans "Body Template" %}</h2>
-      <div class="card-body">
-        {% if object.body_template %}
-          <pre>{{ object.body_template }}</pre>
-        {% else %}
-          <span class="text-muted">{% trans "None" %}</span>
-        {% endif %}
-      </div>
-    </div>
-    {% include 'inc/panels/custom_fields.html' %}
-    {% include 'inc/panels/tags.html' %}
-    {% plugin_right_page object %}
-  </div>
-</div>
-<div class="row">
-    <div class="col col-md-12">
-        {% plugin_full_width_page object %}
-    </div>
-</div>
-{% endblock %}

+ 15 - 0
netbox/templates/ui/panels/text_code.html

@@ -0,0 +1,15 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+  <div class="card-body">
+    {% if value %}
+      <pre>{{ value }}</pre>
+    {% else %}
+      {% if show_sync_warning %}
+        {% include 'inc/sync_warning.html' %}
+      {% endif %}
+      <span class="text-muted">{% trans "None" %}</span>
+    {% endif %}
+  </div>
+{% endblock panel_content %}