From 8d46521cab99e56ef3b2a5171f474e1792f431cc Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 11 Dec 2023 07:46:41 +1100 Subject: [PATCH] Plugin loading improvements (#6056) * Add API endpoint to reload plugin registry * Tweak debug * Add elements for CUI * Update API version * Reload plugins from PUI --- InvenTree/InvenTree/api_version.py | 5 ++- InvenTree/plugin/api.py | 13 +++++++ InvenTree/plugin/registry.py | 2 +- InvenTree/plugin/serializers.py | 31 ++++++++++++++++ .../templates/InvenTree/settings/plugin.html | 8 ++++- .../InvenTree/settings/settings_staff_js.html | 5 +++ InvenTree/templates/js/translated/plugin.js | 34 +++++++++++++++++- .../tables/plugin/PluginListTable.tsx | 35 +++++++++++++++++-- src/frontend/src/enums/ApiEndpoints.tsx | 1 + src/frontend/src/states/ApiState.tsx | 2 ++ 10 files changed, 129 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index ca9c6efb85..7688b6f027 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -2,11 +2,14 @@ # InvenTree API version -INVENTREE_API_VERSION = 158 +INVENTREE_API_VERSION = 159 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v159 -> 2023-12-08 : https://github.com/inventree/InvenTree/pull/6056 + - Adds API endpoint for reloading plugin registry + v158 -> 2023-11-21 : https://github.com/inventree/InvenTree/pull/5953 - Adds API endpoint for listing all settings of a particular plugin - Adds API endpoint for registry status (errors) diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py index 6eeb69b5c5..4e04b0b5b3 100644 --- a/InvenTree/plugin/api.py +++ b/InvenTree/plugin/api.py @@ -189,6 +189,18 @@ class PluginActivate(UpdateAPI): serializer.save() +class PluginReload(CreateAPI): + """Endpoint for reloading all plugins.""" + + queryset = PluginConfig.objects.none() + serializer_class = PluginSerializers.PluginReloadSerializer + permission_classes = [IsSuperuser,] + + def perform_create(self, serializer): + """Saving the serializer instance performs plugin installation""" + return serializer.save() + + class PluginSettingList(ListAPI): """List endpoint for all plugin related settings. @@ -374,6 +386,7 @@ plugin_api_urls = [ re_path('^metadata/', MetadataView.as_view(), {'model': PluginConfig}, name='api-plugin-metadata'), # Plugin management + re_path(r'^reload/', PluginReload.as_view(), name='api-plugin-reload'), re_path(r'^install/', PluginInstall.as_view(), name='api-plugin-install'), re_path(r'^activate/', PluginActivate.as_view(), name='api-plugin-activate'), diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 9af87a56d8..b6b749f168 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -260,7 +260,7 @@ class PluginsRegistry: if self.loading_lock.acquire(blocking=False): - logger.info('Plugin Registry: Reloading plugins') + logger.info('Plugin Registry: Reloading plugins - Force: %s, Full: %s, Collect: %s', force_reload, full_reload, collect) with maintenance_mode_on(): if collect: diff --git a/InvenTree/plugin/serializers.py b/InvenTree/plugin/serializers.py index 7d24b11d42..e86dc2b3fe 100644 --- a/InvenTree/plugin/serializers.py +++ b/InvenTree/plugin/serializers.py @@ -131,6 +131,37 @@ class PluginConfigEmptySerializer(serializers.Serializer): ... +class PluginReloadSerializer(serializers.Serializer): + """Serializer for remotely forcing plugin registry reload""" + + full_reload = serializers.BooleanField( + required=False, default=False, + label=_("Full reload"), + help_text=_("Perform a full reload of the plugin registry") + ) + + force_reload = serializers.BooleanField( + required=False, default=False, + label=_("Force reload"), + help_text=_("Force a reload of the plugin registry, even if it is already loaded") + ) + + collect_plugins = serializers.BooleanField( + required=False, default=False, + label=_("Collect plugins"), + help_text=_("Collect plugins and add them to the registry") + ) + + def save(self): + """Reload the plugin registry.""" + from plugin.registry import registry + registry.reload_plugins( + full_reload=self.validated_data.get('full_reload', False), + force_reload=self.validated_data.get('force_reload', False), + collect=self.validated_data.get('collect_plugins', False), + ) + + class PluginActivateSerializer(serializers.Serializer): """Serializer for activating or deactivating a plugin""" diff --git a/InvenTree/templates/InvenTree/settings/plugin.html b/InvenTree/templates/InvenTree/settings/plugin.html index 5ba99b735b..fe6a296536 100644 --- a/InvenTree/templates/InvenTree/settings/plugin.html +++ b/InvenTree/templates/InvenTree/settings/plugin.html @@ -38,7 +38,13 @@ {% admin_url user "plugin.pluginconfig" None as url %} {% include "admin_button.html" with url=url %} {% if plug %} - + + + {% endif %} diff --git a/InvenTree/templates/InvenTree/settings/settings_staff_js.html b/InvenTree/templates/InvenTree/settings/settings_staff_js.html index 3ee6ab180d..512d21898d 100644 --- a/InvenTree/templates/InvenTree/settings/settings_staff_js.html +++ b/InvenTree/templates/InvenTree/settings/settings_staff_js.html @@ -591,5 +591,10 @@ onPanelLoad('plugin', function() { installPlugin(); }); + // Callback to reload plugins + $('#reload-plugins').click(function() { + reloadPlugins(); + }); + {% endif %} }); diff --git a/InvenTree/templates/js/translated/plugin.js b/InvenTree/templates/js/translated/plugin.js index 29d276ff2d..1c5b3db908 100644 --- a/InvenTree/templates/js/translated/plugin.js +++ b/InvenTree/templates/js/translated/plugin.js @@ -20,7 +20,8 @@ activatePlugin, installPlugin, loadPluginTable, - locateItemOrLocation + locateItemOrLocation, + reloadPlugins, */ @@ -213,6 +214,37 @@ function activatePlugin(plugin_id, active=true) { } +/* + * Reload the plugin registry + */ +function reloadPlugins() { + let url = '{% url "api-plugin-reload" %}'; + + constructForm(url, { + title: '{% trans "Reload Plugins" %}', + method: 'POST', + confirm: true, + fields: { + force_reload: { + // hidden: true, + value: true, + }, + full_reload: { + // hidden: true, + value: true, + }, + collect_plugins: { + // hidden: true, + value: true, + }, + }, + onSuccess: function() { + location.reload(); + } + }); +} + + function locateItemOrLocation(options={}) { if (!options.item && !options.location) { diff --git a/src/frontend/src/components/tables/plugin/PluginListTable.tsx b/src/frontend/src/components/tables/plugin/PluginListTable.tsx index c9662413b0..4690a8c555 100644 --- a/src/frontend/src/components/tables/plugin/PluginListTable.tsx +++ b/src/frontend/src/components/tables/plugin/PluginListTable.tsx @@ -11,7 +11,7 @@ import { Tooltip } from '@mantine/core'; import { modals } from '@mantine/modals'; -import { notifications } from '@mantine/notifications'; +import { notifications, showNotification } from '@mantine/notifications'; import { IconCircleCheck, IconCircleX, @@ -30,6 +30,7 @@ import { useCreateApiFormModal } from '../../../hooks/UseForm'; import { useInstance } from '../../../hooks/UseInstance'; import { useTable } from '../../../hooks/UseTable'; import { apiUrl, useServerApiState } from '../../../states/ApiState'; +import { useUserState } from '../../../states/UserState'; import { ActionButton } from '../../buttons/ActionButton'; import { ActionDropdown, EditItemAction } from '../../items/ActionDropdown'; import { InfoItem } from '../../items/InfoItem'; @@ -423,11 +424,39 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) { } }); + const user = useUserState(); + + const reloadPlugins = useCallback(() => { + api + .post(apiUrl(ApiPaths.plugin_reload), { + full_reload: true, + force_reload: true, + collect_plugins: true + }) + .then(() => { + showNotification({ + title: t`Plugins reloaded`, + message: t`Plugins were reloaded successfully`, + color: 'green' + }); + table.refreshTable(); + }); + }, []); + // Custom table actions const tableActions = useMemo(() => { let actions = []; - if (pluginsEnabled) { + if (user.user?.is_superuser && pluginsEnabled) { + actions.push( + } + tooltip={t`Reload Plugins`} + onClick={reloadPlugins} + /> + ); + actions.push( diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 6495758455..c2d2308de5 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -86,6 +86,7 @@ export enum ApiPaths { plugin_list = 'api-plugin-list', plugin_setting_list = 'api-plugin-settings', plugin_install = 'api-plugin-install', + plugin_reload = 'api-plugin-reload', plugin_registry_status = 'api-plugin-registry-status', project_code_list = 'api-project-code-list', diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index 1a834ba2d3..c99fb598ef 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -187,6 +187,8 @@ export function apiEndpoint(path: ApiPaths): string { return 'plugins/status/'; case ApiPaths.plugin_install: return 'plugins/install/'; + case ApiPaths.plugin_reload: + return 'plugins/reload/'; case ApiPaths.project_code_list: return 'project-code/'; case ApiPaths.custom_unit_list: