From e9640f4f155e21dc64ab2c9713b11ae9a3bfb31f Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 30 Sep 2024 23:39:38 +1000 Subject: [PATCH] Plugin helper function (#8220) * Add helper function for constructing URL to static file * Fix PluginListTable - Allow uninstallation of plugin - Allow deletion of plugin config * Move helper method to InvenTreePlugin class * Bump API version info --- .../InvenTree/InvenTree/api_version.py | 7 ++- src/backend/InvenTree/plugin/api.py | 7 +-- src/backend/InvenTree/plugin/plugin.py | 8 +++ .../src/tables/plugin/PluginListTable.tsx | 50 ++++++++++++++----- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 90bd51dfd7..747ef01e37 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,14 +1,17 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 261 +INVENTREE_API_VERSION = 262 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ -261 - 2024-09;26 : https://github.com/inventree/InvenTree/pull/8184 +262 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8220 + - Tweak permission requirements for uninstalling plugins via API + +261 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8184 - Fixes for BuildOrder API serializers v260 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190 diff --git a/src/backend/InvenTree/plugin/api.py b/src/backend/InvenTree/plugin/api.py index ff1e301357..ab7626e2af 100644 --- a/src/backend/InvenTree/plugin/api.py +++ b/src/backend/InvenTree/plugin/api.py @@ -21,11 +21,11 @@ from InvenTree.filters import SEARCH_ORDER_FILTER from InvenTree.mixins import ( CreateAPI, ListAPI, + RetrieveDestroyAPI, RetrieveUpdateAPI, - RetrieveUpdateDestroyAPI, UpdateAPI, ) -from InvenTree.permissions import IsSuperuser +from InvenTree.permissions import IsSuperuser, IsSuperuserOrReadOnly from plugin import registry from plugin.base.action.api import ActionPluginView from plugin.base.barcodes.api import barcode_api_urls @@ -143,7 +143,7 @@ class PluginList(ListAPI): search_fields = ['key', 'name'] -class PluginDetail(RetrieveUpdateDestroyAPI): +class PluginDetail(RetrieveDestroyAPI): """API detail endpoint for PluginConfig object. get: @@ -158,6 +158,7 @@ class PluginDetail(RetrieveUpdateDestroyAPI): queryset = PluginConfig.objects.all() serializer_class = PluginSerializers.PluginConfigSerializer + permission_classes = [IsSuperuserOrReadOnly] lookup_field = 'key' lookup_url_kwarg = 'plugin' diff --git a/src/backend/InvenTree/plugin/plugin.py b/src/backend/InvenTree/plugin/plugin.py index 5f16bac369..ce3555720c 100644 --- a/src/backend/InvenTree/plugin/plugin.py +++ b/src/backend/InvenTree/plugin/plugin.py @@ -438,3 +438,11 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase): self.package = package # endregion + + def plugin_static_file(self, *args): + """Construct a path to a static file within the plugin directory.""" + import os + + from django.conf import settings + + return '/' + os.path.join(settings.STATIC_URL, 'plugins', self.SLUG, *args) diff --git a/src/frontend/src/tables/plugin/PluginListTable.tsx b/src/frontend/src/tables/plugin/PluginListTable.tsx index 3fc1a3e746..2f18762736 100644 --- a/src/frontend/src/tables/plugin/PluginListTable.tsx +++ b/src/frontend/src/tables/plugin/PluginListTable.tsx @@ -17,7 +17,8 @@ import { IconHelpCircle, IconInfoCircle, IconPlaylistAdd, - IconRefresh + IconRefresh, + IconTrash } from '@tabler/icons-react'; import { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -352,9 +353,10 @@ export default function PluginListTable() { { hidden: record.is_builtin != false || - record.is_installed != true || + !record.is_installed || record.active != false, title: t`Activate`, + tooltip: t`Activate selected plugin`, color: 'green', icon: , onClick: () => { @@ -364,11 +366,9 @@ export default function PluginListTable() { } }, { - hidden: - record.active != true || - record.is_package != true || - !record.package_name, + hidden: !record.active || !record.is_package || !record.package_name, title: t`Update`, + tooltip: t`Update selected plugin`, color: 'blue', icon: , onClick: () => { @@ -377,10 +377,37 @@ export default function PluginListTable() { } }, { - hidden: record.is_installed != false, - title: t`Delete`, + // Uninstall an installed plugin + // Must be inactive, not a builtin, not a sample, and installed as a package + hidden: + !user.isSuperuser() || + record.active || + record.is_builtin || + record.is_sample || + !record.is_installed || + !record.is_package, + title: t`Uninstall`, + tooltip: t`Uninstall selected plugin`, color: 'red', icon: , + onClick: () => { + setSelectedPlugin(record.key); + uninstallPluginModal.open(); + } + }, + { + // Delete a plugin configuration + // Must be inactive, not a builtin, not a sample, and not installed (i.e. no matching plugin) + hidden: + record.active || + record.is_builtin || + record.is_sample || + record.is_installed || + !user.isSuperuser(), + title: t`Delete`, + tooltip: t`Delete selected plugin configuration`, + color: 'red', + icon: , onClick: () => { setSelectedPlugin(record.key); deletePluginModal.open(); @@ -436,7 +463,7 @@ export default function PluginListTable() { const uninstallPluginModal = useEditApiFormModal({ title: t`Uninstall Plugin`, url: ApiEndpoints.plugin_uninstall, - // pathParams: { key: selectedPlugin }, + pathParams: { key: selectedPlugin }, fetchInitialData: false, timeout: 30000, fields: { @@ -460,15 +487,14 @@ export default function PluginListTable() { const deletePluginModal = useDeleteApiFormModal({ url: ApiEndpoints.plugin_list, - pathParams: { key: selectedPlugin }, + pk: selectedPlugin, + fetchInitialData: false, title: t`Delete Plugin`, preFormWarning: t`Deleting this plugin configuration will remove all associated settings and data. Are you sure you want to delete this plugin?`, table: table }); const reloadPlugins = useCallback(() => { - console.log('reloadPlugins:'); - api .post(apiUrl(ApiEndpoints.plugin_reload), { full_reload: true,