diff --git a/netbox/core/ui/__init__.py b/netbox/core/ui/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/netbox/core/ui/panels.py b/netbox/core/ui/panels.py
new file mode 100644
index 000000000..a2a0781bd
--- /dev/null
+++ b/netbox/core/ui/panels.py
@@ -0,0 +1,91 @@
+from django.utils.translation import gettext_lazy as _
+
+from netbox.ui import attrs, panels
+
+
+class DataSourcePanel(panels.ObjectAttributesPanel):
+ title = _('Data Source')
+ name = attrs.TextAttr('name')
+ type = attrs.ChoiceAttr('type')
+ enabled = attrs.BooleanAttr('enabled')
+ status = attrs.ChoiceAttr('status')
+ sync_interval = attrs.ChoiceAttr('sync_interval', label=_('Sync interval'))
+ last_synced = attrs.DateTimeAttr('last_synced', label=_('Last synced'))
+ description = attrs.TextAttr('description')
+ source_url = attrs.TemplatedAttr(
+ 'source_url',
+ label=_('URL'),
+ template_name='core/datasource/attrs/source_url.html',
+ )
+ ignore_rules = attrs.TemplatedAttr(
+ 'ignore_rules',
+ label=_('Ignore rules'),
+ template_name='core/datasource/attrs/ignore_rules.html',
+ )
+
+
+class DataSourceBackendPanel(panels.ObjectPanel):
+ template_name = 'core/panels/datasource_backend.html'
+ title = _('Backend')
+
+
+class DataFilePanel(panels.ObjectAttributesPanel):
+ title = _('Data File')
+ source = attrs.RelatedObjectAttr('source', linkify=True)
+ path = attrs.TextAttr('path', style='font-monospace', copy_button=True)
+ last_updated = attrs.DateTimeAttr('last_updated')
+ size = attrs.TemplatedAttr('size', template_name='core/datafile/attrs/size.html')
+ hash = attrs.TextAttr('hash', label=_('SHA256 hash'), style='font-monospace', copy_button=True)
+
+
+class DataFileContentPanel(panels.ObjectPanel):
+ template_name = 'core/panels/datafile_content.html'
+ title = _('Content')
+
+
+class JobPanel(panels.ObjectAttributesPanel):
+ title = _('Job')
+ object_type = attrs.TemplatedAttr(
+ 'object_type',
+ label=_('Object type'),
+ template_name='core/job/attrs/object_type.html',
+ )
+ name = attrs.TextAttr('name')
+ status = attrs.ChoiceAttr('status')
+ error = attrs.TextAttr('error')
+ user = attrs.TextAttr('user', label=_('Created by'))
+
+
+class JobSchedulingPanel(panels.ObjectAttributesPanel):
+ title = _('Scheduling')
+ created = attrs.DateTimeAttr('created')
+ scheduled = attrs.TemplatedAttr('scheduled', template_name='core/job/attrs/scheduled.html')
+ started = attrs.DateTimeAttr('started')
+ completed = attrs.DateTimeAttr('completed')
+ queue = attrs.TextAttr('queue_name', label=_('Queue'))
+
+
+class ObjectChangePanel(panels.ObjectAttributesPanel):
+ title = _('Change')
+ time = attrs.DateTimeAttr('time')
+ user = attrs.TemplatedAttr(
+ 'user_name',
+ label=_('User'),
+ template_name='core/objectchange/attrs/user.html',
+ )
+ action = attrs.ChoiceAttr('action')
+ changed_object_type = attrs.TextAttr(
+ 'changed_object_type',
+ label=_('Object type'),
+ )
+ changed_object = attrs.TemplatedAttr(
+ 'object_repr',
+ label=_('Object'),
+ template_name='core/objectchange/attrs/changed_object.html',
+ )
+ message = attrs.TextAttr('message')
+ request_id = attrs.TemplatedAttr(
+ 'request_id',
+ label=_('Request ID'),
+ template_name='core/objectchange/attrs/request_id.html',
+ )
diff --git a/netbox/core/views.py b/netbox/core/views.py
index 21e68d1b6..b7b5c6335 100644
--- a/netbox/core/views.py
+++ b/netbox/core/views.py
@@ -23,9 +23,20 @@ from rq.worker import Worker
from rq.worker_registration import clean_worker_registry
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
+from extras.ui.panels import CustomFieldsPanel, TagsPanel
from netbox.config import PARAMS, get_config
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
from netbox.plugins.utils import get_installed_plugins
+from netbox.ui import layout
+from netbox.ui.panels import (
+ CommentsPanel,
+ ContextTablePanel,
+ JSONPanel,
+ ObjectsTablePanel,
+ PluginContentPanel,
+ RelatedObjectsPanel,
+ TemplatePanel,
+)
from netbox.views import generic
from netbox.views.generic.base import BaseObjectView
from netbox.views.generic.mixins import TableMixin
@@ -48,6 +59,7 @@ from .jobs import SyncDataSourceJob
from .models import *
from .plugins import get_catalog_plugins, get_local_plugins
from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
+from .ui import panels
#
# Data sources
@@ -67,6 +79,24 @@ class DataSourceListView(generic.ObjectListView):
@register_model_view(DataSource)
class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DataSource.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.DataSourcePanel(),
+ TagsPanel(),
+ CommentsPanel(),
+ ],
+ right_panels=[
+ panels.DataSourceBackendPanel(),
+ RelatedObjectsPanel(),
+ CustomFieldsPanel(),
+ ],
+ bottom_panels=[
+ ObjectsTablePanel(
+ model='core.DataFile',
+ filters={'source_id': lambda ctx: ctx['object'].pk},
+ ),
+ ],
+ )
def get_extra_context(self, request, instance):
return {
@@ -157,6 +187,12 @@ class DataFileListView(generic.ObjectListView):
class DataFileView(generic.ObjectView):
queryset = DataFile.objects.all()
actions = (DeleteObject,)
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.DataFilePanel(),
+ panels.DataFileContentPanel(),
+ ],
+ )
@register_model_view(DataFile, 'delete')
@@ -188,6 +224,17 @@ class JobListView(generic.ObjectListView):
class JobView(generic.ObjectView):
queryset = Job.objects.all()
actions = (DeleteObject,)
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.JobPanel(),
+ ],
+ right_panels=[
+ panels.JobSchedulingPanel(),
+ ],
+ bottom_panels=[
+ JSONPanel('data', title=_('Data')),
+ ],
+ )
@register_model_view(Job, 'log')
@@ -200,6 +247,13 @@ class JobLogView(generic.ObjectView):
badge=lambda obj: len(obj.log_entries),
weight=500,
)
+ layout = layout.Layout(
+ layout.Row(
+ layout.Column(
+ ContextTablePanel('table', title=_('Log Entries')),
+ ),
+ ),
+ )
def get_extra_context(self, request, instance):
table = JobLogEntryTable(instance.log_entries)
@@ -241,6 +295,26 @@ class ObjectChangeListView(generic.ObjectListView):
@register_model_view(ObjectChange)
class ObjectChangeView(generic.ObjectView):
queryset = None
+ layout = layout.Layout(
+ layout.Row(
+ layout.Column(panels.ObjectChangePanel()),
+ layout.Column(TemplatePanel('core/panels/objectchange_difference.html')),
+ ),
+ layout.Row(
+ layout.Column(TemplatePanel('core/panels/objectchange_prechange.html')),
+ layout.Column(TemplatePanel('core/panels/objectchange_postchange.html')),
+ ),
+ layout.Row(
+ layout.Column(PluginContentPanel('left_page')),
+ layout.Column(PluginContentPanel('right_page')),
+ ),
+ layout.Row(
+ layout.Column(
+ TemplatePanel('core/panels/objectchange_related.html'),
+ PluginContentPanel('full_width_page'),
+ ),
+ ),
+ )
def get_queryset(self, request):
return ObjectChange.objects.valid_models()
@@ -312,6 +386,14 @@ class ConfigRevisionListView(generic.ObjectListView):
@register_model_view(ConfigRevision)
class ConfigRevisionView(generic.ObjectView):
queryset = ConfigRevision.objects.all()
+ layout = layout.Layout(
+ layout.Row(
+ layout.Column(
+ TemplatePanel('core/panels/configrevision_data.html'),
+ TemplatePanel('core/panels/configrevision_comment.html'),
+ ),
+ ),
+ )
def get_extra_context(self, request, instance):
"""
diff --git a/netbox/templates/core/configrevision.html b/netbox/templates/core/configrevision.html
index 28179b7fd..2bd361a84 100644
--- a/netbox/templates/core/configrevision.html
+++ b/netbox/templates/core/configrevision.html
@@ -1,10 +1,7 @@
{% extends 'generic/object.html' %}
{% load buttons %}
-{% load custom_links %}
{% load helpers %}
{% load perms %}
-{% load plugins %}
-{% load static %}
{% load i18n %}
{% block breadcrumbs %}
@@ -27,22 +24,3 @@
{% endif %}
{% endblock subtitle %}
-
-{% block content %}
-
-
-
-
- {% include 'core/inc/config_data.html' %}
-
-
-
-
-
- {{ object.comment|placeholder }}
-
-
-
-
-
-{% endblock %}
diff --git a/netbox/templates/core/datafile.html b/netbox/templates/core/datafile.html
index 0747547b1..714ef22f8 100644
--- a/netbox/templates/core/datafile.html
+++ b/netbox/templates/core/datafile.html
@@ -1,62 +1,7 @@
{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load custom_links %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
{{ object.source }}
{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Source" %} |
- {{ object.source|linkify }} |
-
-
- | {% trans "Path" %} |
-
- {{ object.path }}
- {% copy_content "datafile_path" %}
- |
-
-
- | {% trans "Last Updated" %} |
- {{ object.last_updated }} |
-
-
- | {% trans "Size" %} |
- {{ object.size }} {% trans "bytes" %} |
-
-
- | {% trans "SHA256 Hash" %} |
-
- {{ object.hash }}
- {% copy_content "datafile_hash" %}
- |
-
-
-
-
-
-
-
{{ object.data_as_string }}
-
-
- {% plugin_left_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/core/datafile/attrs/size.html b/netbox/templates/core/datafile/attrs/size.html
new file mode 100644
index 000000000..ac54aca60
--- /dev/null
+++ b/netbox/templates/core/datafile/attrs/size.html
@@ -0,0 +1 @@
+{% load i18n %}{{ value }} {% trans "bytes" %}
diff --git a/netbox/templates/core/datasource.html b/netbox/templates/core/datasource.html
index 519e111af..07a7324ab 100644
--- a/netbox/templates/core/datasource.html
+++ b/netbox/templates/core/datasource.html
@@ -1,8 +1,4 @@
{% extends 'generic/object.html' %}
-{% load static %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -23,102 +19,3 @@
{% endif %}
{% endif %}
{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Type" %} |
- {{ object.get_type_display }} |
-
-
- | {% trans "Enabled" %} |
- {% checkmark object.enabled %} |
-
-
- | {% trans "Status" %} |
- {% badge object.get_status_display bg_color=object.get_status_color %} |
-
-
- | {% trans "Sync interval" %} |
- {{ object.get_sync_interval_display|placeholder }} |
-
-
- | {% trans "Last synced" %} |
- {{ object.last_synced|placeholder }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "URL" %} |
-
- {% if not object.type.is_local %}
- {{ object.source_url }}
- {% else %}
- {{ object.source_url }}
- {% endif %}
- |
-
-
- | {% trans "Ignore rules" %} |
-
- {% if object.ignore_rules %}
- {{ object.ignore_rules }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %} |
-
-
-
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_left_page object %}
-
-
-
-
- {% with backend=object.backend_class %}
-
- {% for name, field in backend.parameters.items %}
-
- | {{ field.label }} |
- {% if name in backend.sensitive_parameters %}
- ******** |
- {% else %}
- {{ object.parameters|get_key:name|placeholder }} |
- {% endif %}
-
- {% empty %}
-
- |
- {% trans "No parameters defined" %}
- |
-
- {% endfor %}
-
- {% endwith %}
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% plugin_right_page object %}
-
-
-
-
-
-
- {% htmx_table 'core:datafile_list' source_id=object.pk %}
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/core/datasource/attrs/ignore_rules.html b/netbox/templates/core/datasource/attrs/ignore_rules.html
new file mode 100644
index 000000000..ec1709b99
--- /dev/null
+++ b/netbox/templates/core/datasource/attrs/ignore_rules.html
@@ -0,0 +1 @@
+{{ value }}
diff --git a/netbox/templates/core/datasource/attrs/source_url.html b/netbox/templates/core/datasource/attrs/source_url.html
new file mode 100644
index 000000000..6a788c3eb
--- /dev/null
+++ b/netbox/templates/core/datasource/attrs/source_url.html
@@ -0,0 +1 @@
+{% if not object.type.is_local %}{{ value }}{% else %}{{ value }}{% endif %}
diff --git a/netbox/templates/core/job.html b/netbox/templates/core/job.html
index 48adf0319..03302b939 100644
--- a/netbox/templates/core/job.html
+++ b/netbox/templates/core/job.html
@@ -1,78 +1 @@
{% extends 'core/job/base.html' %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Object Type" %} |
-
- {{ object.object_type }}
- |
-
-
- | {% trans "Name" %} |
- {{ object.name|placeholder }} |
-
-
- | {% trans "Status" %} |
- {% badge object.get_status_display object.get_status_color %} |
-
- {% if object.error %}
-
- | {% trans "Error" %} |
- {{ object.error }} |
-
- {% endif %}
-
- | {% trans "Created By" %} |
- {{ object.user|placeholder }} |
-
-
-
-
-
-
-
-
-
- | {% trans "Created" %} |
- {{ object.created|isodatetime }} |
-
-
- | {% trans "Scheduled" %} |
-
- {{ object.scheduled|isodatetime|placeholder }}
- {% if object.interval %}
- ({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %})
- {% endif %}
- |
-
-
- | {% trans "Started" %} |
- {{ object.started|isodatetime|placeholder }} |
-
-
- | {% trans "Completed" %} |
- {{ object.completed|isodatetime|placeholder }} |
-
-
- | {% trans "Queue" %} |
- {{ object.queue_name|placeholder }} |
-
-
-
-
-
-
-
-
-
-
{{ object.data|json }}
-
-
-
-{% endblock %}
diff --git a/netbox/templates/core/job/attrs/object_type.html b/netbox/templates/core/job/attrs/object_type.html
new file mode 100644
index 000000000..fd3937c17
--- /dev/null
+++ b/netbox/templates/core/job/attrs/object_type.html
@@ -0,0 +1 @@
+{{ value }}
diff --git a/netbox/templates/core/job/attrs/scheduled.html b/netbox/templates/core/job/attrs/scheduled.html
new file mode 100644
index 000000000..2af7fdd49
--- /dev/null
+++ b/netbox/templates/core/job/attrs/scheduled.html
@@ -0,0 +1,3 @@
+{% load helpers %}
+{% load i18n %}
+{{ value|isodatetime }}{% if object.interval %} ({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %}){% endif %}
diff --git a/netbox/templates/core/job/log.html b/netbox/templates/core/job/log.html
index b8c727299..03302b939 100644
--- a/netbox/templates/core/job/log.html
+++ b/netbox/templates/core/job/log.html
@@ -1,12 +1 @@
{% extends 'core/job/base.html' %}
-{% load render_table from django_tables2 %}
-
-{% block content %}
-
-
-
- {% render_table table %}
-
-
-
-{% endblock %}
diff --git a/netbox/templates/core/objectchange.html b/netbox/templates/core/objectchange.html
index e4c7d4900..3f883bea8 100644
--- a/netbox/templates/core/objectchange.html
+++ b/netbox/templates/core/objectchange.html
@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
{% load i18n %}
{% block title %}{{ object }}{% endblock %}
@@ -21,161 +19,3 @@
{# ObjectChange does not support the default add/edit/delete controls #}
{% block control-buttons %}{% endblock %}
{% block subtitle %}{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Time" %} |
- {{ object.time|isodatetime }} |
-
-
- | {% trans "User" %} |
-
- {% if object.user.get_full_name %}
- {{ object.user.get_full_name }} ({{ object.user_name }})
- {% else %}
- {{ object.user_name }}
- {% endif %}
- |
-
-
- | {% trans "Action" %} |
-
- {{ object.get_action_display }}
- |
-
-
- | {% trans "Object Type" %} |
-
- {{ object.changed_object_type }}
- |
-
-
- | {% trans "Object" %} |
-
- {% if object.changed_object and object.changed_object.get_absolute_url %}
- {{ object.changed_object|linkify }}
- {% else %}
- {{ object.object_repr }}
- {% endif %}
- |
-
-
- | {% trans "Message" %} |
-
- {{ object.message|placeholder }}
- |
-
-
- | {% trans "Request ID" %} |
-
- {{ object.request_id }}
- |
-
-
-
-
-
-
-
-
- {% if diff_added == diff_removed %}
-
- {% if object.action == 'create' %}
- {% trans "Object Created" %}
- {% elif object.action == 'delete' %}
- {% trans "Object Deleted" %}
- {% else %}
- {% trans "No Changes" %}
- {% endif %}
-
- {% else %}
-
{{ diff_removed|json }}
-
{{ diff_added|json }}
- {% endif %}
-
-
-
-
-
-
-
-
-
- {% if object.prechange_data %}
- {% spaceless %}
-
- {% for k, v in object.prechange_data_clean.items %}
- {{ k }}: {{ v|json }}
- {% endfor %}
-
- {% endspaceless %}
- {% elif non_atomic_change %}
- {% trans "Warning: Comparing non-atomic change to previous change record" %} (
{{ prev_change.pk }})
- {% else %}
-
{% trans "None" %}
- {% endif %}
-
-
-
-
-
-
-
- {% if object.postchange_data %}
- {% spaceless %}
-
- {% for k, v in object.postchange_data_clean.items %}
- {{ k }}: {{ v|json }}
- {% endfor %}
-
- {% endspaceless %}
- {% else %}
-
{% trans "None" %}
- {% endif %}
-
-
-
-
-
-
- {% plugin_left_page object %}
-
-
- {% plugin_right_page object %}
-
-
-
-
- {% include 'inc/panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %}
- {% if related_changes_count > related_changes_table.rows|length %}
-
- {% endif %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/core/objectchange/attrs/changed_object.html b/netbox/templates/core/objectchange/attrs/changed_object.html
new file mode 100644
index 000000000..e3d8abe33
--- /dev/null
+++ b/netbox/templates/core/objectchange/attrs/changed_object.html
@@ -0,0 +1,2 @@
+{% load helpers %}
+{% if object.changed_object and object.changed_object.get_absolute_url %}{{ object.changed_object|linkify }}{% else %}{{ value }}{% endif %}
diff --git a/netbox/templates/core/objectchange/attrs/request_id.html b/netbox/templates/core/objectchange/attrs/request_id.html
new file mode 100644
index 000000000..92011812f
--- /dev/null
+++ b/netbox/templates/core/objectchange/attrs/request_id.html
@@ -0,0 +1 @@
+{{ value }}
diff --git a/netbox/templates/core/objectchange/attrs/user.html b/netbox/templates/core/objectchange/attrs/user.html
new file mode 100644
index 000000000..e897078c3
--- /dev/null
+++ b/netbox/templates/core/objectchange/attrs/user.html
@@ -0,0 +1 @@
+{% if object.user and object.user.get_full_name %}{{ object.user.get_full_name }} ({{ value }}){% else %}{{ value }}{% endif %}
diff --git a/netbox/templates/core/panels/configrevision_comment.html b/netbox/templates/core/panels/configrevision_comment.html
new file mode 100644
index 000000000..5ce365522
--- /dev/null
+++ b/netbox/templates/core/panels/configrevision_comment.html
@@ -0,0 +1,11 @@
+{% load i18n %}
+
+
+
+ {% if object.comment %}
+ {{ object.comment }}
+ {% else %}
+ —
+ {% endif %}
+
+
diff --git a/netbox/templates/core/panels/configrevision_data.html b/netbox/templates/core/panels/configrevision_data.html
new file mode 100644
index 000000000..461e2ab82
--- /dev/null
+++ b/netbox/templates/core/panels/configrevision_data.html
@@ -0,0 +1,5 @@
+{% load i18n %}
+
+
+ {% include 'core/inc/config_data.html' %}
+
diff --git a/netbox/templates/core/panels/datafile_content.html b/netbox/templates/core/panels/datafile_content.html
new file mode 100644
index 000000000..90702964e
--- /dev/null
+++ b/netbox/templates/core/panels/datafile_content.html
@@ -0,0 +1,8 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+
+
{{ object.data_as_string }}
+
+{% endblock panel_content %}
diff --git a/netbox/templates/core/panels/datasource_backend.html b/netbox/templates/core/panels/datasource_backend.html
new file mode 100644
index 000000000..97d7ae573
--- /dev/null
+++ b/netbox/templates/core/panels/datasource_backend.html
@@ -0,0 +1,26 @@
+{% extends "ui/panels/_base.html" %}
+{% load helpers %}
+{% load i18n %}
+
+{% block panel_content %}
+ {% with backend=object.backend_class %}
+
+ {% for name, field in backend.parameters.items %}
+
+ | {{ field.label }} |
+ {% if name in backend.sensitive_parameters %}
+ ******** |
+ {% else %}
+ {{ object.parameters|get_key:name|placeholder }} |
+ {% endif %}
+
+ {% empty %}
+
+ |
+ {% trans "No parameters defined" %}
+ |
+
+ {% endfor %}
+
+ {% endwith %}
+{% endblock panel_content %}
diff --git a/netbox/templates/core/panels/objectchange_difference.html b/netbox/templates/core/panels/objectchange_difference.html
new file mode 100644
index 000000000..5c954ee56
--- /dev/null
+++ b/netbox/templates/core/panels/objectchange_difference.html
@@ -0,0 +1,31 @@
+{% load helpers %}
+{% load i18n %}
+
+
+
+ {% if diff_added == diff_removed %}
+
+ {% if object.action == 'create' %}
+ {% trans "Object Created" %}
+ {% elif object.action == 'delete' %}
+ {% trans "Object Deleted" %}
+ {% else %}
+ {% trans "No Changes" %}
+ {% endif %}
+
+ {% else %}
+
{{ diff_removed|json }}
+
{{ diff_added|json }}
+ {% endif %}
+
+
diff --git a/netbox/templates/core/panels/objectchange_postchange.html b/netbox/templates/core/panels/objectchange_postchange.html
new file mode 100644
index 000000000..903ec9586
--- /dev/null
+++ b/netbox/templates/core/panels/objectchange_postchange.html
@@ -0,0 +1,18 @@
+{% load helpers %}
+{% load i18n %}
+
+
+
+ {% if object.postchange_data %}
+ {% spaceless %}
+
+ {% for k, v in object.postchange_data_clean.items %}
+ {{ k }}: {{ v|json }}
+ {% endfor %}
+
+ {% endspaceless %}
+ {% else %}
+
{% trans "None" %}
+ {% endif %}
+
+
diff --git a/netbox/templates/core/panels/objectchange_prechange.html b/netbox/templates/core/panels/objectchange_prechange.html
new file mode 100644
index 000000000..057f59731
--- /dev/null
+++ b/netbox/templates/core/panels/objectchange_prechange.html
@@ -0,0 +1,20 @@
+{% load helpers %}
+{% load i18n %}
+
+
+
+ {% if object.prechange_data %}
+ {% spaceless %}
+
+ {% for k, v in object.prechange_data_clean.items %}
+ {{ k }}: {{ v|json }}
+ {% endfor %}
+
+ {% endspaceless %}
+ {% elif non_atomic_change %}
+ {% trans "Warning: Comparing non-atomic change to previous change record" %} (
{{ prev_change.pk }})
+ {% else %}
+
{% trans "None" %}
+ {% endif %}
+
+
diff --git a/netbox/templates/core/panels/objectchange_related.html b/netbox/templates/core/panels/objectchange_related.html
new file mode 100644
index 000000000..27d26d3f1
--- /dev/null
+++ b/netbox/templates/core/panels/objectchange_related.html
@@ -0,0 +1,11 @@
+{% load i18n %}
+{% include 'inc/panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %}
+{% if related_changes_count > related_changes_table.rows|length %}
+
+{% endif %}