From c0391ad2ead74d8950bb0e5f91537794145570e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 23 Oct 2025 06:18:23 +0200 Subject: [PATCH] Chore: Enable new pluginImporter (#112625) * Chore: Enable new pluginImporter * chore: remove pluginLoader --- .../src/types/featureToggles.gen.ts | 5 - pkg/services/featuremgmt/registry.go | 10 -- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 3 +- public/app/app.ts | 3 + .../history/RichHistoryRemoteStorage.test.ts | 2 +- .../app/features/datasources/state/actions.ts | 8 +- .../panel/state/getAllSuggestions.test.ts | 1 + .../plugins/components/AppRootPage.test.tsx | 12 +- .../plugins/components/AppRootPage.tsx | 4 +- .../features/plugins/datasource_srv.test.ts | 7 +- public/app/features/plugins/datasource_srv.ts | 4 +- .../app/features/plugins/importPanelPlugin.ts | 46 +---- .../plugins/importer/importPluginModule.ts | 1 - .../plugins/importer/pluginImporter.test.ts | 2 - .../plugins/importer/pluginImporter.ts | 2 - .../features/plugins/loader/systemjsHooks.ts | 54 +++++- .../app/features/plugins/pluginLoader.test.ts | 66 ------- public/app/features/plugins/pluginLoader.ts | 166 ------------------ .../features/plugins/pluginPreloader.test.ts | 8 +- .../app/features/plugins/pluginPreloader.ts | 4 +- public/app/features/plugins/utils.ts | 6 +- .../editor/getVariableQueryEditor.tsx | 4 +- .../features/variables/query/actions.test.tsx | 18 +- 25 files changed, 99 insertions(+), 342 deletions(-) delete mode 100644 public/app/features/plugins/pluginLoader.test.ts delete mode 100644 public/app/features/plugins/pluginLoader.ts diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index df3221f5cf8..3b2f3214f04 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -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; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index c50bbd6a727..34e66bc254f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 1f24ed369a6..b232bbee6e7 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index ce8fab8f287..452f3fab788 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 821558b66a6..9d56cde1e30 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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", diff --git a/public/app/app.ts b/public/app/app.ts index 48b97f40024..76a77a46fd7 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -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) { diff --git a/public/app/core/history/RichHistoryRemoteStorage.test.ts b/public/app/core/history/RichHistoryRemoteStorage.test.ts index b99967b09b7..4471554a8b8 100644 --- a/public/app/core/history/RichHistoryRemoteStorage.test.ts +++ b/public/app/core/history/RichHistoryRemoteStorage.test.ts @@ -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; diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 09835ce72b2..500aac05ef9 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -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 => { return async (dispatch, getState) => { @@ -225,7 +225,7 @@ export function loadDataSource(uid: string): ThunkResult { 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, diff --git a/public/app/features/panel/state/getAllSuggestions.test.ts b/public/app/features/panel/state/getAllSuggestions.test.ts index 552c80a74b5..0ce616f9309 100644 --- a/public/app/features/panel/state/getAllSuggestions.test.ts +++ b/public/app/features/panel/state/getAllSuggestions.test.ts @@ -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; } diff --git a/public/app/features/plugins/components/AppRootPage.test.tsx b/public/app/features/plugins/components/AppRootPage.test.tsx index 1b054508724..621556d8a02 100644 --- a/public/app/features/plugins/components/AppRootPage.test.tsx +++ b/public/app/features/plugins/components/AppRootPage.test.tsx @@ -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, - Parameters +const importAppPluginMock = pluginImporter.importApp as jest.Mock< + ReturnType, + Parameters >; const getPluginSettingsMock = getPluginSettings as jest.Mock< diff --git a/public/app/features/plugins/components/AppRootPage.tsx b/public/app/features/plugins/components/AppRootPage.tsx index ad94e6ea460..0a8d9efd9c0 100644 --- a/public/app/features/plugins/components/AppRootPage.tsx +++ b/public/app/features/plugins/components/AppRootPage.tsx @@ -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 ({ - importDataSourcePlugin: (meta: DataSourcePluginMeta) => { - return Promise.resolve(new DataSourcePlugin(TestDataSource as any)); +jest.mock('./importer/pluginImporter', () => ({ + pluginImporter: { + importDataSource: () => Promise.resolve(new DataSourcePlugin(TestDataSource as any)), }, })); diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 99dda9e89a1..f330bb7f76e 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -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 = {}; // 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]; diff --git a/public/app/features/plugins/importPanelPlugin.ts b/public/app/features/plugins/importPanelPlugin.ts index 2cf402cb2b6..1357c31f505 100644 --- a/public/app/features/plugins/importPanelPlugin.ts +++ b/public/app/features/plugins/importPanelPlugin.ts @@ -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> = {}; -const panelPluginCache: Record = {}; export function importPanelPlugin(id: string): Promise { const loaded = promiseCache[id]; @@ -51,45 +47,9 @@ export function importPanelPluginFromMeta(meta: PanelPluginMeta): Promise { - 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); } diff --git a/public/app/features/plugins/importer/importPluginModule.ts b/public/app/features/plugins/importer/importPluginModule.ts index 3ba0f3b58c8..a571f8ec59a 100644 --- a/public/app/features/plugins/importer/importPluginModule.ts +++ b/public/app/features/plugins/importer/importPluginModule.ts @@ -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 || '', diff --git a/public/app/features/plugins/importer/pluginImporter.test.ts b/public/app/features/plugins/importer/pluginImporter.test.ts index a8dd2254b59..f5cb4738fa8 100644 --- a/public/app/features/plugins/importer/pluginImporter.test.ts +++ b/public/app/features/plugins/importer/pluginImporter.test.ts @@ -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', diff --git a/public/app/features/plugins/importer/pluginImporter.ts b/public/app/features/plugins/importer/pluginImporter.ts index 742a6739463..8566f665b94 100644 --- a/public/app/features/plugins/importer/pluginImporter.ts +++ b/public/app/features/plugins/importer/pluginImporter.ts @@ -146,7 +146,6 @@ const importPlugin = ) { + 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) { + 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'], diff --git a/public/app/features/plugins/pluginLoader.test.ts b/public/app/features/plugins/pluginLoader.test.ts deleted file mode 100644 index ea91ffb7d3e..00000000000 --- a/public/app/features/plugins/pluginLoader.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/public/app/features/plugins/pluginLoader.ts b/public/app/features/plugins/pluginLoader.ts deleted file mode 100644 index 377874a0a5f..00000000000 --- a/public/app/features/plugins/pluginLoader.ts +++ /dev/null @@ -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) { - 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) { - 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 { - 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 - >(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> = {}; - -export async function importAppPlugin(meta: PluginMeta): Promise { - 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 { - 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; -} diff --git a/public/app/features/plugins/pluginPreloader.test.ts b/public/app/features/plugins/pluginPreloader.test.ts index 4890184a638..29aef9d46cc 100644 --- a/public/app/features/plugins/pluginPreloader.test.ts +++ b/public/app/features/plugins/pluginPreloader.test.ts @@ -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 => ({ id: 'test-plugin', diff --git a/public/app/features/plugins/pluginPreloader.ts b/public/app/features/plugins/pluginPreloader.ts index f1bec151de1..938d42458dc 100644 --- a/public/app/features/plugins/pluginPreloader.ts +++ b/public/app/features/plugins/pluginPreloader.ts @@ -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 { 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); } diff --git a/public/app/features/plugins/utils.ts b/public/app/features/plugins/utils.ts index 9961ee71016..17df6db1a89 100644 --- a/public/app/features/plugins/utils.ts +++ b/public/app/features/plugins/utils.ts @@ -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 { @@ -10,10 +10,10 @@ export async function loadPlugin(pluginId: string): Promise { 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); diff --git a/public/app/features/variables/editor/getVariableQueryEditor.tsx b/public/app/features/variables/editor/getVariableQueryEditor.tsx index ad96e78cca1..5cba6fbf54b 100644 --- a/public/app/features/variables/editor/getVariableQueryEditor.tsx +++ b/public/app/features/variables/editor/getVariableQueryEditor.tsx @@ -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, - importDataSourcePluginFunc = importDataSourcePlugin + importDataSourcePluginFunc = pluginImporter.importDataSource ): Promise { if (hasCustomVariableSupport(datasource)) { return datasource.variables.editor; diff --git a/public/app/features/variables/query/actions.test.tsx b/public/app/features/variables/query/actions.test.tsx index 7a203bd4b77..9647aa7cbf2 100644 --- a/public/app/features/variables/query/actions.test.tsx +++ b/public/app/features/variables/query/actions.test.tsx @@ -60,7 +60,7 @@ const mocks: Record = { getList: jest.fn().mockReturnValue([]), }, pluginLoader: { - importDataSourcePlugin: jest.fn().mockResolvedValue({ components: {} }), + importDataSource: jest.fn().mockResolvedValue({ components: {} }), }, VariableQueryEditor(props: VariableQueryEditorProps) { return
this is a variable query editor
; @@ -69,8 +69,8 @@ const mocks: Record = { 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: {}, });