Chore: Enable new pluginImporter (#112625)
* Chore: Enable new pluginImporter * chore: remove pluginLoader
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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: {},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user