diff --git a/netbox/core/tables/__init__.py b/netbox/core/tables/__init__.py
index 69f9d8a48..29dc7d85e 100644
--- a/netbox/core/tables/__init__.py
+++ b/netbox/core/tables/__init__.py
@@ -1,3 +1,4 @@
from .config import *
from .data import *
from .jobs import *
+from .plugins import *
diff --git a/netbox/core/tables/plugins.py b/netbox/core/tables/plugins.py
new file mode 100644
index 000000000..2e3c0a991
--- /dev/null
+++ b/netbox/core/tables/plugins.py
@@ -0,0 +1,39 @@
+import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
+from netbox.tables import BaseTable
+
+__all__ = (
+ 'PluginTable',
+)
+
+
+class PluginTable(BaseTable):
+ name = tables.Column(
+ accessor=tables.A('verbose_name'),
+ verbose_name=_('Name')
+ )
+ version = tables.Column(
+ verbose_name=_('Version')
+ )
+ package = tables.Column(
+ accessor=tables.A('name'),
+ verbose_name=_('Package')
+ )
+ author = tables.Column(
+ verbose_name=_('Author')
+ )
+ author_email = tables.Column(
+ verbose_name=_('Author Email')
+ )
+ description = tables.Column(
+ verbose_name=_('Description')
+ )
+
+ class Meta(BaseTable.Meta):
+ empty_text = _('No plugins found')
+ fields = (
+ 'name', 'version', 'package', 'author', 'author_email', 'description',
+ )
+ default_columns = (
+ 'name', 'version', 'package', 'author', 'author_email', 'description',
+ )
diff --git a/netbox/core/urls.py b/netbox/core/urls.py
index 77c0d3194..3bb5cd24c 100644
--- a/netbox/core/urls.py
+++ b/netbox/core/urls.py
@@ -35,4 +35,6 @@ urlpatterns = (
# Configuration
path('config/', views.ConfigView.as_view(), name='config'),
+ # Plugins
+ path('plugins/', views.PluginListView.as_view(), name='plugin_list'),
)
diff --git a/netbox/core/views.py b/netbox/core/views.py
index 537c33d9d..f81957927 100644
--- a/netbox/core/views.py
+++ b/netbox/core/views.py
@@ -1,4 +1,7 @@
+from django.apps import apps
+from django.conf import settings
from django.contrib import messages
+from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.cache import cache
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
@@ -232,3 +235,27 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
messages.success(request, f"Restored configuration revision #{pk}")
return redirect(candidate_config.get_absolute_url())
+
+
+#
+# Plugins
+#
+
+class PluginListView(UserPassesTestMixin, View):
+
+ def test_func(self):
+ return self.request.user.is_staff
+
+ def get(self, request):
+ plugins = [
+ # Look up app config by package name
+ 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,
+ })
diff --git a/netbox/extras/views.py b/netbox/extras/views.py
index a3dd7f193..5a1fbf19e 100644
--- a/netbox/extras/views.py
+++ b/netbox/extras/views.py
@@ -1,3 +1,5 @@
+from django.apps import apps
+from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py
index 32b19ef62..e63947424 100644
--- a/netbox/netbox/navigation/menu.py
+++ b/netbox/netbox/navigation/menu.py
@@ -450,6 +450,16 @@ ADMIN_MENU = Menu(
),
),
),
+ MenuGroup(
+ label=_('Plugins'),
+ items=(
+ MenuItem(
+ link='core:plugin_list',
+ link_text=_('Plugins'),
+ staff_only=True
+ ),
+ ),
+ ),
),
)
diff --git a/netbox/netbox/plugins/urls.py b/netbox/netbox/plugins/urls.py
index 2f237f56a..075bda811 100644
--- a/netbox/netbox/plugins/urls.py
+++ b/netbox/netbox/plugins/urls.py
@@ -15,9 +15,6 @@ plugin_api_patterns = [
path('', views.PluginsAPIRootView.as_view(), name='api-root'),
path('installed-plugins/', views.InstalledPluginsAPIView.as_view(), name='plugins-list')
]
-plugin_admin_patterns = [
- path('installed-plugins/', staff_member_required(views.InstalledPluginsAdminView.as_view()), name='plugins_list')
-]
# Register base/API URL patterns for each plugin
for plugin_path in settings.PLUGINS:
diff --git a/netbox/netbox/plugins/views.py b/netbox/netbox/plugins/views.py
index 5971f78ef..777a4c69e 100644
--- a/netbox/netbox/plugins/views.py
+++ b/netbox/netbox/plugins/views.py
@@ -12,17 +12,6 @@ from rest_framework.reverse import reverse
from rest_framework.views import APIView
-class InstalledPluginsAdminView(View):
- """
- Admin view for listing all installed plugins
- """
- def get(self, request):
- plugins = [apps.get_app_config(plugin) for plugin in settings.PLUGINS]
- return render(request, 'extras/admin/plugins_list.html', {
- 'plugins': plugins,
- })
-
-
@extend_schema(exclude=True)
class InstalledPluginsAPIView(APIView):
"""
diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py
index 984358911..7f37f01f1 100644
--- a/netbox/netbox/urls.py
+++ b/netbox/netbox/urls.py
@@ -9,7 +9,7 @@ from account.views import LoginView, LogoutView
from netbox.api.views import APIRootView, StatusView
from netbox.graphql.schema import schema
from netbox.graphql.views import GraphQLView
-from netbox.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
+from netbox.plugins.urls import plugin_patterns, plugin_api_patterns
from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
from .admin import admin_site
@@ -73,7 +73,6 @@ _patterns = [
# Admin
path('admin/background-tasks/', include('django_rq.urls')),
- path('admin/plugins/', include(plugin_admin_patterns)),
path('admin/', admin_site.urls),
]
diff --git a/netbox/templates/core/plugin_list.html b/netbox/templates/core/plugin_list.html
new file mode 100644
index 000000000..42f0b7156
--- /dev/null
+++ b/netbox/templates/core/plugin_list.html
@@ -0,0 +1,36 @@
+{% extends 'generic/_base.html' %}
+{% load buttons %}
+{% load helpers %}
+{% load i18n %}
+{% load render_table from django_tables2 %}
+
+{% block title %}{% trans "Plugins" %}{% endblock %}
+
+{% block tabs %}
+
+{% endblock tabs %}
+
+{% block content %}
+
+
+ {# Table configuration button #}
+
+
+
+
+
+
+
+ {% render_table table %}
+
+{% endblock content %}
+
+{% block modals %}
+ {% table_config_form table table_name="ObjectTable" %}
+{% endblock modals %}
diff --git a/netbox/templates/extras/admin/plugins_list.html b/netbox/templates/extras/admin/plugins_list.html
deleted file mode 100644
index 6795c66e6..000000000
--- a/netbox/templates/extras/admin/plugins_list.html
+++ /dev/null
@@ -1,58 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% load i18n %}
-
-{% block title %}{% trans "Installed Plugins" %} {{ block.super }}{% endblock %}
-
-{% block breadcrumbs %}
-
-{% endblock %}
-
-{% block content_title %}{% trans "Installed Plugins" %}{{ queue.name }}
{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
- {% trans "Name" %} |
- {% trans "Package Name" %} |
- {% trans "Author" %} |
- {% trans "Author Email" %} |
- {% trans "Description" %} |
- {% trans "Version" %} |
-
-
-
- {% for plugin in plugins %}
-
-
- {{ plugin.verbose_name }}
- |
-
- {{ plugin.name }}
- |
-
- {{ plugin.author }}
- |
-
- {{ plugin.author_email }}
- |
-
- {{ plugin.description }}
- |
-
- {{ plugin.version }}
- |
-
- {% endfor %}
-
-
-
-
-
-{% endblock %}