Quellcode durchsuchen

Closes #15915: Replace plugins list with an overall system status view (#15950)

* Replace plugins list with an overall system status view

* Enable export of system status data
Jeremy Stretch vor 1 Jahr
Ursprung
Commit
8e1c2ecd92

+ 1 - 1
netbox/core/tables/plugins.py

@@ -35,5 +35,5 @@ class PluginTable(BaseTable):
             'name', 'version', 'package', 'author', 'author_email', 'description',
             'name', 'version', 'package', 'author', 'author_email', 'description',
         )
         )
         default_columns = (
         default_columns = (
-            'name', 'version', 'package', 'author', 'author_email', 'description',
+            'name', 'version', 'package', 'description',
         )
         )

+ 2 - 5
netbox/core/urls.py

@@ -43,9 +43,6 @@ urlpatterns = (
     path('config-revisions/<int:pk>/restore/', views.ConfigRevisionRestoreView.as_view(), name='configrevision_restore'),
     path('config-revisions/<int:pk>/restore/', views.ConfigRevisionRestoreView.as_view(), name='configrevision_restore'),
     path('config-revisions/<int:pk>/', include(get_model_urls('core', 'configrevision'))),
     path('config-revisions/<int:pk>/', include(get_model_urls('core', 'configrevision'))),
 
 
-    # Configuration
-    path('config/', views.ConfigView.as_view(), name='config'),
-
-    # Plugins
-    path('plugins/', views.PluginListView.as_view(), name='plugin_list'),
+    # System
+    path('system/', views.SystemView.as_view(), name='system'),
 )
 )

+ 62 - 23
netbox/core/views.py

@@ -1,14 +1,19 @@
+import json
+import platform
+
+from django import __version__ as DJANGO_VERSION
 from django.apps import apps
 from django.apps import apps
 from django.conf import settings
 from django.conf import settings
 from django.contrib import messages
 from django.contrib import messages
 from django.contrib.auth.mixins import UserPassesTestMixin
 from django.contrib.auth.mixins import UserPassesTestMixin
 from django.core.cache import cache
 from django.core.cache import cache
-from django.http import HttpResponseForbidden, Http404
+from django.db import connection, ProgrammingError
+from django.http import HttpResponse, HttpResponseForbidden, Http404
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import View
 from django.views.generic import View
-from django_rq.queues import get_queue_by_index, get_redis_connection
+from django_rq.queues import get_connection, get_queue_by_index, get_redis_connection
 from django_rq.settings import QUEUES_MAP, QUEUES_LIST
 from django_rq.settings import QUEUES_MAP, QUEUES_LIST
 from django_rq.utils import get_jobs, get_statistics, stop_jobs
 from django_rq.utils import get_jobs, get_statistics, stop_jobs
 from rq import requeue_job
 from rq import requeue_job
@@ -175,20 +180,6 @@ class JobBulkDeleteView(generic.BulkDeleteView):
 # Config Revisions
 # Config Revisions
 #
 #
 
 
-class ConfigView(generic.ObjectView):
-    queryset = ConfigRevision.objects.all()
-
-    def get_object(self, **kwargs):
-        revision_id = cache.get('config_version')
-        try:
-            return ConfigRevision.objects.get(pk=revision_id)
-        except ConfigRevision.DoesNotExist:
-            # Fall back to using the active config data if no record is found
-            return ConfigRevision(
-                data=get_config().defaults
-            )
-
-
 class ConfigRevisionListView(generic.ObjectListView):
 class ConfigRevisionListView(generic.ObjectListView):
     queryset = ConfigRevision.objects.all()
     queryset = ConfigRevision.objects.all()
     filterset = filtersets.ConfigRevisionFilterSet
     filterset = filtersets.ConfigRevisionFilterSet
@@ -527,21 +518,69 @@ class WorkerView(BaseRQView):
 # Plugins
 # Plugins
 #
 #
 
 
-class PluginListView(UserPassesTestMixin, View):
+class SystemView(UserPassesTestMixin, View):
 
 
     def test_func(self):
     def test_func(self):
         return self.request.user.is_staff
         return self.request.user.is_staff
 
 
     def get(self, request):
     def get(self, request):
