Просмотр исходного кода

Merge pull request #21754 from netbox-community/20923-core-ui-layouts

#20923: Migrate core app to the new UI layouts
bctiemann 1 день назад
Родитель
Сommit
f5c97e367c
26 измененных файлов с 317 добавлено и 429 удалено
  1. 0 0
      netbox/core/ui/__init__.py
  2. 91 0
      netbox/core/ui/panels.py
  3. 84 0
      netbox/core/views.py
  4. 0 22
      netbox/templates/core/configrevision.html
  5. 0 55
      netbox/templates/core/datafile.html
  6. 1 0
      netbox/templates/core/datafile/attrs/size.html
  7. 0 103
      netbox/templates/core/datasource.html
  8. 1 0
      netbox/templates/core/datasource/attrs/ignore_rules.html
  9. 1 0
      netbox/templates/core/datasource/attrs/source_url.html
  10. 0 77
      netbox/templates/core/job.html
  11. 1 0
      netbox/templates/core/job/attrs/object_type.html
  12. 3 0
      netbox/templates/core/job/attrs/scheduled.html
  13. 0 11
      netbox/templates/core/job/log.html
  14. 0 160
      netbox/templates/core/objectchange.html
  15. 2 0
      netbox/templates/core/objectchange/attrs/changed_object.html
  16. 1 0
      netbox/templates/core/objectchange/attrs/request_id.html
  17. 1 0
      netbox/templates/core/objectchange/attrs/user.html
  18. 11 0
      netbox/templates/core/panels/configrevision_comment.html
  19. 5 0
      netbox/templates/core/panels/configrevision_data.html
  20. 8 0
      netbox/templates/core/panels/datafile_content.html
  21. 26 0
      netbox/templates/core/panels/datasource_backend.html
  22. 31 0
      netbox/templates/core/panels/objectchange_difference.html
  23. 18 0
      netbox/templates/core/panels/objectchange_postchange.html
  24. 20 0
      netbox/templates/core/panels/objectchange_prechange.html
  25. 11 0
      netbox/templates/core/panels/objectchange_related.html
  26. 1 1
      netbox/templates/inc/panel_table.html

+ 0 - 0
netbox/core/ui/__init__.py


+ 91 - 0
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',
+    )

+ 84 - 0
netbox/core/views.py

@@ -23,9 +23,20 @@ from rq.worker import Worker
 from rq.worker_registration import clean_worker_registry
 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 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.config import PARAMS, get_config
 from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
 from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
 from netbox.plugins.utils import get_installed_plugins
 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 import generic
 from netbox.views.generic.base import BaseObjectView
 from netbox.views.generic.base import BaseObjectView
 from netbox.views.generic.mixins import TableMixin
 from netbox.views.generic.mixins import TableMixin
@@ -48,6 +59,7 @@ from .jobs import SyncDataSourceJob
 from .models import *
 from .models import *
 from .plugins import get_catalog_plugins, get_local_plugins
 from .plugins import get_catalog_plugins, get_local_plugins
 from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
 from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
+from .ui import panels
 
 
 #
 #
 # Data sources
 # Data sources
@@ -67,6 +79,24 @@ class DataSourceListView(generic.ObjectListView):
 @register_model_view(DataSource)
 @register_model_view(DataSource)
 class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
 class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = DataSource.objects.all()
     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):
     def get_extra_context(self, request, instance):
         return {
         return {
@@ -157,6 +187,14 @@ class DataFileListView(generic.ObjectListView):
 class DataFileView(generic.ObjectView):
 class DataFileView(generic.ObjectView):
     queryset = DataFile.objects.all()
     queryset = DataFile.objects.all()
     actions = (DeleteObject,)
     actions = (DeleteObject,)
+    layout = layout.Layout(
+        layout.Row(
+            layout.Column(
+                panels.DataFilePanel(),
+                panels.DataFileContentPanel(),
+            ),
+        ),
+    )
 
 
 
 
 @register_model_view(DataFile, 'delete')
 @register_model_view(DataFile, 'delete')
@@ -188,6 +226,17 @@ class JobListView(generic.ObjectListView):
 class JobView(generic.ObjectView):
 class JobView(generic.ObjectView):
     queryset = Job.objects.all()
     queryset = Job.objects.all()
     actions = (DeleteObject,)
     actions = (DeleteObject,)
+    layout = layout.SimpleLayout(
+        left_panels=[
+            panels.JobPanel(),
+        ],
+        right_panels=[
+            panels.JobSchedulingPanel(),
+        ],
+        bottom_panels=[
+            JSONPanel('data', title=_('Data')),
+        ],
+    )
 
 
 
 
 @register_model_view(Job, 'log')
 @register_model_view(Job, 'log')
@@ -200,6 +249,13 @@ class JobLogView(generic.ObjectView):
         badge=lambda obj: len(obj.log_entries),
         badge=lambda obj: len(obj.log_entries),
         weight=500,
         weight=500,
     )
     )
