14728 Move installed plugins list from admin UI to NetBox UI (#14768)

* 14728 move plugins view from admin

* 14728 move plugins view from admin

* 14728 remove plugins view from admin

* Update template for #12128

* 14728 review fixes

* 14728 review fixes

* 14728 review fixes

* 14728 review fixes

* 14728 configure table

* Clean up table columns

* Fix app config lookup for plugins referenced by dotted path

* Move template; fix table display

* Fix user table configuration

* Remove nonfunctional quick search

* Limit PluginListView to staff users

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
Arthur Hanson 2024-01-19 08:27:15 -08:00 committed by GitHub
parent 073c2dc8ca
commit ef5e10d360
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 118 additions and 74 deletions

View File

@ -1,3 +1,4 @@
from .config import * from .config import *
from .data import * from .data import *
from .jobs import * from .jobs import *
from .plugins import *

View File

@ -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',
)

View File

@ -35,4 +35,6 @@ urlpatterns = (
# Configuration # Configuration
path('config/', views.ConfigView.as_view(), name='config'), path('config/', views.ConfigView.as_view(), name='config'),
# Plugins
path('plugins/', views.PluginListView.as_view(), name='plugin_list'),
) )

View File

@ -1,4 +1,7 @@
from django.apps import apps
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.cache import cache from django.core.cache import cache
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render 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}") messages.success(request, f"Restored configuration revision #{pk}")
return redirect(candidate_config.get_absolute_url()) 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,
})

View File

@ -1,3 +1,5 @@
from django.apps import apps
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType

View File

@ -450,6 +450,16 @@ ADMIN_MENU = Menu(
), ),
), ),
), ),
MenuGroup(
label=_('Plugins'),
items=(
MenuItem(
link='core:plugin_list',
link_text=_('Plugins'),
staff_only=True
),
),
),
), ),
) )

View File

@ -15,9 +15,6 @@ plugin_api_patterns = [
path('', views.PluginsAPIRootView.as_view(), name='api-root'), path('', views.PluginsAPIRootView.as_view(), name='api-root'),
path('installed-plugins/', views.InstalledPluginsAPIView.as_view(), name='plugins-list') 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 # Register base/API URL patterns for each plugin
for plugin_path in settings.PLUGINS: for plugin_path in settings.PLUGINS:

View File

@ -12,17 +12,6 @@ from rest_framework.reverse import reverse
from rest_framework.views import APIView 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) @extend_schema(exclude=True)
class InstalledPluginsAPIView(APIView): class InstalledPluginsAPIView(APIView):
""" """

View File

@ -9,7 +9,7 @@ from account.views import LoginView, LogoutView
from netbox.api.views import APIRootView, StatusView from netbox.api.views import APIRootView, StatusView
from netbox.graphql.schema import schema from netbox.graphql.schema import schema
from netbox.graphql.views import GraphQLView 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 netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
from .admin import admin_site from .admin import admin_site
@ -73,7 +73,6 @@ _patterns = [
# Admin # Admin
path('admin/background-tasks/', include('django_rq.urls')), path('admin/background-tasks/', include('django_rq.urls')),
path('admin/plugins/', include(plugin_admin_patterns)),
path('admin/', admin_site.urls), path('admin/', admin_site.urls),
] ]

View File

@ -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 %}
<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 %}

View File

@ -1,58 +0,0 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{% trans "Installed Plugins" %} {{ block.super }}{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans "Home" %}</a> &rsaquo;
<a href="{% url 'plugins_list' %}">{% trans "Installed Plugins" %}</a>
</div>
{% endblock %}
{% block content_title %}<h1>{% trans "Installed Plugins" %}{{ queue.name }}</h1>{% endblock %}
{% block content %}
<div id="content-main">
<div class="module" id="changelist">
<div class="results">
<table id="result_list">
<thead>
<tr>
<th><div class="text"><span>{% trans "Name" %}</span></div></th>
<th><div class="text"><span>{% trans "Package Name" %}</span></div></th>
<th><div class="text"><span>{% trans "Author" %}</span></div></th>
<th><div class="text"><span>{% trans "Author Email" %}</span></div></th>
<th><div class="text"><span>{% trans "Description" %}</span></div></th>
<th><div class="text"><span>{% trans "Version" %}</span></div></th>
</tr>
</thead>
<tbody>
{% for plugin in plugins %}
<tr class="{% cycle 'row1' 'row2' %}">
<td>
{{ plugin.verbose_name }}
</td>
<td>
{{ plugin.name }}
</td>
<td>
{{ plugin.author }}
</td>
<td>
{{ plugin.author_email }}
</td>
<td>
{{ plugin.description }}
</td>
<td>
{{ plugin.version }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}