+
+        # System stats
+        psql_version = db_name = db_size = None
+        try:
+            with connection.cursor() as cursor:
+                cursor.execute("SELECT version()")
+                psql_version = cursor.fetchone()[0]
+                psql_version = psql_version.split('(')[0].strip()
+                cursor.execute("SELECT current_database()")
+                db_name = cursor.fetchone()[0]
+                cursor.execute(f"SELECT pg_size_pretty(pg_database_size('{db_name}'))")
+                db_size = cursor.fetchone()[0]
+        except (ProgrammingError, IndexError):
+            pass
+        stats = {
+            'netbox_version': settings.VERSION,
+            'django_version': DJANGO_VERSION,
+            'python_version': platform.python_version(),
+            'postgresql_version': psql_version,
+            'database_name': db_name,
+            'database_size': db_size,
+            'rq_worker_count': Worker.count(get_connection('default')),
+        }
+
+        # Plugins
         plugins = [
         plugins = [
             # Look up app config by package name
             # Look up app config by package name
             apps.get_app_config(plugin.rsplit('.', 1)[-1]) for plugin in settings.PLUGINS
             apps.get_app_config(plugin.rsplit('.', 1)[-1]) for plugin in settings.PLUGINS
         ]
         ]
-        table = tables.PluginTable(plugins, user=request.user)
-        table.configure(request)
 
 
-        return render(request, 'core/plugin_list.html', {
-            'plugins': plugins,
-            'active_tab': 'api-tokens',
-            'table': table,
+        # Configuration
+        try:
+            config = ConfigRevision.objects.get(pk=cache.get('config_version'))
+        except ConfigRevision.DoesNotExist:
+            # Fall back to using the active config data if no record is found
+            config = ConfigRevision(data=get_config().defaults)
+
+        # Raw data export
+        if 'export' in request.GET:
+            data = {
+                **stats,
+                'plugins': {
+                    plugin.name: plugin.version for plugin in plugins
+                },
+                'config': {
+                    k: config.data[k] for k in sorted(config.data)
+                },
+            }
+            response = HttpResponse(json.dumps(data, indent=4), content_type='text/json')
+            response['Content-Disposition'] = 'attachment; filename="netbox.json"'
+            return response
+
+        plugins_table = tables.PluginTable(plugins, orderable=False)
+        plugins_table.configure(request)
+
+        return render(request, 'core/system.html', {
+            'stats': stats,
+            'plugins_table': plugins_table,
+            'config': config,
         })
         })

+ 4 - 14
netbox/netbox/navigation/menu.py