+    layout = layout.Layout(
+        layout.Row(
+            layout.Column(
+                ContextTablePanel('table', title=_('Log Entries')),
+            ),
+        ),
+    )
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
         table = JobLogEntryTable(instance.log_entries)
         table = JobLogEntryTable(instance.log_entries)
@@ -241,6 +297,26 @@ class ObjectChangeListView(generic.ObjectListView):
 @register_model_view(ObjectChange)
 @register_model_view(ObjectChange)
 class ObjectChangeView(generic.ObjectView):
 class ObjectChangeView(generic.ObjectView):
     queryset = None
     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):
     def get_queryset(self, request):
         return ObjectChange.objects.valid_models()
         return ObjectChange.objects.valid_models()
@@ -312,6 +388,14 @@ class ConfigRevisionListView(generic.ObjectListView):
 @register_model_view(ConfigRevision)
 @register_model_view(ConfigRevision)
 class ConfigRevisionView(generic.ObjectView):
 class ConfigRevisionView(generic.ObjectView):
     queryset = ConfigRevision.objects.all()
     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):
     def get_extra_context(self, request, instance):
         """
         """

+ 0 - 22
netbox/templates/core/configrevision.html

@@ -1,10 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load buttons %}
 {% load buttons %}
-{% load custom_links %}
 {% load helpers %}
 {% load helpers %}
 {% load perms %}
 {% load perms %}
