Chore: Enable new pluginImporter (#112625)

* Chore: Enable new pluginImporter

* chore: remove pluginLoader
This commit is contained in:
Hugo Häggmark
2025-10-23 06:18:23 +02:00
committed by GitHub
parent 04ea9eebd4
commit c0391ad2ea
25 changed files with 99 additions and 342 deletions
@@ -1115,11 +1115,6 @@ export interface FeatureToggles {
*/
foldersAppPlatformAPI?: boolean;
/**
* Set this to true to use the new PluginImporter functionality
* @default false
*/
enablePluginImporter?: boolean;
/**
* Applies OTel formatting templates to displayed logs
*/
otelLogsFormatting?: boolean;
-10
View File
@@ -1933,16 +1933,6 @@ var (
FrontendOnly: true,
Expression: "false",
},
{
Name: "enablePluginImporter",
Description: "Set this to true to use the new PluginImporter functionality",
Stage: FeatureStageExperimental,
Owner: grafanaPluginsPlatformSquad,
HideFromAdminPage: true,
HideFromDocs: true,
FrontendOnly: true,
Expression: "false",
},
{
Name: "otelLogsFormatting",
Description: "Applies OTel formatting templates to displayed logs",
-1
View File
@@ -249,7 +249,6 @@ newInfluxDSConfigPageDesign,privatePreview,@grafana/partner-datasources,false,fa
enableAppChromeExtensions,experimental,@grafana/plugins-platform-backend,false,false,true
enableDashboardEmptyExtensions,experimental,@grafana/dashboards-squad,false,false,true
foldersAppPlatformAPI,experimental,@grafana/grafana-search-navigate-organise,false,false,true
enablePluginImporter,experimental,@grafana/plugins-platform-backend,false,false,true
otelLogsFormatting,experimental,@grafana/observability-logs,false,false,true
alertingNotificationHistory,experimental,@grafana/alerting-squad,false,false,false
unifiedStorageSearchDualReaderEnabled,experimental,@grafana/search-and-storage,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
249 enableAppChromeExtensions experimental @grafana/plugins-platform-backend false false true
250 enableDashboardEmptyExtensions experimental @grafana/dashboards-squad false false true
251 foldersAppPlatformAPI experimental @grafana/grafana-search-navigate-organise false false true
enablePluginImporter experimental @grafana/plugins-platform-backend false false true
252 otelLogsFormatting experimental @grafana/observability-logs false false true
253 alertingNotificationHistory experimental @grafana/alerting-squad false false false
254 unifiedStorageSearchDualReaderEnabled experimental @grafana/search-and-storage false false false
-4
View File
@@ -1006,10 +1006,6 @@ const (
// Enables use of app platform API for folders
FlagFoldersAppPlatformAPI = "foldersAppPlatformAPI"
// FlagEnablePluginImporter
// Set this to true to use the new PluginImporter functionality
FlagEnablePluginImporter = "enablePluginImporter"
// FlagOtelLogsFormatting
// Applies OTel formatting templates to displayed logs
FlagOtelLogsFormatting = "otelLogsFormatting"
+2 -1
View File
@@ -1445,7 +1445,8 @@
"metadata": {
"name": "enablePluginImporter",
"resourceVersion": "1753448760331",
"creationTimestamp": "2025-07-16T04:42:28Z"
"creationTimestamp": "2025-07-16T04:42:28Z",
"deletionTimestamp": "2025-10-20T05:42:33Z"
},
"spec": {
"description": "Set this to true to use the new PluginImporter functionality",
+3
View File
@@ -102,6 +102,7 @@ import { usePluginFunctions } from './features/plugins/extensions/usePluginFunct
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
import { initSystemJSHooks } from './features/plugins/loader/systemjsHooks';
import { preloadPlugins } from './features/plugins/pluginPreloader';
import { QueryRunner } from './features/query/state/QueryRunner';
import { runRequest } from './features/query/state/runRequest';
@@ -136,6 +137,8 @@ export class GrafanaApp {
// Let iframe container know grafana has started loading
window.parent.postMessage('GrafanaAppInit', '*');
initSystemJSHooks();
// Currently the OpenFeature API requires a signed in user. This means feature flags cannot be used
// on the login page.
if (contextSrv.user.isSignedIn) {
@@ -45,7 +45,7 @@ jest.mock('../services/PreferencesService', () => ({
}));
// FIXME: Tests break unless plugin loader is mocked. This is likely due to a circular dependency
jest.mock('app/features/plugins/pluginLoader', () => ({}));
jest.mock('app/features/plugins/importer/pluginImporter', () => ({}));
describe('RichHistoryRemoteStorage', () => {
let storage: RichHistoryRemoteStorage;
@@ -23,7 +23,7 @@ import { getBackendSrv } from 'app/core/services/backend_srv';
import { DatasourceAPIVersions } from 'app/features/apiserver/client';
import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { importDataSourcePlugin } from 'app/features/plugins/pluginLoader';
import { pluginImporter } from 'app/features/plugins/importer/pluginImporter';
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
import { AccessControlAction } from 'app/types/accessControl';
import { DataSourcePluginCategory } from 'app/types/datasources';
@@ -60,7 +60,7 @@ export interface InitDataSourceSettingDependencies {
loadDataSourceMeta: typeof loadDataSourceMeta;
getDataSource: typeof getDataSource;
getDataSourceMeta: typeof getDataSourceMeta;
importDataSourcePlugin: typeof importDataSourcePlugin;
importDataSourcePlugin: typeof pluginImporter.importDataSource;
}
export interface TestDataSourceDependencies {
@@ -103,7 +103,7 @@ export const initDataSourceSettings = (
loadDataSourceMeta,
getDataSource,
getDataSourceMeta,
importDataSourcePlugin,
importDataSourcePlugin: pluginImporter.importDataSource,
}
): ThunkResult<void> => {
return async (dispatch, getState) => {
@@ -225,7 +225,7 @@ export function loadDataSource(uid: string): ThunkResult<Promise<DataSourceSetti
export function loadDataSourceMeta(dataSource: DataSourceSettings): ThunkResult<void> {
return async (dispatch) => {
const pluginInfo: DataSourcePluginMeta = await getPluginSettings(dataSource.type);
const plugin = await importDataSourcePlugin(pluginInfo);
const plugin = await pluginImporter.importDataSource(pluginInfo);
const isBackend = plugin.DataSourceClass.prototype instanceof DataSourceWithBackend;
const meta = {
...pluginInfo,
@@ -16,6 +16,7 @@ import { getAllSuggestions, panelsToCheckFirst } from './getAllSuggestions';
for (const pluginId of panelsToCheckFirst) {
config.panels[pluginId] = {
module: `core:plugin/${pluginId}`,
id: pluginId,
} as PanelPluginMeta;
}
@@ -15,7 +15,7 @@ import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsR
import { AddedFunctionsRegistry } from '../extensions/registry/AddedFunctionsRegistry';
import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry';
import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry';
import { importAppPlugin } from '../pluginLoader';
import { pluginImporter } from '../importer/pluginImporter';
import { getPluginSettings } from '../pluginSettings';
import AppRootPage from './AppRootPage';
@@ -23,8 +23,8 @@ import AppRootPage from './AppRootPage';
jest.mock('../pluginSettings', () => ({
getPluginSettings: jest.fn(),
}));
jest.mock('../pluginLoader', () => ({
importAppPlugin: jest.fn(),
jest.mock('../importer/pluginImporter', () => ({
pluginImporter: { importApp: jest.fn() },
}));
jest.mock('@grafana/runtime', () => ({
@@ -45,9 +45,9 @@ jest.mock('@grafana/runtime', () => ({
},
}));
const importAppPluginMock = importAppPlugin as jest.Mock<
ReturnType<typeof importAppPlugin>,
Parameters<typeof importAppPlugin>
const importAppPluginMock = pluginImporter.importApp as jest.Mock<
ReturnType<typeof pluginImporter.importApp>,
Parameters<typeof pluginImporter.importApp>
>;
const getPluginSettingsMock = getPluginSettings as jest.Mock<
@@ -32,7 +32,7 @@ import {
useExposedComponentsRegistry,
useAddedFunctionsRegistry,
} from '../extensions/ExtensionRegistriesContext';
import { importAppPlugin } from '../pluginLoader';
import { pluginImporter } from '../importer/pluginImporter';
import { getPluginSettings } from '../pluginSettings';
import { buildPluginSectionNav, pluginsLogger } from '../utils';
@@ -234,7 +234,7 @@ async function loadAppPlugin(pluginId: string, dispatch: React.Dispatch<AnyActio
dispatch(stateSlice.actions.setState({ pluginNav: getWarningNav(error) }));
return null;
}
return importAppPlugin(info);
return pluginImporter.importApp(info);
});
dispatch(stateSlice.actions.setState({ plugin: app, loading: false, loadingError: false, pluginNav: null }));
} catch (err) {
@@ -7,7 +7,6 @@ import {
DataSourceApi,
DataSourceInstanceSettings,
DataSourcePlugin,
DataSourcePluginMeta,
ScopedVars,
} from '@grafana/data';
import { RuntimeDataSource, TemplateSrv } from '@grafana/runtime';
@@ -61,9 +60,9 @@ class TestRuntimeDataSource extends RuntimeDataSource {
}
}
jest.mock('./pluginLoader', () => ({
importDataSourcePlugin: (meta: DataSourcePluginMeta) => {
return Promise.resolve(new DataSourcePlugin(TestDataSource as any));
jest.mock('./importer/pluginImporter', () => ({
pluginImporter: {
importDataSource: () => Promise.resolve(new DataSourcePlugin(TestDataSource as any)),
},
}));
@@ -29,7 +29,7 @@ import {
} from 'app/features/expressions/ExpressionDatasource';
import { ExpressionDatasourceUID } from 'app/features/expressions/types';
import { importDataSourcePlugin } from './pluginLoader';
import { pluginImporter } from './importer/pluginImporter';
export class DatasourceSrv implements DataSourceService {
private datasources: Record<string, DataSourceApi> = {}; // UID
@@ -215,7 +215,7 @@ export class DatasourceSrv implements DataSourceService {
}
try {
const dsPlugin = await importDataSourcePlugin(instanceSettings.meta);
const dsPlugin = await pluginImporter.importDataSource(instanceSettings.meta);
// check if its in cache now
if (this.datasources[key]) {
return this.datasources[key];
@@ -1,13 +1,9 @@
import { PanelPlugin, PanelPluginMeta, PluginLoadingStrategy, throwIfAngular } from '@grafana/data';
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
import config from 'app/core/config';
import { getPanelPluginLoadError } from '../panel/components/PanelPluginError';
import { importPluginModule } from './importer/importPluginModule';
import { pluginImporter } from './importer/pluginImporter';
const promiseCache: Record<string, Promise<PanelPlugin>> = {};
const panelPluginCache: Record<string, PanelPlugin> = {};
export function importPanelPlugin(id: string): Promise<PanelPlugin> {
const loaded = promiseCache[id];
@@ -51,45 +47,9 @@ export function importPanelPluginFromMeta(meta: PanelPluginMeta): Promise<PanelP
}
export function syncGetPanelPlugin(id: string): PanelPlugin | undefined {
if (config.featureToggles.enablePluginImporter) {
return pluginImporter.getPanel(id);
}
return panelPluginCache[id];
return pluginImporter.getPanel(id);
}
function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
if (config.featureToggles.enablePluginImporter) {
return pluginImporter.importPanel(meta);
}
throwIfAngular(meta);
const fallbackLoadingStrategy = meta.loadingStrategy ?? PluginLoadingStrategy.fetch;
return importPluginModule({
path: meta.module,
version: meta.info?.version,
loadingStrategy: fallbackLoadingStrategy,
pluginId: meta.id,
moduleHash: meta.moduleHash,
translations: meta.translations,
})
.then((pluginExports) => {
if (pluginExports.plugin) {
return pluginExports.plugin;
}
throwIfAngular(pluginExports);
throw new Error('missing export: plugin');
})
.then((plugin: PanelPlugin) => {
plugin.meta = meta;
panelPluginCache[meta.id] = plugin;
return plugin;
})
.catch((err) => {
// TODO, maybe a different error plugin
console.warn('Error loading panel plugin: ' + meta.id, err);
return getPanelPluginLoadError(meta, err);
});
return pluginImporter.importPanel(meta);
}
@@ -76,7 +76,6 @@ export async function importPluginModule({
expectedHash: moduleHash ?? '',
loadingStrategy: loadingStrategy.toString(),
sriChecksEnabled: String(Boolean(config.featureToggles.pluginsSriChecks)),
newPluginLoadingEnabled: String(Boolean(config.featureToggles.enablePluginImporter)),
originalErrorMessage: e.originalErr?.message || '',
originalErrorStack: e.originalErr?.stack || '',
systemJSOriginalErr: e.originalErr?.message || '',
@@ -337,7 +337,6 @@ describe('pluginImporter', () => {
expect(logSpy).toHaveBeenCalledWith(`Retrieving plugin from cache`, {
expectedHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b',
loadingStrategy: 'fetch',
newPluginLoadingEnabled: 'false',
path: 'public/plugins/test-plugin/module.js',
pluginId: 'test-plugin',
pluginVersion: '1.0.0',
@@ -368,7 +367,6 @@ describe('pluginImporter', () => {
expect(logSpy).toHaveBeenCalledWith(`Retrieving plugin from inflight plugin load request`, {
expectedHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b',
loadingStrategy: 'fetch',
newPluginLoadingEnabled: 'false',
path: 'public/plugins/test-plugin/module.js',
pluginId: 'test-plugin',
pluginVersion: '1.0.0',
@@ -146,7 +146,6 @@ const importPlugin = <M extends PluginMeta, P extends PanelPlugin | GenericDataS
expectedHash: meta.moduleHash ?? '',
loadingStrategy: meta.loadingStrategy ?? PluginLoadingStrategy.fetch,
sriChecksEnabled: String(Boolean(config.featureToggles.pluginsSriChecks)),
newPluginLoadingEnabled: String(Boolean(config.featureToggles.enablePluginImporter)),
});
return Promise.resolve(cached);
}
@@ -159,7 +158,6 @@ const importPlugin = <M extends PluginMeta, P extends PanelPlugin | GenericDataS
expectedHash: meta.moduleHash ?? '',
loadingStrategy: meta.loadingStrategy ?? PluginLoadingStrategy.fetch,
sriChecksEnabled: String(Boolean(config.featureToggles.pluginsSriChecks)),
newPluginLoadingEnabled: String(Boolean(config.featureToggles.enablePluginImporter)),
});
return getPromiseFromCache(meta);
}
@@ -1,12 +1,62 @@
import { PluginLoadingStrategy } from '@grafana/data';
import { config } from '@grafana/runtime';
import { transformPluginSourceForCDN } from '../cdn/utils';
import { LOAD_PLUGIN_CSS_REGEX, JS_CONTENT_TYPE_REGEX, SHARED_DEPENDENCY_PREFIX } from './constants';
import { resolvePluginUrlWithCache } from './pluginInfoCache';
import { getPluginInfoFromCache, resolvePluginUrlWithCache } from './pluginInfoCache';
// SystemJS has to be imported before the sharedDependenciesMap
import { SystemJS } from './systemjs';
// eslint-disable-next-line import/order
import { sharedDependenciesMap } from './sharedDependencies';
import { SystemJSWithLoaderHooks } from './types';
import { isHostedOnCDN } from './utils';
import { buildImportMap, isHostedOnCDN } from './utils';
export function initSystemJSHooks() {
const imports = buildImportMap(sharedDependenciesMap);
SystemJS.addImportMap({ imports });
const systemJSPrototype: SystemJSWithLoaderHooks = SystemJS.constructor.prototype;
// This instructs SystemJS to load plugin assets using fetch and eval if it returns a truthy value, otherwise
// it will load the plugin using a script tag. The logic that sets loadingStrategy comes from the backend.
// See: pkg/services/pluginsintegration/pluginassets/pluginassets.go
systemJSPrototype.shouldFetch = function (url) {
const pluginInfo = getPluginInfoFromCache(url);
const jsTypeRegEx = /^[^#?]+\.(js)([?#].*)?$/;
if (!jsTypeRegEx.test(url)) {
return true;
}
return Boolean(pluginInfo?.loadingStrategy !== PluginLoadingStrategy.script);
};
const originalImport = systemJSPrototype.import;
// Hook Systemjs import to support plugins that only have a default export.
systemJSPrototype.import = function (...args: Parameters<typeof originalImport>) {
return originalImport.apply(this, args).then((module) => {
if (module && module.__useDefault) {
return module.default;
}
return module;
});
};
const systemJSFetch = systemJSPrototype.fetch;
systemJSPrototype.fetch = function (url: string, options?: Record<string, unknown>) {
return decorateSystemJSFetch(systemJSFetch, url, options);
};
const systemJSResolve = systemJSPrototype.resolve;
systemJSPrototype.resolve = decorateSystemJSResolve.bind(systemJSPrototype, systemJSResolve);
// Older plugins load .css files which resolves to a CSS Module.
// https://github.com/WICG/webcomponents/blob/gh-pages/proposals/css-modules-v1-explainer.md#importing-a-css-module
// Any css files loaded via SystemJS have their styles applied onload.
systemJSPrototype.onload = decorateSystemJsOnload;
}
export async function decorateSystemJSFetch(
systemJSFetch: SystemJSWithLoaderHooks['fetch'],
@@ -1,66 +0,0 @@
jest.mock('app/core/core', () => {
return {
coreModule: {
directive: jest.fn(),
},
};
});
import { AppPluginMeta, PluginMetaInfo, PluginType, AppPlugin } from '@grafana/data';
// Loaded after the `unmock` above
import { addedComponentsRegistry, addedLinksRegistry, exposedComponentsRegistry } from './extensions/registry/setup';
import { SystemJS } from './loader/systemjs';
import { importAppPlugin } from './pluginLoader';
jest.mock('./extensions/registry/setup');
describe('Load App', () => {
const app = new AppPlugin();
const modulePath = 'http://localhost:3000/public/plugins/my-app-plugin/module.js';
// Hook resolver for tests
const originalResolve = SystemJS.constructor.prototype.resolve;
SystemJS.constructor.prototype.resolve = (x: unknown) => x;
beforeAll(() => {
app.init = jest.fn();
addedComponentsRegistry.register = jest.fn();
addedLinksRegistry.register = jest.fn();
exposedComponentsRegistry.register = jest.fn();
SystemJS.set(modulePath, { plugin: app });
});
afterAll(() => {
SystemJS.delete(modulePath);
SystemJS.constructor.prototype.resolve = originalResolve;
});
it('should call init and set meta', async () => {
const meta: AppPluginMeta = {
id: 'test-app',
module: modulePath,
baseUrl: 'xxx',
info: {} as PluginMetaInfo,
type: PluginType.app,
name: 'test',
};
// Check that we mocked the import OK
const m = await SystemJS.import(modulePath);
expect(m.plugin).toBe(app);
// Importing the app should initialise the meta
const importedApp = await importAppPlugin(meta);
expect(importedApp).toBe(app);
expect(app.meta).toBe(meta);
// Importing the same app again doesn't initialise it twice
const importedAppAgain = await importAppPlugin(meta);
expect(importedAppAgain).toBe(app);
expect(app.init).toHaveBeenCalledTimes(1);
expect(addedComponentsRegistry.register).toHaveBeenCalledTimes(1);
expect(addedLinksRegistry.register).toHaveBeenCalledTimes(1);
expect(exposedComponentsRegistry.register).toHaveBeenCalledTimes(1);
});
});
-166
View File
@@ -1,166 +0,0 @@
import {
AppPlugin,
DataSourceApi,
DataSourceJsonData,
DataSourcePlugin,
DataSourcePluginMeta,
PluginLoadingStrategy,
PluginMeta,
throwIfAngular,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { GenericDataSourcePlugin } from '../datasources/types';
import {
addedComponentsRegistry,
addedFunctionsRegistry,
addedLinksRegistry,
exposedComponentsRegistry,
} from './extensions/registry/setup';
import { importPluginModule } from './importer/importPluginModule';
import { pluginImporter } from './importer/pluginImporter';
import { getPluginInfoFromCache } from './loader/pluginInfoCache';
// SystemJS has to be imported before the sharedDependenciesMap
import { SystemJS } from './loader/systemjs';
// eslint-disable-next-line import/order
import { sharedDependenciesMap } from './loader/sharedDependencies';
import { decorateSystemJSFetch, decorateSystemJSResolve, decorateSystemJsOnload } from './loader/systemjsHooks';
import { SystemJSWithLoaderHooks } from './loader/types';
import { buildImportMap } from './loader/utils';
const imports = buildImportMap(sharedDependenciesMap);
SystemJS.addImportMap({ imports });
const systemJSPrototype: SystemJSWithLoaderHooks = SystemJS.constructor.prototype;
// This instructs SystemJS to load plugin assets using fetch and eval if it returns a truthy value, otherwise
// it will load the plugin using a script tag. The logic that sets loadingStrategy comes from the backend.
// See: pkg/services/pluginsintegration/pluginassets/pluginassets.go
systemJSPrototype.shouldFetch = function (url) {
const pluginInfo = getPluginInfoFromCache(url);
const jsTypeRegEx = /^[^#?]+\.(js)([?#].*)?$/;
if (!jsTypeRegEx.test(url)) {
return true;
}
return Boolean(pluginInfo?.loadingStrategy !== PluginLoadingStrategy.script);
};
const originalImport = systemJSPrototype.import;
// Hook Systemjs import to support plugins that only have a default export.
systemJSPrototype.import = function (...args: Parameters<typeof originalImport>) {
return originalImport.apply(this, args).then((module) => {
if (module && module.__useDefault) {
return module.default;
}
return module;
});
};
const systemJSFetch = systemJSPrototype.fetch;
systemJSPrototype.fetch = function (url: string, options?: Record<string, unknown>) {
return decorateSystemJSFetch(systemJSFetch, url, options);
};
const systemJSResolve = systemJSPrototype.resolve;
systemJSPrototype.resolve = decorateSystemJSResolve.bind(systemJSPrototype, systemJSResolve);
// Older plugins load .css files which resolves to a CSS Module.
// https://github.com/WICG/webcomponents/blob/gh-pages/proposals/css-modules-v1-explainer.md#importing-a-css-module
// Any css files loaded via SystemJS have their styles applied onload.
systemJSPrototype.onload = decorateSystemJsOnload;
export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<GenericDataSourcePlugin> {
if (config.featureToggles.enablePluginImporter) {
return pluginImporter.importDataSource(meta);
}
throwIfAngular(meta);
const fallbackLoadingStrategy = meta.loadingStrategy ?? PluginLoadingStrategy.fetch;
return importPluginModule({
path: meta.module,
version: meta.info?.version,
loadingStrategy: fallbackLoadingStrategy,
pluginId: meta.id,
moduleHash: meta.moduleHash,
translations: meta.translations,
}).then((pluginExports) => {
if (pluginExports.plugin) {
const dsPlugin: GenericDataSourcePlugin = pluginExports.plugin;
dsPlugin.meta = meta;
return dsPlugin;
}
if (pluginExports.Datasource) {
const dsPlugin = new DataSourcePlugin<
DataSourceApi<DataQuery, DataSourceJsonData>,
DataQuery,
DataSourceJsonData
>(pluginExports.Datasource);
dsPlugin.setComponentsFromLegacyExports(pluginExports);
dsPlugin.meta = meta;
return dsPlugin;
}
throw new Error('Plugin module is missing DataSourcePlugin or Datasource constructor export');
});
}
// Cache for import promises to prevent duplicate imports
const importPromises: Record<string, Promise<AppPlugin>> = {};
export async function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
if (config.featureToggles.enablePluginImporter) {
return pluginImporter.importApp(meta);
}
const pluginId = meta.id;
// We are caching the import promises to prevent duplicate imports
if (importPromises[pluginId] === undefined) {
importPromises[pluginId] = doImportAppPlugin(meta);
}
return importPromises[pluginId];
}
async function doImportAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
throwIfAngular(meta);
const pluginExports = await importPluginModule({
path: meta.module,
version: meta.info?.version,
pluginId: meta.id,
loadingStrategy: meta.loadingStrategy ?? PluginLoadingStrategy.fetch,
moduleHash: meta.moduleHash,
translations: meta.translations,
});
const { plugin = new AppPlugin() } = pluginExports;
plugin.init(meta);
plugin.meta = meta;
plugin.setComponentsFromLegacyExports(pluginExports);
exposedComponentsRegistry.register({
pluginId: meta.id,
configs: plugin.exposedComponentConfigs || [],
});
addedComponentsRegistry.register({
pluginId: meta.id,
configs: plugin.addedComponentConfigs || [],
});
addedLinksRegistry.register({
pluginId: meta.id,
configs: plugin.addedLinkConfigs || [],
});
addedFunctionsRegistry.register({
pluginId: meta.id,
configs: plugin.addedFunctionConfigs || [],
});
return plugin;
}
@@ -11,7 +11,7 @@ import {
import type { AppPluginConfig } from '@grafana/runtime';
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
import { importAppPlugin } from './pluginLoader';
import { pluginImporter } from './importer/pluginImporter';
import { clearPreloadedPluginsCache, preloadPlugins } from './pluginPreloader';
jest.mock('app/core/services/context_srv', () => ({
@@ -26,12 +26,12 @@ jest.mock('app/features/plugins/pluginSettings', () => ({
getPluginSettings: jest.fn(),
}));
jest.mock('./pluginLoader', () => ({
importAppPlugin: jest.fn(),
jest.mock('./importer/pluginImporter', () => ({
pluginImporter: { importApp: jest.fn() },
}));
const getPluginSettingsMock = jest.mocked(getPluginSettings);
const importAppPluginMock = jest.mocked(importAppPlugin);
const importAppPluginMock = jest.mocked(pluginImporter.importApp);
const createMockAppPluginConfig = (overrides: Partial<AppPluginConfig> = {}): AppPluginConfig => ({
id: 'test-plugin',
@@ -7,7 +7,7 @@ import type { AppPluginConfig } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
import { importAppPlugin } from './pluginLoader';
import { pluginImporter } from './importer/pluginImporter';
export type PluginPreloadResult = {
pluginId: string;
@@ -41,7 +41,7 @@ async function preload(config: AppPluginConfig): Promise<void> {
showErrorAlert: contextSrv.user.orgRole !== '',
});
await importAppPlugin(meta);
await pluginImporter.importApp(meta);
} catch (error) {
console.error(`[Plugins] Failed to preload plugin: ${config.path} (version: ${config.version})`, error);
}
+3 -3
View File
@@ -2,7 +2,7 @@ import { GrafanaPlugin, NavModel, NavModelItem, PanelPluginMeta, PluginType } fr
import { createMonitoringLogger } from '@grafana/runtime';
import { importPanelPluginFromMeta } from './importPanelPlugin';
import { importAppPlugin, importDataSourcePlugin } from './pluginLoader';
import { pluginImporter } from './importer/pluginImporter';
import { getPluginSettings } from './pluginSettings';
export async function loadPlugin(pluginId: string): Promise<GrafanaPlugin> {
@@ -10,10 +10,10 @@ export async function loadPlugin(pluginId: string): Promise<GrafanaPlugin> {
let result: GrafanaPlugin | undefined;
if (info.type === PluginType.app) {
result = await importAppPlugin(info);
result = await pluginImporter.importApp(info);
}
if (info.type === PluginType.datasource) {
result = await importDataSourcePlugin(info);
result = await pluginImporter.importDataSource(info);
}
if (info.type === PluginType.panel) {
const panelPlugin = await importPanelPluginFromMeta(info as PanelPluginMeta);
@@ -3,7 +3,7 @@ import { useCallback } from 'react';
import { DataQuery, DataSourceApi, DataSourceJsonData, QueryEditorProps, StandardVariableQuery } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { importDataSourcePlugin } from '../../plugins/pluginLoader';
import { pluginImporter } from '../../plugins/importer/pluginImporter';
import {
hasCustomVariableSupport,
hasDatasourceVariableSupport,
@@ -20,7 +20,7 @@ export async function getVariableQueryEditor<
VariableQuery extends DataQuery = TQuery,
>(
datasource: DataSourceApi<TQuery, TOptions>,
importDataSourcePluginFunc = importDataSourcePlugin
importDataSourcePluginFunc = pluginImporter.importDataSource
): Promise<VariableQueryEditorType> {
if (hasCustomVariableSupport(datasource)) {
return datasource.variables.editor;
@@ -60,7 +60,7 @@ const mocks: Record<string, any> = {
getList: jest.fn().mockReturnValue([]),
},
pluginLoader: {
importDataSourcePlugin: jest.fn().mockResolvedValue({ components: {} }),
importDataSource: jest.fn().mockResolvedValue({ components: {} }),
},
VariableQueryEditor(props: VariableQueryEditorProps) {
return <div>this is a variable query editor</div>;
@@ -69,8 +69,8 @@ const mocks: Record<string, any> = {
setDataSourceSrv(mocks.dataSourceSrv as DataSourceSrv);
jest.mock('../../plugins/pluginLoader', () => ({
importDataSourcePlugin: () => mocks.pluginLoader.importDataSourcePlugin(),
jest.mock('../../plugins/importer/pluginImporter', () => ({
pluginImporter: { importDataSource: () => mocks.pluginLoader.importDataSource() },
}));
jest.mock('../../templating/template_srv', () => ({
@@ -252,7 +252,7 @@ describe('query actions', () => {
const editor = mocks.VariableQueryEditor;
mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([testMetricSource]);
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
mocks.pluginLoader.importDataSource = jest.fn().mockResolvedValue({
components: { VariableQueryEditor: editor },
});
@@ -280,7 +280,7 @@ describe('query actions', () => {
const editor = mocks.VariableQueryEditor;
mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([testMetricSource]);
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
mocks.pluginLoader.importDataSource = jest.fn().mockResolvedValue({
components: { VariableQueryEditor: editor },
});
@@ -307,7 +307,7 @@ describe('query actions', () => {
const editor = mocks.VariableQueryEditor;
mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([]);
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
mocks.pluginLoader.importDataSource = jest.fn().mockResolvedValue({
components: { VariableQueryEditor: editor },
});
@@ -333,7 +333,7 @@ describe('query actions', () => {
const variable = createVariable({ datasource: { uid: 'other' } });
const editor = mocks.VariableQueryEditor;
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
mocks.pluginLoader.importDataSource = jest.fn().mockResolvedValue({
components: { VariableQueryEditor: editor },
});
@@ -369,7 +369,7 @@ describe('query actions', () => {
};
const preloadedState = getPreloadedState('key', templatingState);
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
mocks.pluginLoader.importDataSource = jest.fn().mockResolvedValue({
components: { VariableQueryEditor: editor },
});
@@ -400,7 +400,7 @@ describe('query actions', () => {
const variable = createVariable({ datasource: { uid: 'other' } });
const editor = LegacyVariableQueryEditor;
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
mocks.pluginLoader.importDataSource = jest.fn().mockResolvedValue({
components: {},
});