diff --git a/public/app/features/plugins/components/AppRootPage.tsx b/public/app/features/plugins/components/AppRootPage.tsx index 1b46f33b091..941b1756586 100644 --- a/public/app/features/plugins/components/AppRootPage.tsx +++ b/public/app/features/plugins/components/AppRootPage.tsx @@ -1,6 +1,6 @@ // Libraries import { AnyAction, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { useCallback, useEffect, useMemo, useReducer } from 'react'; +import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import * as React from 'react'; import { useLocation, useParams } from 'react-router-dom-v5-compat'; @@ -25,6 +25,7 @@ import { useGrafana } from 'app/core/context/GrafanaContext'; import { getNotFoundNav, getWarningNav, getExceptionNav } from 'app/core/navigation/errorModels'; import { contextSrv } from 'app/core/services/context_srv'; import { getMessageFromError } from 'app/core/utils/errors'; +import { PluginReloadedEvent } from 'app/types/events'; import { ExtensionRegistriesProvider, @@ -68,6 +69,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) { const addedFunctionsRegistry = useAddedFunctionsRegistry(); const location = useLocation(); const [state, dispatch] = useReducer(stateSlice.reducer, initialState); + const [reloadKey, setReloadKey] = useState(0); const currentUrl = config.appSubUrl + location.pathname + location.search; const { plugin, loading, loadingError, pluginNav } = state; const navModel = buildPluginSectionNav(currentUrl, pluginNavSection); @@ -77,6 +79,20 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) { useEffect(() => { loadAppPlugin(pluginId, dispatch); + }, [pluginId, reloadKey]); + + // Subscribe to plugin reload events + useEffect(() => { + const subscription = appEvents.subscribe(PluginReloadedEvent, (event) => { + if (event.payload.pluginId === pluginId) { + // Force reload by incrementing reloadKey + setReloadKey((prev) => prev + 1); + } + }); + + return () => { + subscription.unsubscribe(); + }; }, [pluginId]); const onNavChanged = useCallback( diff --git a/public/app/features/plugins/importer/pluginImporter.ts b/public/app/features/plugins/importer/pluginImporter.ts index 07edda0d981..9568ab7be38 100644 --- a/public/app/features/plugins/importer/pluginImporter.ts +++ b/public/app/features/plugins/importer/pluginImporter.ts @@ -13,8 +13,10 @@ import { throwIfAngular, } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { appEvents } from 'app/core/app_events'; import { GenericDataSourcePlugin } from 'app/features/datasources/types'; import { getPanelPluginLoadError } from 'app/features/panel/components/PanelPluginError'; +import { PluginReloadedEvent } from 'app/types/events'; import { isBuiltinPluginPath } from '../built_in_plugins'; import { @@ -254,6 +256,9 @@ export async function reloadPlugin(pluginId: string): Promise { await loadPlugin(pluginId); pluginsLogger.logDebug(`Plugin reloaded successfully`, { pluginId }); + + // Emit event to notify components that the plugin has been reloaded + appEvents.publish(new PluginReloadedEvent({ pluginId })); } catch (error) { pluginsLogger.logError(error instanceof Error ? error : new Error('Failed to reload plugin'), { pluginId, diff --git a/public/app/types/events.ts b/public/app/types/events.ts index 5c7e4ba151b..26ac351a649 100644 --- a/public/app/types/events.ts +++ b/public/app/types/events.ts @@ -67,7 +67,11 @@ export interface DashScrollPayload { pos?: number; } -export interface PanelChangeViewPayload {} +export interface PanelChangeViewPayload { } + +export interface PluginReloadedEventPayload { + pluginId: string; +} /** * Events @@ -221,3 +225,7 @@ export class PanelEditExitedEvent extends BusEventWithPayload { export class RecordHistoryEntryEvent extends BusEventWithPayload { static type = 'record-history-entry'; } + +export class PluginReloadedEvent extends BusEventWithPayload { + static type = 'plugin-reloaded'; +}