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: