mirror of
https://github.com/grafana/grafana.git
synced 2025-12-23 13:14:35 +08:00
Compare commits
2 Commits
docs/add-t
...
ckbedwell/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
022b02c594 | ||
|
|
f5073b3b64 |
@@ -1,6 +1,6 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import { AnyAction, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
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 * as React from 'react';
|
||||||
import { useLocation, useParams } from 'react-router-dom-v5-compat';
|
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 { getNotFoundNav, getWarningNav, getExceptionNav } from 'app/core/navigation/errorModels';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { getMessageFromError } from 'app/core/utils/errors';
|
import { getMessageFromError } from 'app/core/utils/errors';
|
||||||
|
import { PluginReloadedEvent } from 'app/types/events';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ExtensionRegistriesProvider,
|
ExtensionRegistriesProvider,
|
||||||
@@ -68,6 +69,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
|
|||||||
const addedFunctionsRegistry = useAddedFunctionsRegistry();
|
const addedFunctionsRegistry = useAddedFunctionsRegistry();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [state, dispatch] = useReducer(stateSlice.reducer, initialState);
|
const [state, dispatch] = useReducer(stateSlice.reducer, initialState);
|
||||||
|
const [reloadKey, setReloadKey] = useState(0);
|
||||||
const currentUrl = config.appSubUrl + location.pathname + location.search;
|
const currentUrl = config.appSubUrl + location.pathname + location.search;
|
||||||
const { plugin, loading, loadingError, pluginNav } = state;
|
const { plugin, loading, loadingError, pluginNav } = state;
|
||||||
const navModel = buildPluginSectionNav(currentUrl, pluginNavSection);
|
const navModel = buildPluginSectionNav(currentUrl, pluginNavSection);
|
||||||
@@ -77,6 +79,20 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAppPlugin(pluginId, dispatch);
|
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]);
|
}, [pluginId]);
|
||||||
|
|
||||||
const onNavChanged = useCallback(
|
const onNavChanged = useCallback(
|
||||||
|
|||||||
@@ -13,16 +13,23 @@ import {
|
|||||||
throwIfAngular,
|
throwIfAngular,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { appEvents } from 'app/core/app_events';
|
||||||
import { GenericDataSourcePlugin } from 'app/features/datasources/types';
|
import { GenericDataSourcePlugin } from 'app/features/datasources/types';
|
||||||
import { getPanelPluginLoadError } from 'app/features/panel/components/PanelPluginError';
|
import { getPanelPluginLoadError } from 'app/features/panel/components/PanelPluginError';
|
||||||
|
import { PluginReloadedEvent } from 'app/types/events';
|
||||||
|
|
||||||
|
import { isBuiltinPluginPath } from '../built_in_plugins';
|
||||||
import {
|
import {
|
||||||
addedComponentsRegistry,
|
addedComponentsRegistry,
|
||||||
addedFunctionsRegistry,
|
addedFunctionsRegistry,
|
||||||
addedLinksRegistry,
|
addedLinksRegistry,
|
||||||
exposedComponentsRegistry,
|
exposedComponentsRegistry,
|
||||||
} from '../extensions/registry/setup';
|
} from '../extensions/registry/setup';
|
||||||
import { pluginsLogger } from '../utils';
|
import { clearPluginInfoInCache } from '../loader/pluginInfoCache';
|
||||||
|
import { SystemJS } from '../loader/systemjs';
|
||||||
|
import { resolveModulePath } from '../loader/utils';
|
||||||
|
import { clearPluginSettingsCache, getPluginSettings } from '../pluginSettings';
|
||||||
|
import { pluginsLogger, loadPlugin } from '../utils';
|
||||||
|
|
||||||
import { importPluginModule } from './importPluginModule';
|
import { importPluginModule } from './importPluginModule';
|
||||||
import { PluginImporter, PostImportStrategy, PreImportStrategy } from './types';
|
import { PluginImporter, PostImportStrategy, PreImportStrategy } from './types';
|
||||||
@@ -182,3 +189,99 @@ export const clearCaches = () => {
|
|||||||
promisesCache.clear();
|
promisesCache.clear();
|
||||||
pluginsCache.clear();
|
pluginsCache.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads a plugin by clearing its caches and re-importing it.
|
||||||
|
* @param pluginId - The ID of the plugin to reload
|
||||||
|
* @returns Promise that resolves when the plugin has been reloaded
|
||||||
|
*/
|
||||||
|
export async function reloadPlugin(pluginId: string): Promise<void> {
|
||||||
|
pluginsLogger.logDebug(`Reloading plugin`, { pluginId });
|
||||||
|
|
||||||
|
// Get plugin metadata to resolve module path
|
||||||
|
const meta = await getPluginSettings(pluginId);
|
||||||
|
|
||||||
|
// Clear plugin from caches
|
||||||
|
pluginsCache.delete(pluginId);
|
||||||
|
promisesCache.delete(pluginId);
|
||||||
|
|
||||||
|
// Clear plugin settings cache
|
||||||
|
clearPluginSettingsCache(pluginId);
|
||||||
|
|
||||||
|
// Clear plugin info cache (used for URL resolution)
|
||||||
|
clearPluginInfoInCache(pluginId);
|
||||||
|
|
||||||
|
// Clear SystemJS module cache to force a fresh fetch
|
||||||
|
if (!isBuiltinPluginPath(meta.module)) {
|
||||||
|
const modulePath = resolveModulePath(meta.module);
|
||||||
|
try {
|
||||||
|
// Resolve the module path to get the actual URL SystemJS uses
|
||||||
|
const resolvedPath = SystemJS.resolve(modulePath);
|
||||||
|
|
||||||
|
// Delete from SystemJS cache - try both the resolved path and original path
|
||||||
|
if (SystemJS.has(resolvedPath)) {
|
||||||
|
SystemJS.delete(resolvedPath);
|
||||||
|
pluginsLogger.logDebug(`Deleted SystemJS cache for resolved path`, { resolvedPath, pluginId });
|
||||||
|
}
|
||||||
|
if (SystemJS.has(modulePath)) {
|
||||||
|
SystemJS.delete(modulePath);
|
||||||
|
pluginsLogger.logDebug(`Deleted SystemJS cache for module path`, { modulePath, pluginId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try deleting any entries that match the plugin ID
|
||||||
|
for (const [key] of SystemJS.entries()) {
|
||||||
|
const keyStr = String(key);
|
||||||
|
if (keyStr.includes(pluginId) || keyStr.includes(meta.module)) {
|
||||||
|
SystemJS.delete(key);
|
||||||
|
pluginsLogger.logDebug(`Deleted SystemJS cache entry`, { key: keyStr, pluginId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If resolution fails, try to delete by module path anyway
|
||||||
|
pluginsLogger.logDebug(`Could not resolve module path, attempting direct delete`, {
|
||||||
|
modulePath,
|
||||||
|
pluginId,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
if (SystemJS.has(modulePath)) {
|
||||||
|
SystemJS.delete(modulePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Re-import the plugin (this will re-register extensions for app plugins)
|
||||||
|
// Note: Extension registries use scan/accumulate, so entries may accumulate on reload.
|
||||||
|
// This is acceptable for a reload feature.
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearWebpackCache(pluginId: string) {
|
||||||
|
const pluginNormalisedCacheName = pluginId.replaceAll('-', '_');
|
||||||
|
const pluginCacheName = `webpackChunk${pluginNormalisedCacheName}`;
|
||||||
|
|
||||||
|
if (window[pluginCacheName]) {
|
||||||
|
// Remove the plugin's webpack cache by name
|
||||||
|
delete (window)[pluginCacheName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.reloadPlugin = async (options: { id: string }) => {
|
||||||
|
if (!options?.id) {
|
||||||
|
throw new Error('Plugin ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearWebpackCache(options.id);
|
||||||
|
return reloadPlugin(options.id);
|
||||||
|
};
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ export interface DashScrollPayload {
|
|||||||
pos?: number;
|
pos?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelChangeViewPayload {}
|
export interface PanelChangeViewPayload { }
|
||||||
|
|
||||||
|
export interface PluginReloadedEventPayload {
|
||||||
|
pluginId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events
|
* Events
|
||||||
@@ -221,3 +225,7 @@ export class PanelEditExitedEvent extends BusEventWithPayload<number> {
|
|||||||
export class RecordHistoryEntryEvent extends BusEventWithPayload<HistoryEntryView> {
|
export class RecordHistoryEntryEvent extends BusEventWithPayload<HistoryEntryView> {
|
||||||
static type = 'record-history-entry';
|
static type = 'record-history-entry';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PluginReloadedEvent extends BusEventWithPayload<PluginReloadedEventPayload> {
|
||||||
|
static type = 'plugin-reloaded';
|
||||||
|
}
|
||||||
|
|||||||
12
public/app/types/window.d.ts
vendored
12
public/app/types/window.d.ts
vendored
@@ -12,6 +12,18 @@ export declare global {
|
|||||||
**/
|
**/
|
||||||
__grafana_boot_data_promise: Promise<void>;
|
__grafana_boot_data_promise: Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads a plugin without refreshing the browser.
|
||||||
|
* @param options - Options object containing the plugin ID
|
||||||
|
* @param options.id - The ID of the plugin to reload
|
||||||
|
* @returns Promise that resolves when the plugin has been reloaded
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* await window.reloadPlugin({ id: 'grafana-synthetic-monitoring-app' });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
reloadPlugin: (options: { id: string }) => Promise<void>;
|
||||||
|
|
||||||
public_cdn_path: string;
|
public_cdn_path: string;
|
||||||
nonce: string | undefined;
|
nonce: string | undefined;
|
||||||
System: typeof System;
|
System: typeof System;
|
||||||
|
|||||||
Reference in New Issue
Block a user