-{% load plugins %}
-{% load static %}
 {% load i18n %}
 {% load i18n %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
@@ -27,22 +24,3 @@
     </div>
     </div>
   {% endif %}
   {% endif %}
 {% endblock subtitle %}
 {% endblock subtitle %}
-
-{% block content %}
-  <div class="row">
-    <div class="col col-md-12">
-      <div class="card">
-        <h2 class="card-header">{% trans "Configuration Data" %}</h2>
-        {% include 'core/inc/config_data.html' %}
-      </div>
-
-      <div class="card">
-        <h2 class="card-header">{% trans "Comment" %}</h2>
-        <div class="card-body">
-          {{ object.comment|placeholder }}
-        </div>
-      </div>
-
-    </div>
-  </div>
-{% endblock %}

+ 0 - 55
netbox/templates/core/datafile.html

@@ -1,62 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
-{% load buttons %}
-{% load custom_links %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
 {% load i18n %}
 {% load i18n %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   {{ block.super }}
   {{ block.super }}
   <li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li>
   <li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li>
 {% endblock %}
 {% endblock %}
-
-{% block content %}
-  <div class="row mb-3">
-    <div class="col">
-      <div class="card">
-        <h2 class="card-header">{% trans "Data File" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Source" %}</th>
-            <td>{{ object.source|linkify }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Path" %}</th>
-            <td>
-              <span class="font-monospace" id="datafile_path">{{ object.path }}</span>
-              {% copy_content "datafile_path" %}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Last Updated" %}</th>
-            <td>{{ object.last_updated }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Size" %}</th>
-            <td>{{ object.size }} {% trans "bytes" %}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "SHA256 Hash" %}</th>
-            <td>
-            <span class="font-monospace" id="datafile_hash">{{ object.hash }}</span>
-              {% copy_content "datafile_hash" %}
-            </td>
-          </tr>
-        </table>
-      </div>
-      <div class="card">
-        <h2 class="card-header">{% trans "Content" %}</h2>
-        <div class="card-body">
-          <pre>{{ object.data_as_string }}</pre>
-        </div>
-      </div>
-      {% plugin_left_page object %}
-    </div>
-  </div>
-  <div class="row mb-3">
-    <div class="col col-md-12">
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 1 - 0
netbox/templates/core/datafile/attrs/size.html

@@ -0,0 +1 @@
+{% load i18n %}{{ value }} {% trans "bytes" %}

+ 0 - 103
netbox/templates/core/datasource.html

@@ -1,8 +1,4 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
-{% load static %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
 {% load i18n %}
 {% load i18n %}
 
 
 {% block extra_controls %}
 {% block extra_controls %}
@@ -23,102 +19,3 @@
     {% endif %}
     {% endif %}
   {% endif %}
   {% endif %}
 {% endblock %}
 {% endblock %}
-
-{% block content %}
-  <div class="row mb-3">
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Data Source" %}</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 "Type" %}</th>
-            <td>{{ object.get_type_display }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Enabled" %}</th>
-            <td>{% checkmark object.enabled %}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Status" %}</th>
-            <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Sync interval" %}</th>
-            <td>{{ object.get_sync_interval_display|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Last synced" %}</th>
-            <td>{{ object.last_synced|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Description" %}</th>
-            <td>{{ object.description|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "URL" %}</th>
-            <td>
-              {% if not object.type.is_local %}
-                <a href="{{ object.source_url }}">{{ object.source_url }}</a>
-              {% else %}
-                {{ object.source_url }}
-              {% endif %}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Ignore rules" %}</th>
-            <td>
-              {% if object.ignore_rules %}
-                <pre>{{ object.ignore_rules }}</pre>
-              {% else %}
-                {{ ''|placeholder }}
-              {% endif %}</td>
-          </tr>
-        </table>
-      </div>
-      {% include 'inc/panels/tags.html' %}
-      {% 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 "Backend" %}</h2>
-          {% with backend=object.backend_class %}
-            <table class="table table-hover attr-table">
-              {% for name, field in backend.parameters.items %}
-                <tr>
-                  <th scope="row">{{ field.label }}</th>
-                  {% if name in backend.sensitive_parameters %}
-                    <td>********</td>
-                  {% else %}
-                    <td>{{ object.parameters|get_key:name|placeholder }}</td>
-                  {% endif %}
-                </tr>
-              {% empty %}
-                <tr>
-                  <td colspan="2" class="text-muted">
-                    {% trans "No parameters defined" %}
-                  </td>
-                </tr>
-              {% endfor %}
-            </table>
-          {% endwith %}
-      </div>
-      {% include 'inc/panels/related_objects.html' %}
-      {% include 'inc/panels/custom_fields.html' %}
-      {% plugin_right_page object %}
-    </div>
-  </div>
-  <div class="row mb-3">
-    <div class="col col-md-12">
-      <div class="card">
-        <h2 class="card-header">{% trans "Files" %}</h2>
-        {% htmx_table 'core:datafile_list' source_id=object.pk %}
-      </div>
-      {% plugin_full_width_page object %}
-    </div>
-  </div>
-{% endblock %}

+ 1 - 0
netbox/templates/core/datasource/attrs/ignore_rules.html

@@ -0,0 +1 @@
+<pre>{{ value }}</pre>

+ 1 - 0
netbox/templates/core/datasource/attrs/source_url.html

@@ -0,0 +1 @@
+{% if not object.type.is_local %}<a href="{{ value }}">{{ value }}</a>{% else %}{{ value }}{% endif %}

+ 0 - 77
netbox/templates/core/job.html

@@ -1,78 +1 @@
 {% extends 'core/job/base.html' %}
 {% extends 'core/job/base.html' %}
-{% 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 "Job" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Object Type" %}</th>
-            <td>
-              <a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object_type }}</a>
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Name" %}</th>
-            <td>{{ object.name|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Status" %}</th>
-            <td>{% badge object.get_status_display object.get_status_color %}</td>
-          </tr>
-          {% if object.error %}
-            <tr>
-              <th scope="row">{% trans "Error" %}</th>
-              <td>{{ object.error }}</td>
-            </tr>
-          {% endif %}
-          <tr>
-            <th scope="row">{% trans "Created By" %}</th>
-            <td>{{ object.user|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-    </div>
-    <div class="col col-12 col-md-6">
-      <div class="card">
-        <h2 class="card-header">{% trans "Scheduling" %}</h2>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Created" %}</th>
-            <td>{{ object.created|isodatetime }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Scheduled" %}</th>
-            <td>
-              {{ object.scheduled|isodatetime|placeholder }}
-              {% if object.interval %}
-                ({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %})
-              {% endif %}
-            </td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Started" %}</th>
-            <td>{{ object.started|isodatetime|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Completed" %}</th>
-            <td>{{ object.completed|isodatetime|placeholder }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Queue" %}</th>
-            <td>{{ object.queue_name|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-    </div>
-  </div>
-  <div class="row">
-    <div class="col col-12">
-      <div class="card">
-        <h2 class="card-header">{% trans "Data" %}</h2>
-        <pre class="card-body m-0">{{ object.data|json }}</pre>
-      </div>
-    </div>
-  </div>
-{% endblock %}

+ 1 - 0
netbox/templates/core/job/attrs/object_type.html

@@ -0,0 +1 @@
+<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ value }}</a>

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

+ 0 - 11
netbox/templates/core/job/log.html

@@ -1,12 +1 @@
 {% extends 'core/job/base.html' %}
 {% extends 'core/job/base.html' %}
-{% load render_table from django_tables2 %}
-
-{% block content %}
-  <div class="row mb-3">
-    <div class="col">
-      <div class="card">
-        {% render_table table %}
-      </div>
-    </div>
-  </div>
-{% endblock %}

+ 0 - 160
netbox/templates/core/objectchange.html

@@ -1,6 +1,4 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
 {% load i18n %}
 {% load i18n %}
 
 
 {% block title %}{{ object }}{% endblock %}
 {% block title %}{{ object }}{% endblock %}
@@ -21,161 +19,3 @@
 {# ObjectChange does not support the default add/edit/delete controls #}
 {# ObjectChange does not support the default add/edit/delete controls #}
 {% block control-buttons %}{% endblock %}
 {% block control-buttons %}{% endblock %}
 {% block subtitle %}{% endblock %}
 {% block subtitle %}{% endblock %}
-
-{% block content %}
-<div class="row">
-    <div class="col col-12 col-md-5">
-        <div class="card">
-            <h2 class="card-header">{% trans "Change" %}</h2>
-            <table class="table table-hover attr-table">
-                <tr>
-                    <th scope="row">{% trans "Time" %}</th>
-                    <td>{{ object.time|isodatetime }}</td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "User" %}</th>
-                    <td>
-                        {% if object.user.get_full_name %}
-                          {{ object.user.get_full_name }} ({{ object.user_name }})
-                        {% else %}
-                          {{ object.user_name }}
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Action" %}</th>
-                    <td>
-                        {{ object.get_action_display }}
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Object Type" %}</th>
-                    <td>
-                        {{ object.changed_object_type }}
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Object" %}</th>
-                    <td>
-                        {% if object.changed_object and object.changed_object.get_absolute_url %}
-                            {{ object.changed_object|linkify }}
-                        {% else %}
-                            {{ object.object_repr }}
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Message" %}</th>
-                    <td>
-                        {{ object.message|placeholder }}
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">{% trans "Request ID" %}</th>
-                    <td>
-                        <a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}">{{ object.request_id }}</a>
-                    </td>
-                </tr>
-            </table>
-        </div>
-    </div>
-    <div class="col col-12 col-md-7">
-        <div class="card">
-            <h2 class="card-header d-flex justify-content-between">
-              {% trans "Difference" %}
-              <div class="btn-group btn-group-sm d-print-none">
-                <a {% if prev_change %}href="{% url 'core:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
-                  <i class="mdi mdi-chevron-left" aria-hidden="true"></i> {% trans "Previous" %}
-                </a>
-                <a {% if next_change %}href="{% url 'core:objectchange' pk=next_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
-                  {% trans "Next" %} <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
-                </a>
-              </div>
-            </h2>
-            <div class="card-body">
-                {% if diff_added == diff_removed %}
-                    <span class="text-muted" style="margin-left: 10px;">
-                        {% if object.action == 'create' %}
-                            {% trans "Object Created" %}
-                        {% elif object.action == 'delete' %}
-                            {% trans "Object Deleted" %}
-                        {% else %}
-                            {% trans "No Changes" %}
-                        {% endif %}
-                    </span>
-                {% else %}
-                    <pre class="change-diff change-removed">{{ diff_removed|json }}</pre>
-                    <pre class="change-diff change-added">{{ diff_added|json }}</pre>
-                {% endif %}
-            </div>
-        </div>
-    </div>
-</div>
-<div class="row">
-    <div class="col col-12 col-md-6">
-        <div class="card">
-            <h2 class="card-header">{% trans "Pre-Change Data" %}</h2>
-            <div class="card-body">
-            {% if object.prechange_data %}
-              {% spaceless %}
-                <pre class="change-data">
-                  {% for k, v in object.prechange_data_clean.items %}
-                    <span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
-                  {% endfor %}
-                </pre>
-              {% endspaceless %}
-            {% elif non_atomic_change %}
-              {% trans "Warning: Comparing non-atomic change to previous change record" %} (<a href="{% url 'core:objectchange' pk=prev_change.pk %}">{{ prev_change.pk }}</a>)
-            {% else %}
-              <span class="text-muted">{% trans "None" %}</span>
-            {% endif %}
-            </div>
-        </div>
-    </div>
-    <div class="col col-12 col-md-6">
-        <div class="card">
-            <h2 class="card-header">{% trans "Post-Change Data" %}</h2>
-            <div class="card-body">
-                {% if object.postchange_data %}
-                  {% spaceless %}
-                    <pre class="change-data">
-                      {% for k, v in object.postchange_data_clean.items %}
-                        <span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
-                      {% endfor %}
-                    </pre>
-                  {% endspaceless %}
-                {% else %}
-                  <span class="text-muted">{% trans "None" %}</span>
-                {% endif %}
-            </div>
-        </div>
-    </div>
-</div>
-<div class="row">
-  <div class="col col-12 col-md-6">
-    {% plugin_left_page object %}
-  </div>
-  <div class="col col-12 col-md-6">
-    {% plugin_right_page object %}
-  </div>
-</div>
-<div class="row">
-    <div class="col col-md-12">
-        {% 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 %}
-            <div class="float-end">
-                <a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}" class="btn btn-primary">
-                  {% blocktrans trimmed with count=related_changes_count|add:"1" %}
-                    See All {{ count }} Changes
-                  {% endblocktrans %}
-                </a>
-            </div>
-        {% endif %}
-    </div>
-</div>
-<div class="row">
-  <div class="col col-md-12">
-    {% plugin_full_width_page object %}
-  </div>
-</div>
-{% endblock %}

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

+ 1 - 0
netbox/templates/core/objectchange/attrs/request_id.html

@@ -0,0 +1 @@
+<a href="{% url 'core:objectchange_list' %}?request_id={{ value }}">{{ value }}</a>

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

+ 11 - 0
netbox/templates/core/panels/configrevision_comment.html

@@ -0,0 +1,11 @@
+{% load i18n %}
+<div class="card">
+  <h2 class="card-header">{% trans "Comment" %}</h2>
+  <div class="card-body">
+    {% if object.comment %}
+      {{ object.comment }}
+    {% else %}
+      <span class="text-muted">&mdash;</span>
+    {% endif %}
+  </div>
+</div>

+ 5 - 0
netbox/templates/core/panels/configrevision_data.html

@@ -0,0 +1,5 @@
+{% load i18n %}
+<div class="card">
+  <h2 class="card-header">{% trans "Configuration Data" %}</h2>
+  {% include 'core/inc/config_data.html' %}
+</div>

+ 8 - 0
netbox/templates/core/panels/datafile_content.html

@@ -0,0 +1,8 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+  <div class="card-body">
+    <pre>{{ object.data_as_string }}</pre>
+  </div>
+{% endblock panel_content %}

+ 26 - 0
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 %}
+    <table class="table table-hover attr-table">
+      {% for name, field in backend.parameters.items %}
+        <tr>
+          <th scope="row">{{ field.label }}</th>
+          {% if name in backend.sensitive_parameters %}
+            <td>********</td>
+          {% else %}
+            <td>{{ object.parameters|get_key:name|placeholder }}</td>
+          {% endif %}
+        </tr>
+      {% empty %}
+        <tr>
+          <td colspan="2" class="text-muted">
+            {% trans "No parameters defined" %}
+          </td>
+        </tr>
+      {% endfor %}
+    </table>
+  {% endwith %}
+{% endblock panel_content %}

+ 31 - 0
netbox/templates/core/panels/objectchange_difference.html

@@ -0,0 +1,31 @@
+{% load helpers %}
+{% load i18n %}
+<div class="card">
+  <h2 class="card-header d-flex justify-content-between">
+    {% trans "Difference" %}
+    <div class="btn-group btn-group-sm d-print-none">
+      <a {% if prev_change %}href="{% url 'core:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
+        <i class="mdi mdi-chevron-left" aria-hidden="true"></i> {% trans "Previous" %}
+      </a>
+      <a {% if next_change %}href="{% url 'core:objectchange' pk=next_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
+        {% trans "Next" %} <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
+      </a>
+    </div>
+  </h2>
+  <div class="card-body">
+    {% if diff_added == diff_removed %}
+      <span class="text-muted" style="margin-left: 10px;">
+        {% if object.action == 'create' %}
+          {% trans "Object Created" %}
+        {% elif object.action == 'delete' %}
+          {% trans "Object Deleted" %}
+        {% else %}
+          {% trans "No Changes" %}
+        {% endif %}
+      </span>
+    {% else %}
+      <pre class="change-diff change-removed">{{ diff_removed|json }}</pre>
+      <pre class="change-diff change-added">{{ diff_added|json }}</pre>
+    {% endif %}
+  </div>
+</div>

+ 18 - 0
netbox/templates/core/panels/objectchange_postchange.html

@@ -0,0 +1,18 @@
+{% load helpers %}
+{% load i18n %}
+<div class="card">
+  <h2 class="card-header">{% trans "Post-Change Data" %}</h2>
+  <div class="card-body">
+    {% if object.postchange_data %}
+      {% spaceless %}
+        <pre class="change-data">
+          {% for k, v in object.postchange_data_clean.items %}
+            <span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
+          {% endfor %}
+        </pre>
+      {% endspaceless %}
+    {% else %}
+      <span class="text-muted">{% trans "None" %}</span>
+    {% endif %}
+  </div>
+</div>

+ 20 - 0
netbox/templates/core/panels/objectchange_prechange.html

@@ -0,0 +1,20 @@
+{% load helpers %}
+{% load i18n %}
+<div class="card">
+  <h2 class="card-header">{% trans "Pre-Change Data" %}</h2>
+  <div class="card-body">
+    {% if object.prechange_data %}
+      {% spaceless %}
+        <pre class="change-data">
+          {% for k, v in object.prechange_data_clean.items %}
+            <span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
+          {% endfor %}
+        </pre>
+      {% endspaceless %}
+    {% elif non_atomic_change %}
+      {% trans "Warning: Comparing non-atomic change to previous change record" %} (<a href="{% url 'core:objectchange' pk=prev_change.pk %}">{{ prev_change.pk }}</a>)
+    {% else %}
+      <span class="text-muted">{% trans "None" %}</span>
+    {% endif %}
+  </div>
+</div>

+ 11 - 0
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 %}
+  <div class="float-end">
+    <a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}" class="btn btn-primary">
+      {% blocktrans trimmed with count=related_changes_count|add:"1" %}
+        See All {{ count }} Changes
+      {% endblocktrans %}
+    </a>
+  </div>
+{% endif %}

+ 1 - 1
netbox/templates/inc/panel_table.html

@@ -3,7 +3,7 @@
 
 
 <div class="card {% if panel_class %}border-{{ panel_class }}{% endif %}">
 <div class="card {% if panel_class %}border-{{ panel_class }}{% endif %}">
   {% if heading %}
   {% if heading %}
-    <h2 class="card-header{% if panel_class %} text-{{ panel_class }}{% endif %}">{{ heading }}</h2>
+    <h2 class="card-header{% if panel_class %} text-{{ panel_class }}{% endif %}">{% trans heading %}</h2>
   {% endif %}
   {% endif %}
   {% if table.rows %}
   {% if table.rows %}
     <div class="table-responsive">
     <div class="table-responsive">