@@ -421,27 +421,17 @@ ADMIN_MENU = Menu(
             ),
             ),
         ),
         ),
         MenuGroup(
         MenuGroup(
-            label=_('Configuration'),
+            label=_('System'),
             items=(
             items=(
                 MenuItem(
                 MenuItem(
-                    link='core:config',
-                    link_text=_('Current Config'),
-                    permissions=['core.view_configrevision']
+                    link='core:system',
+                    link_text=_('System')
                 ),
                 ),
                 MenuItem(
                 MenuItem(
                     link='core:configrevision_list',
                     link='core:configrevision_list',
-                    link_text=_('Config Revisions'),
+                    link_text=_('Configuration History'),
                     permissions=['core.view_configrevision']
                     permissions=['core.view_configrevision']
                 ),
                 ),
-            ),
-        ),
-        MenuGroup(
-            label=_('System'),
-            items=(
-                MenuItem(
-                    link='core:plugin_list',
-                    link_text=_('Plugins')
-                ),
                 MenuItem(
                 MenuItem(
                     link='core:background_queue_list',
                     link='core:background_queue_list',
                     link_text=_('Background Tasks')
                     link_text=_('Background Tasks')

+ 2 - 157
netbox/templates/core/configrevision.html

@@ -32,163 +32,8 @@
   <div class="row">
   <div class="row">
     <div class="col col-md-12">
     <div class="col col-md-12">
       <div class="card">
       <div class="card">
-        <h5 class="card-header">{% trans "Rack Elevations" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Default unit height" %}</th>
-            <td>{{ object.data.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Default unit width" %}</th>
-            <td>{{ object.data.RACK_ELEVATION_DEFAULT_UNIT_WIDTH }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Power Feeds" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Default voltage" %}</th>
-            <td>{{ object.data.POWERFEED_DEFAULT_VOLTAGE }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Default amperage" %}</th>
-            <td>{{ object.data.POWERFEED_DEFAULT_AMPERAGE }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Default max utilization" %}</th>
-            <td>{{ object.data.POWERFEED_DEFAULT_MAX_UTILIZATION }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "IPAM" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Enforce global unique" %}</th>
-            <td>{{ object.data.ENFORCE_GLOBAL_UNIQUE }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Prefer IPv4" %}</th>
-            <td>{{ object.data.PREFER_IPV4 }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Security" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Allowed URL schemes" %}</th>
-            <td>{{ object.data.ALLOWED_URL_SCHEMES|join:", "|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Banners" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Login banner" %}</th>
-            <td>{{ object.data.BANNER_LOGIN }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Maintenance banner" %}</th>
-            <td>{{ object.data.BANNER_MAINTENANCE }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Top banner" %}</th>
-            <td>{{ object.data.BANNER_TOP }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Bottom banner" %}</th>
-            <td>{{ object.data.BANNER_BOTTOM }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Pagination" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Paginate count" %}</th>
-            <td>{{ object.data.PAGINATE_COUNT }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Max page size" %}</th>
-            <td>{{ object.data.MAX_PAGE_SIZE }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Validation" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Custom validators" %}</th>
-            {% if object.data.CUSTOM_VALIDATORS %}
-              <td class="font-monospace">
-                <pre>{{ object.data.CUSTOM_VALIDATORS|json }}</pre>
-              </td>
-            {% else %}
-              <td>{{ ''|placeholder }}</td>
-            {% endif %}
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Protection rules" %}</th>
-            {% if object.data.PROTECTION_RULES %}
-              <td>
-                <pre>{{ object.data.PROTECTION_RULES|json }}</pre>
-              </td>
-            {% else %}
-              <td>{{ ''|placeholder }}</td>
-            {% endif %}
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "User Preferences" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Default user preferences" %}</th>
-            {% if object.data.DEFAULT_USER_PREFERENCES %}
-              <td>
-                <pre>{{ object.data.DEFAULT_USER_PREFERENCES|json }}</pre>
-              </td>
-            {% else %}
-              <td>{{ ''|placeholder }}</td>
-            {% endif %}
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Miscellaneous" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Maintenance mode" %}</th>
-            <td>{{ object.data.MAINTENANCE_MODE }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "GraphQL enabled" %}</th>
-            <td>{{ object.data.GRAPHQL_ENABLED }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Changelog retention" %}</th>
-            <td>{{ object.data.CHANGELOG_RETENTION }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Job retention" %}</th>
-            <td>{{ object.data.JOB_RETENTION }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Maps URL" %}</th>
-            <td>{{ object.data.MAPS_URL }}</td>
-          </tr>
-        </table>
+        <h5 class="card-header">{% trans "Configuration Data" %}</h5>
+        {% include 'core/inc/config_data.html' with config=config.data %}
       </div>
       </div>
 
 
       <div class="card">
       <div class="card">

+ 149 - 0
netbox/templates/core/inc/config_data.html

@@ -0,0 +1,149 @@
+{% load i18n %}
+
+<table class="table table-hover attr-table">
+
+  {# Rack elevations #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Rack elevations" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Default unit height" %}</th>
+    <td>{{ config.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Default unit width" %}</th>
+    <td class="border-0">{{ config.RACK_ELEVATION_DEFAULT_UNIT_WIDTH }}</td>
+  </tr>
+
+  {# Power feeds #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Power feeds" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Default voltage" %}</th>
+    <td>{{ config.POWERFEED_DEFAULT_VOLTAGE }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Default amperage" %}</th>
+    <td>{{ config.POWERFEED_DEFAULT_AMPERAGE }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Default max utilization" %}</th>
+    <td class="border-0">{{ config.POWERFEED_DEFAULT_MAX_UTILIZATION }}</td>
+  </tr>
+
+  {# IPAM #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "IPAM" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Enforce global unique" %}</th>
+    <td>{% checkmark config.ENFORCE_GLOBAL_UNIQUE %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Prefer IPv4" %}</th>
+    <td class="border-0">{% checkmark config.PREFER_IPV4 %}</td>
+  </tr>
+
+  {# Security #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Security" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Allowed URL schemes" %}</th>
+    <td class="border-0">{{ config.ALLOWED_URL_SCHEMES|join:", "|placeholder }}</td>
+  </tr>
+
+  {# Banners #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Banners" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Login banner" %}</th>
+    <td>{{ config.BANNER_LOGIN|placeholder }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Maintenance banner" %}</th>
+    <td>{{ config.BANNER_MAINTENANCE|placeholder }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Top banner" %}</th>
+    <td>{{ config.BANNER_TOP|placeholder }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Bottom banner" %}</th>
+    <td class="border-0">{{ config.BANNER_BOTTOM|placeholder }}</td>
+  </tr>
+
+  {# Pagination #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Pagination" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Paginate count" %}</th>
+    <td>{{ config.PAGINATE_COUNT }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Max page size" %}</th>
+    <td class="border-0">{{ config.MAX_PAGE_SIZE }}</td>
+  </tr>
+
+  {# Validation #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Validation" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Custom validators" %}</th>
+    {% if config.CUSTOM_VALIDATORS %}
+      <td><pre>{{ config.CUSTOM_VALIDATORS|json }}</pre></td>
+    {% else %}
+      <td>{{ ''|placeholder }}</td>
+    {% endif %}
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Protection rules" %}</th>
+    {% if config.PROTECTION_RULES %}
+      <td class="border-0"><pre>{{ config.PROTECTION_RULES|json }}</pre></td>
+    {% else %}
+      <td class="border-0">{{ ''|placeholder }}</td>
+    {% endif %}
+  </tr>
+
+  {# User Preferences #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "User preferences" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Default preferences" %}</th>
+    {% if config.DEFAULT_USER_PREFERENCES %}
+      <td class="border-0"><pre>{{ config.DEFAULT_USER_PREFERENCES|json }}</pre></td>
+    {% else %}
+      <td class="border-0">{{ ''|placeholder }}</td>
+    {% endif %}
+  </tr>
+
+  {# Miscellaneous #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Miscellaneous" %}</td>
+  <tr>
+  <th scope="row" class="ps-3">{% trans "Maintenance mode" %}</th>
+    <td>{% checkmark config.MAINTENANCE_MODE %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "GraphQL enabled" %}</th>
+    <td>{% checkmark config.GRAPHQL_ENABLED %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Changelog retention" %}</th>
+    <td>{{ config.CHANGELOG_RETENTION }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Job retention" %}</th>
+    <td>{{ config.JOB_RETENTION }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Maps URL" %}</th>
+    <td class="border-0">{{ config.MAPS_URL }}</td>
+  </tr>
+
+</table>

+ 0 - 36
netbox/templates/core/plugin_list.html

@@ -1,36 +0,0 @@
-{% extends 'generic/_base.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load i18n %}
-{% load render_table from django_tables2 %}
-
-{% block title %}{% trans "Plugins" %}{% endblock %}
-
-{% block tabs %}
-  <ul class="nav nav-tabs px-3">
-    <li class="nav-item" role="presentation">
-      <a class="nav-link active" role="tab">{% trans "Plugins" %}</a>
-    </li>
-  </ul>
-{% endblock tabs %}
-
-{% block content %}
-  <div class="row mb-3">
-    <div class="col-auto ms-auto d-print-none">
-      {# Table configuration button #}
-      <div class="table-configure input-group">
-        <button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#ObjectTable_config" class="btn">
-          <i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
-        </button>
-      </div>
-    </div>
-  </div>
-
-  <div class="card">
-    {% render_table table %}
-  </div>
-{% endblock content %}
-
-{% block modals %}
-  {% table_config_form table table_name="ObjectTable" %}
-{% endblock modals %}

+ 92 - 0
netbox/templates/core/system.html

@@ -0,0 +1,92 @@
+{% extends 'generic/_base.html' %}
+{% load buttons %}
+{% load helpers %}
+{% load i18n %}
+{% load render_table from django_tables2 %}
+
+{% block title %}{% trans "System" %}{% endblock %}
+
+{% block controls %}
+  <a href="?export=true" class="btn btn-purple">
+    <i class="mdi mdi-download"></i> {% trans "Export" %}
+  </a>
+{% endblock controls %}
+
+{% block tabs %}
+  <ul class="nav nav-tabs px-3">
+    <li class="nav-item" role="presentation">
+      <a class="nav-link active" role="tab">{% trans "Status" %}</a>
+    </li>
+  </ul>
+{% endblock tabs %}
+
+{% block content %}
+  {# System status #}
+  <div class="row mb-3">
+    <div class="col">
+      <div class="card">
+        <h5 class="card-header">{% trans "System Status" %}</h5>
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">{% trans "NetBox version" %}</th>
+            <td>{{ stats.netbox_version }}</td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "Django version" %}</th>
+            <td>{{ stats.django_version }}</td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "PotsgreSQL version" %}</th>
+            <td>{{ stats.postgresql_version }}</td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "Database name" %}</th>
+            <td>{{ stats.database_name }}</td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "Database size" %}</th>
+            <td>
+              {% if stats.database_size %}
+                {{ stats.database_size }}
+              {% else %}
+                <span class="text-muted">{% trans "Unavailable" %}</span>
+              {% endif %}
+            </td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "RQ workers" %}</th>
+            <td>
+              <a href="{% url 'core:background_queue_list' %}">{{ stats.rq_worker_count }}</a>
+              ({% trans "default queue" %})
+            </td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "System time" %}</th>
+            <td>{% now 'Y-m-d H:i:s T' %}</td>
+          </tr>
+        </table>
+      </div>
+    </div>
+  </div>
+
+  {# Plugins #}
+  <div class="row mb-3">
+    <div class="col col-md-12">
+      <div class="card">
+        <h5 class="card-header">{% trans "Plugins" %}</h5>
+        {% render_table plugins_table %}
+      </div>
+    </div>
+  </div>
+
+  {# Configuration #}
+  <div class="row mb-3">
+    <div class="col col-md-12">
+      <div class="card">
+        <h5 class="card-header">{% trans "Current Configuration" %}</h5>
+        {% include 'core/inc/config_data.html' with config=config.data %}
+      </div>
+
+    </div>
+  </div>
+{% endblock content %}