diff --git a/eslint-suppressions.json b/eslint-suppressions.json index e1727b20713..459f67c6d1f 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2764,56 +2764,6 @@ "count": 1 } }, - "public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts": { - "no-restricted-syntax": { - "count": 6 - } - }, - "public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts": { - "no-restricted-syntax": { - "count": 6 - } - }, - "public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts": { - "no-restricted-syntax": { - "count": 6 - } - }, - "public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts": { - "no-restricted-syntax": { - "count": 6 - } - }, - "public/app/features/plugins/extensions/usePluginComponent.test.tsx": { - "no-restricted-syntax": { - "count": 3 - } - }, - "public/app/features/plugins/extensions/usePluginComponents.test.tsx": { - "no-restricted-syntax": { - "count": 2 - } - }, - "public/app/features/plugins/extensions/usePluginFunctions.test.tsx": { - "no-restricted-syntax": { - "count": 2 - } - }, - "public/app/features/plugins/extensions/usePluginLinks.test.tsx": { - "no-restricted-syntax": { - "count": 2 - } - }, - "public/app/features/plugins/extensions/validators.test.tsx": { - "no-restricted-syntax": { - "count": 30 - } - }, - "public/app/features/plugins/extensions/validators.ts": { - "no-restricted-syntax": { - "count": 4 - } - }, "public/app/features/plugins/sandbox/distortions.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index 07296cc07a1..cfd930ae853 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -17,7 +17,8 @@ import { RouteDescriptor } from './core/navigation/types'; import { ThemeProvider } from './core/utils/ConfigProvider'; import { LiveConnectionWarning } from './features/live/LiveConnectionWarning'; import { ExtensionRegistriesProvider } from './features/plugins/extensions/ExtensionRegistriesContext'; -import { pluginExtensionRegistries } from './features/plugins/extensions/registry/setup'; +import { getPluginExtensionRegistries } from './features/plugins/extensions/registry/setup'; +import { PluginExtensionRegistries } from './features/plugins/extensions/registry/types'; import { ScopesContextProvider } from './features/scopes/ScopesContextProvider'; import { RouterWrapper } from './routes/RoutesWrapper'; @@ -27,6 +28,7 @@ interface AppWrapperProps { interface AppWrapperState { ready?: boolean; + registries?: PluginExtensionRegistries; } /** Used by enterprise */ @@ -55,7 +57,8 @@ export class AppWrapper extends Component { } async componentDidMount() { - this.setState({ ready: true }); + const registries = await getPluginExtensionRegistries(); + this.setState({ ready: true, registries }); this.removePreloader(); // clear any old icon caches @@ -93,7 +96,7 @@ export class AppWrapper extends Component { render() { const { context } = this.props; - const { ready } = this.state; + const { ready, registries } = this.state; navigationLogger('AppWrapper', false, 'rendering'); @@ -125,7 +128,7 @@ export class AppWrapper extends Component { > - + diff --git a/public/app/app.ts b/public/app/app.ts index 52834e2526c..eb871882501 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -95,6 +95,7 @@ import { getObservablePluginComponents, getObservablePluginLinks, } from './features/plugins/extensions/getPluginExtensions'; +import { getPluginExtensionRegistries } from './features/plugins/extensions/registry/setup'; import { usePluginComponent } from './features/plugins/extensions/usePluginComponent'; import { usePluginComponents } from './features/plugins/extensions/usePluginComponents'; import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions'; @@ -262,6 +263,8 @@ export class GrafanaApp { preloadPlugins(await getAppPluginsToPreload()); } + getPluginExtensionRegistries(); + setHelpNavItemHook(useHelpNode); setPluginLinksHook(usePluginLinks); setPluginComponentHook(usePluginComponent); diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index cb671517eaa..2cd8a4f5a54 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -26,7 +26,8 @@ import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/co import { InspectTab } from 'app/features/inspector/types'; import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; import { createPluginExtensionsGetter } from 'app/features/plugins/extensions/getPluginExtensions'; -import { pluginExtensionRegistries } from 'app/features/plugins/extensions/registry/setup'; +import { getPluginExtensionRegistries } from 'app/features/plugins/extensions/registry/setup'; +import { PluginExtensionRegistries } from 'app/features/plugins/extensions/registry/types'; import { GetPluginExtensions } from 'app/features/plugins/extensions/types'; import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils'; import { dispatch } from 'app/store/store'; @@ -47,12 +48,12 @@ import { PanelTimeRangeDrawer } from './panel-timerange/PanelTimeRangeDrawer'; let getPluginExtensions: GetPluginExtensions; -function setupGetPluginExtensions() { +function setupGetPluginExtensions(registries: PluginExtensionRegistries) { if (getPluginExtensions) { return getPluginExtensions; } - getPluginExtensions = createPluginExtensionsGetter(pluginExtensionRegistries); + getPluginExtensions = createPluginExtensionsGetter(registries); return getPluginExtensions; } @@ -300,7 +301,8 @@ export function panelMenuBehavior(menu: VizPanelMenu) { }); } - setupGetPluginExtensions(); + const registries = await getPluginExtensionRegistries(); + setupGetPluginExtensions(registries); const { extensions } = getPluginExtensions({ extensionPointId: PluginExtensionPoints.DashboardPanelMenu, diff --git a/public/app/features/plugins/components/AppRootPage.test.tsx b/public/app/features/plugins/components/AppRootPage.test.tsx index 621556d8a02..ce0638885d0 100644 --- a/public/app/features/plugins/components/AppRootPage.test.tsx +++ b/public/app/features/plugins/components/AppRootPage.test.tsx @@ -89,10 +89,10 @@ function renderUnderRouter(page = '') { appPluginNavItem.parentItem = appsSection; const registries = { - addedComponentsRegistry: new AddedComponentsRegistry(), - exposedComponentsRegistry: new ExposedComponentsRegistry(), - addedLinksRegistry: new AddedLinksRegistry(), - addedFunctionsRegistry: new AddedFunctionsRegistry(), + addedComponentsRegistry: new AddedComponentsRegistry([]), + exposedComponentsRegistry: new ExposedComponentsRegistry([]), + addedLinksRegistry: new AddedLinksRegistry([]), + addedFunctionsRegistry: new AddedFunctionsRegistry([]), }; const pagePath = page ? `/${page}` : ''; const route = { diff --git a/public/app/features/plugins/extensions/ExtensionRegistriesContext.tsx b/public/app/features/plugins/extensions/ExtensionRegistriesContext.tsx index 6d374d676a1..7bd5206ad69 100644 --- a/public/app/features/plugins/extensions/ExtensionRegistriesContext.tsx +++ b/public/app/features/plugins/extensions/ExtensionRegistriesContext.tsx @@ -8,7 +8,7 @@ import { ExposedComponentsRegistry } from 'app/features/plugins/extensions/regis import { PluginExtensionRegistries } from './registry/types'; export interface ExtensionRegistriesContextType { - registries: PluginExtensionRegistries; + registries?: PluginExtensionRegistries; } // Using a different context for each registry to avoid unnecessary re-renders @@ -53,6 +53,10 @@ export const ExtensionRegistriesProvider = ({ registries, children, }: PropsWithChildren) => { + if (!registries) { + return null; + } + return ( diff --git a/public/app/features/plugins/extensions/getPluginExtensions.test.tsx b/public/app/features/plugins/extensions/getPluginExtensions.test.tsx index c9d6ca7cafa..fcab10f804d 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.test.tsx +++ b/public/app/features/plugins/extensions/getPluginExtensions.test.tsx @@ -17,8 +17,10 @@ import { import { log } from './logs/log'; import { resetLogMock } from './logs/testUtils'; import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry'; +import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry'; import { AddedLinksRegistry } from './registry/AddedLinksRegistry'; -import { pluginExtensionRegistries } from './registry/setup'; +import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry'; +import { getPluginExtensionRegistries } from './registry/setup'; import { isReadOnlyProxy } from './utils'; import { assertPluginExtensionLink } from './validators'; @@ -39,6 +41,13 @@ jest.mock('./logs/log', () => { }; }); +jest.mock('./registry/setup', () => ({ + ...jest.requireActual('./registry/setup'), + getPluginExtensionRegistries: jest.fn(), +})); + +const getPluginExtensionRegistriesMock = jest.mocked(getPluginExtensionRegistries); + async function createRegistries( preloadResults: Array<{ pluginId: string; @@ -46,8 +55,8 @@ async function createRegistries( addedLinkConfigs: PluginExtensionAddedLinkConfig[]; }> ) { - const addedLinksRegistry = new AddedLinksRegistry(); - const addedComponentsRegistry = new AddedComponentsRegistry(); + const addedLinksRegistry = new AddedLinksRegistry([]); + const addedComponentsRegistry = new AddedComponentsRegistry([]); for (const { pluginId, addedLinkConfigs, addedComponentConfigs } of preloadResults) { addedLinksRegistry.register({ @@ -552,11 +561,27 @@ describe('getPluginExtensions()', () => { describe('getObservablePluginExtensions()', () => { const extensionPointId = 'grafana/dashboard/panel/menu/v1'; const pluginId = 'grafana-basic-app'; + let addedLinksRegistry: AddedLinksRegistry; + let addedComponentsRegistry: AddedComponentsRegistry; + let addedFunctionsRegistry: AddedFunctionsRegistry; + let exposedComponentsRegistry: ExposedComponentsRegistry; beforeEach(() => { - pluginExtensionRegistries.addedLinksRegistry = new AddedLinksRegistry(); - pluginExtensionRegistries.addedComponentsRegistry = new AddedComponentsRegistry(); - pluginExtensionRegistries.addedLinksRegistry.register({ + addedLinksRegistry = new AddedLinksRegistry([]); + addedComponentsRegistry = new AddedComponentsRegistry([]); + addedFunctionsRegistry = new AddedFunctionsRegistry([]); + exposedComponentsRegistry = new ExposedComponentsRegistry([]); + + const registries = { + addedComponentsRegistry, + addedFunctionsRegistry, + addedLinksRegistry, + exposedComponentsRegistry, + }; + + getPluginExtensionRegistriesMock.mockResolvedValue(registries); + + addedLinksRegistry.register({ pluginId, configs: [ { @@ -569,7 +594,7 @@ describe('getObservablePluginExtensions()', () => { ], }); - pluginExtensionRegistries.addedComponentsRegistry.register({ + addedComponentsRegistry.register({ pluginId, configs: [ { @@ -599,7 +624,7 @@ describe('getObservablePluginExtensions()', () => { const observable = getObservablePluginExtensions({ extensionPointId }).pipe(take(2)); setTimeout(() => { - pluginExtensionRegistries.addedLinksRegistry.register({ + addedLinksRegistry.register({ pluginId, configs: [ { @@ -631,11 +656,27 @@ describe('getObservablePluginExtensions()', () => { describe('getObservablePluginLinks()', () => { const extensionPointId = 'grafana/dashboard/panel/menu/v1'; const pluginId = 'grafana-basic-app'; + let addedLinksRegistry: AddedLinksRegistry; + let addedComponentsRegistry: AddedComponentsRegistry; + let addedFunctionsRegistry: AddedFunctionsRegistry; + let exposedComponentsRegistry: ExposedComponentsRegistry; - beforeEach(() => { - pluginExtensionRegistries.addedLinksRegistry = new AddedLinksRegistry(); - pluginExtensionRegistries.addedComponentsRegistry = new AddedComponentsRegistry(); - pluginExtensionRegistries.addedLinksRegistry.register({ + beforeEach(async () => { + addedLinksRegistry = new AddedLinksRegistry([]); + addedComponentsRegistry = new AddedComponentsRegistry([]); + addedFunctionsRegistry = new AddedFunctionsRegistry([]); + exposedComponentsRegistry = new ExposedComponentsRegistry([]); + + const registries = { + addedComponentsRegistry, + addedFunctionsRegistry, + addedLinksRegistry, + exposedComponentsRegistry, + }; + + getPluginExtensionRegistriesMock.mockResolvedValue(registries); + + addedLinksRegistry.register({ pluginId, configs: [ { @@ -648,7 +689,7 @@ describe('getObservablePluginLinks()', () => { ], }); - pluginExtensionRegistries.addedComponentsRegistry.register({ + addedComponentsRegistry.register({ pluginId, configs: [ { @@ -685,7 +726,7 @@ describe('getObservablePluginLinks()', () => { it('should be possible to receive the last state of the registry', async () => { // Register a new link - pluginExtensionRegistries.addedLinksRegistry.register({ + addedLinksRegistry.register({ pluginId, configs: [ { @@ -709,8 +750,12 @@ describe('getObservablePluginLinks()', () => { }); it('should receive an empty array if there are no links', async () => { - pluginExtensionRegistries.addedLinksRegistry = new AddedLinksRegistry(); - pluginExtensionRegistries.addedComponentsRegistry = new AddedComponentsRegistry(); + getPluginExtensionRegistriesMock.mockResolvedValue({ + addedLinksRegistry: new AddedLinksRegistry([]), + addedComponentsRegistry: new AddedComponentsRegistry([]), + addedFunctionsRegistry: new AddedFunctionsRegistry([]), + exposedComponentsRegistry: new ExposedComponentsRegistry([]), + }); const observable = getObservablePluginLinks({ extensionPointId }).pipe(first()); const links = await firstValueFrom(observable); @@ -722,11 +767,27 @@ describe('getObservablePluginLinks()', () => { describe('getObservablePluginComponents()', () => { const extensionPointId = 'grafana/dashboard/panel/menu/v1'; const pluginId = 'grafana-basic-app'; + let addedLinksRegistry: AddedLinksRegistry; + let addedComponentsRegistry: AddedComponentsRegistry; + let addedFunctionsRegistry: AddedFunctionsRegistry; + let exposedComponentsRegistry: ExposedComponentsRegistry; - beforeEach(() => { - pluginExtensionRegistries.addedLinksRegistry = new AddedLinksRegistry(); - pluginExtensionRegistries.addedComponentsRegistry = new AddedComponentsRegistry(); - pluginExtensionRegistries.addedLinksRegistry.register({ + beforeEach(async () => { + addedLinksRegistry = new AddedLinksRegistry([]); + addedComponentsRegistry = new AddedComponentsRegistry([]); + addedFunctionsRegistry = new AddedFunctionsRegistry([]); + exposedComponentsRegistry = new ExposedComponentsRegistry([]); + + const registries = { + addedComponentsRegistry, + addedFunctionsRegistry, + addedLinksRegistry, + exposedComponentsRegistry, + }; + + getPluginExtensionRegistriesMock.mockResolvedValue(registries); + + addedLinksRegistry.register({ pluginId, configs: [ { @@ -739,7 +800,7 @@ describe('getObservablePluginComponents()', () => { ], }); - pluginExtensionRegistries.addedComponentsRegistry.register({ + addedComponentsRegistry.register({ pluginId, configs: [ { @@ -776,7 +837,7 @@ describe('getObservablePluginComponents()', () => { it('should be possible to receive the last state of the registry', async () => { // Register a new component - pluginExtensionRegistries.addedComponentsRegistry.register({ + addedComponentsRegistry.register({ pluginId, configs: [ { @@ -801,8 +862,12 @@ describe('getObservablePluginComponents()', () => { }); it('should receive an empty array if there are no components', async () => { - pluginExtensionRegistries.addedLinksRegistry = new AddedLinksRegistry(); - pluginExtensionRegistries.addedComponentsRegistry = new AddedComponentsRegistry(); + getPluginExtensionRegistriesMock.mockResolvedValue({ + addedLinksRegistry: new AddedLinksRegistry([]), + addedComponentsRegistry: new AddedComponentsRegistry([]), + addedFunctionsRegistry: new AddedFunctionsRegistry([]), + exposedComponentsRegistry: new ExposedComponentsRegistry([]), + }); const observable = getObservablePluginComponents({ extensionPointId }).pipe(first()); const components = await firstValueFrom(observable); diff --git a/public/app/features/plugins/extensions/getPluginExtensions.ts b/public/app/features/plugins/extensions/getPluginExtensions.ts index 6f02a7daf77..3f06307990a 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.ts @@ -1,5 +1,5 @@ import { isString } from 'lodash'; -import { combineLatest, map, Observable } from 'rxjs'; +import { combineLatest, from, map, Observable, switchMap } from 'rxjs'; import { PluginExtensionTypes, @@ -13,7 +13,7 @@ import { log } from './logs/log'; import { AddedComponentRegistryItem } from './registry/AddedComponentsRegistry'; import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry'; import { RegistryType } from './registry/Registry'; -import { pluginExtensionRegistries } from './registry/setup'; +import { getPluginExtensionRegistries } from './registry/setup'; import type { PluginExtensionRegistries } from './registry/types'; import { GetExtensions, GetExtensionsOptions, GetPluginExtensions } from './types'; import { @@ -38,22 +38,25 @@ export const getObservablePluginExtensions = ( options: Omit ): Observable> => { const { extensionPointId } = options; - const { addedComponentsRegistry, addedLinksRegistry } = pluginExtensionRegistries; - return combineLatest([ - addedComponentsRegistry.asObservableSlice((state) => state[extensionPointId]), - addedLinksRegistry.asObservableSlice((state) => state[extensionPointId]), - ]).pipe( - map(([components, links]) => - getPluginExtensions({ - ...options, - addedComponentsRegistry: { - [extensionPointId]: components, - }, - addedLinksRegistry: { - [extensionPointId]: links, - }, - }) + return from(getPluginExtensionRegistries()).pipe( + switchMap((registries) => + combineLatest([ + registries.addedComponentsRegistry.asObservableSlice((state) => state[extensionPointId]), + registries.addedLinksRegistry.asObservableSlice((state) => state[extensionPointId]), + ]).pipe( + map(([components, links]) => + getPluginExtensions({ + ...options, + addedComponentsRegistry: { + [extensionPointId]: components, + }, + addedLinksRegistry: { + [extensionPointId]: links, + }, + }) + ) + ) ) ); }; diff --git a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts index 98c0fb1781b..e0053fc0442 100644 --- a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts @@ -1,11 +1,11 @@ import React from 'react'; import { firstValueFrom, take } from 'rxjs'; -import { PluginLoadingStrategy } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { AppPluginConfig } from '@grafana/data'; import { log } from '../logs/log'; import { resetLogMock } from '../logs/testUtils'; +import { basicApp } from '../test-fixtures/config.apps'; import { isGrafanaDevMode } from '../utils'; import { AddedComponentsRegistry } from './AddedComponentsRegistry'; @@ -30,48 +30,17 @@ jest.mock('../logs/log', () => { }); describe('AddedComponentsRegistry', () => { - const originalApps = config.apps; - const pluginId = 'grafana-basic-app'; - const appPluginConfig = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - extensions: { - addedLinks: [], - addedComponents: [], - addedFunctions: [], - exposedComponents: [], - extensionPoints: [], - }, - }; + const pluginId = basicApp.id; + const apps = [basicApp]; + const createRegistry = async (override: AppPluginConfig[] = apps) => new AddedComponentsRegistry(override); beforeEach(() => { resetLogMock(log); jest.mocked(isGrafanaDevMode).mockReturnValue(false); - config.apps = { - [pluginId]: appPluginConfig, - }; - }); - - afterEach(() => { - config.apps = originalApps; }); it('should return empty registry when no extensions registered', async () => { - const reactiveRegistry = new AddedComponentsRegistry(); + const reactiveRegistry = await createRegistry(); const observable = reactiveRegistry.asObservable(); const registry = await firstValueFrom(observable); expect(registry).toEqual({}); @@ -79,7 +48,7 @@ describe('AddedComponentsRegistry', () => { it('should be possible to register added components in the registry', async () => { const extensionPointId = `${pluginId}/hello-world/v1`; - const reactiveRegistry = new AddedComponentsRegistry(); + const reactiveRegistry = await createRegistry(); reactiveRegistry.register({ pluginId, @@ -107,7 +76,7 @@ describe('AddedComponentsRegistry', () => { const pluginId1 = 'grafana-basic-app'; const pluginId2 = 'grafana-basic-app2'; const extensionPointId = 'grafana/alerting/home'; - const reactiveRegistry = new AddedComponentsRegistry(); + const reactiveRegistry = await createRegistry(); // Register extensions for the first plugin reactiveRegistry.register({ @@ -166,7 +135,7 @@ describe('AddedComponentsRegistry', () => { const pluginId2 = 'grafana-basic-app2'; const extensionPointId1 = 'grafana/alerting/home'; const extensionPointId2 = 'grafana/user/profile/tab'; - const reactiveRegistry = new AddedComponentsRegistry(); + const reactiveRegistry = await createRegistry(); // Register extensions for the first plugin reactiveRegistry.register({ @@ -226,7 +195,7 @@ describe('AddedComponentsRegistry', () => { }); it('should be possible to asynchronously register component extensions for the same extension point (same plugin)', async () => { - const reactiveRegistry = new AddedComponentsRegistry(); + const reactiveRegistry = await createRegistry(); const extensionPointId = 'grafana/alerting/home'; // Register extensions for the first extension point @@ -265,7 +234,7 @@ describe('AddedComponentsRegistry', () => { }); it('should be possible to register one extension component targeting multiple extension points', async () => { - const reactiveRegistry = new AddedComponentsRegistry(); + const reactiveRegistry = await createRegistry(); const extensionPointId1 = 'grafana/alerting/home'; const extensionPointId2 = 'grafana/user/profile/tab'; @@ -304,7 +273,7 @@ describe('AddedComponentsRegistry', () => { const pluginId2 = 'myorg-extensions-app'; const extensionPointId1 = 'grafana/alerting/home'; const extensionPointId2 = 'grafana/user/profile/tab'; - const reactiveRegistry = new AddedComponentsRegistry(); + const reactiveRegistry = await createRegistry(); const observable = reactiveRegistry.asObservable(); const subscribeCallback = jest.fn(); @@ -359,7 +328,7 @@ describe('AddedComponentsRegistry', () => { }); it('should not register component when title is missing', async () => { - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/alerting/home'; registry.register({ @@ -381,7 +350,7 @@ describe('AddedComponentsRegistry', () => { }); it('should not be possible to register a component on a read-only registry', async () => { - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const readOnlyRegistry = registry.readOnly(); const extensionPointId = 'grafana/alerting/home'; @@ -405,7 +374,7 @@ describe('AddedComponentsRegistry', () => { it('should pass down fresh registrations to the read-only version of the registry', async () => { const pluginId = 'grafana-basic-app'; - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const readOnlyRegistry = registry.readOnly(); const subscribeCallback = jest.fn(); let readOnlyState; @@ -441,7 +410,7 @@ describe('AddedComponentsRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const componentConfig = { title: 'Component title', description: 'Component description', @@ -449,9 +418,6 @@ describe('AddedComponentsRegistry', () => { component: () => React.createElement('div', null, 'Hello World1'), }; - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedComponents = []; - registry.register({ pluginId, configs: [componentConfig], @@ -467,7 +433,7 @@ describe('AddedComponentsRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const componentConfig = { title: 'Component title', description: 'Component description', @@ -490,7 +456,7 @@ describe('AddedComponentsRegistry', () => { // Production mode jest.mocked(isGrafanaDevMode).mockReturnValue(false); - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const componentConfig = { title: 'Component title', description: 'Component description', @@ -498,9 +464,6 @@ describe('AddedComponentsRegistry', () => { component: () => React.createElement('div', null, 'Hello World1'), }; - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedComponents = []; - registry.register({ pluginId, configs: [componentConfig], @@ -516,16 +479,18 @@ describe('AddedComponentsRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new AddedComponentsRegistry(); const componentConfig = { title: 'Component title', description: 'Component description', targets: ['grafana/alerting/home'], component: () => React.createElement('div', null, 'Hello World1'), }; - - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedComponents = [componentConfig]; + const { description, targets, title } = componentConfig; + const app = { + ...basicApp, + extensions: { ...basicApp.extensions, addedComponents: [{ description, targets, title }] }, + }; + const registry = await createRegistry([app]); registry.register({ pluginId, @@ -540,7 +505,7 @@ describe('AddedComponentsRegistry', () => { describe('asObservableSlice', () => { it('should return the selected slice from the registry', async () => { - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/alerting/home'; registry.register({ @@ -568,7 +533,7 @@ describe('AddedComponentsRegistry', () => { }); it('should return undefined when the selected key does not exist', async () => { - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const observable = registry.asObservableSlice((state) => state['non-existent-key']).pipe(take(1)); await expect(observable).toEmitValuesWith((received) => { @@ -578,7 +543,7 @@ describe('AddedComponentsRegistry', () => { }); it('should only emit when the selected slice changes', async () => { - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/alerting/home'; const subscribeCallback = jest.fn(); @@ -641,7 +606,7 @@ describe('AddedComponentsRegistry', () => { }); it('should deep freeze the selected slice', async () => { - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/alerting/home'; registry.register({ @@ -669,7 +634,7 @@ describe('AddedComponentsRegistry', () => { }); it('should work with read-only registries', async () => { - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const readOnlyRegistry = registry.readOnly(); const extensionPointId = 'grafana/alerting/home'; @@ -698,7 +663,7 @@ describe('AddedComponentsRegistry', () => { }); it('should emit immediately to new subscribers with the current slice value', async () => { - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/alerting/home'; registry.register({ @@ -725,7 +690,7 @@ describe('AddedComponentsRegistry', () => { }); it('should not emit when Object.is returns true for the same value', async () => { - const registry = new AddedComponentsRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/alerting/home'; const subscribeCallback = jest.fn(); diff --git a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts index b63f6405bc3..0e70ae5534c 100644 --- a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts +++ b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts @@ -1,6 +1,6 @@ import { ReplaySubject } from 'rxjs'; -import { PluginExtensionAddedComponentConfig } from '@grafana/data'; +import { AppPluginConfig, PluginExtensionAddedComponentConfig } from '@grafana/data'; import * as errors from '../errors'; import { isGrafanaDevMode, wrapWithPluginContext } from '../utils'; @@ -22,12 +22,13 @@ export class AddedComponentsRegistry extends Registry< PluginExtensionAddedComponentConfig > { constructor( + apps: AppPluginConfig[], options: { registrySubject?: ReplaySubject>; initialState?: RegistryType; } = {} ) { - super(options); + super(apps, options); } mapToRegistry( @@ -51,7 +52,7 @@ export class AddedComponentsRegistry extends Registry< if ( pluginId !== 'grafana' && isGrafanaDevMode() && - isAddedComponentMetaInfoMissing(pluginId, config, configLog) + isAddedComponentMetaInfoMissing(pluginId, config, configLog, this.apps) ) { continue; } @@ -85,7 +86,7 @@ export class AddedComponentsRegistry extends Registry< // Returns a read-only version of the registry. readOnly() { - return new AddedComponentsRegistry({ + return new AddedComponentsRegistry(this.apps, { registrySubject: this.registrySubject, }); } diff --git a/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts index 4c68a3a3eac..d976bcf3ad0 100644 --- a/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts @@ -1,10 +1,10 @@ import { firstValueFrom, take } from 'rxjs'; -import { PluginLoadingStrategy } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { AppPluginConfig } from '@grafana/data'; import { log } from '../logs/log'; import { resetLogMock } from '../logs/testUtils'; +import { basicApp } from '../test-fixtures/config.apps'; import { isGrafanaDevMode } from '../utils'; import { AddedFunctionsRegistry } from './AddedFunctionsRegistry'; @@ -29,55 +29,24 @@ jest.mock('../logs/log', () => { }); describe('addedFunctionsRegistry', () => { - const originalApps = config.apps; - const pluginId = 'grafana-basic-app'; - const appPluginConfig = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - extensions: { - addedFunctions: [], - addedLinks: [], - addedComponents: [], - exposedComponents: [], - extensionPoints: [], - }, - }; + const pluginId = basicApp.id; + const apps = [basicApp]; + const createRegistry = async (override: AppPluginConfig[] = apps) => new AddedFunctionsRegistry(override); beforeEach(() => { resetLogMock(log); jest.mocked(isGrafanaDevMode).mockReturnValue(false); - config.apps = { - [pluginId]: appPluginConfig, - }; - }); - - afterEach(() => { - config.apps = originalApps; }); it('should return empty registry when no extensions registered', async () => { - const addedFunctionsRegistry = new AddedFunctionsRegistry(); + const addedFunctionsRegistry = await createRegistry(); const observable = addedFunctionsRegistry.asObservable(); const registry = await firstValueFrom(observable); expect(registry).toEqual({}); }); it('should be possible to register function extensions in the registry', async () => { - const addedFunctionsRegistry = new AddedFunctionsRegistry(); + const addedFunctionsRegistry = await createRegistry(); addedFunctionsRegistry.register({ pluginId, @@ -122,7 +91,7 @@ describe('addedFunctionsRegistry', () => { }); it('should not emit when registering with empty configs', async () => { - const addedFunctionsRegistry = new AddedFunctionsRegistry(); + const addedFunctionsRegistry = await createRegistry(); const observable = addedFunctionsRegistry.asObservable(); const subscribeCallback = jest.fn(); @@ -147,7 +116,7 @@ describe('addedFunctionsRegistry', () => { }); it('should not change registry state when registering with empty configs after previous registrations', async () => { - const addedFunctionsRegistry = new AddedFunctionsRegistry(); + const addedFunctionsRegistry = await createRegistry(); // First register some extensions addedFunctionsRegistry.register({ @@ -178,7 +147,7 @@ describe('addedFunctionsRegistry', () => { it('should be possible to asynchronously register function extensions for the same placement (different plugins)', async () => { const pluginId1 = 'grafana-basic-app'; const pluginId2 = 'grafana-basic-app2'; - const reactiveRegistry = new AddedFunctionsRegistry(); + const reactiveRegistry = await createRegistry(); // Register extensions for the first plugin reactiveRegistry.register({ @@ -245,7 +214,7 @@ describe('addedFunctionsRegistry', () => { it('should be possible to asynchronously register function extensions for a different placement (different plugin)', async () => { const pluginId1 = 'grafana-basic-app'; const pluginId2 = 'grafana-basic-app2'; - const reactiveRegistry = new AddedFunctionsRegistry(); + const reactiveRegistry = await createRegistry(); // Register extensions for the first plugin reactiveRegistry.register({ @@ -314,7 +283,7 @@ describe('addedFunctionsRegistry', () => { it('should be possible to asynchronously register function extensions for the same placement (same plugin)', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedFunctionsRegistry(); + const reactiveRegistry = await createRegistry(); // Register extensions for the first extension point reactiveRegistry.register({ @@ -368,7 +337,7 @@ describe('addedFunctionsRegistry', () => { it('should be possible to asynchronously register function extensions for a different placement (same plugin)', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedFunctionsRegistry(); + const reactiveRegistry = await createRegistry(); // Register extensions for the first extension point reactiveRegistry.register({ @@ -424,7 +393,7 @@ describe('addedFunctionsRegistry', () => { it('should notify subscribers when the registry changes', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedFunctionsRegistry(); + const reactiveRegistry = await createRegistry(); const observable = reactiveRegistry.asObservable(); const subscribeCallback = jest.fn(); @@ -484,7 +453,7 @@ describe('addedFunctionsRegistry', () => { it('should give the last version of the registry for new subscribers', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedFunctionsRegistry(); + const reactiveRegistry = await createRegistry(); const observable = reactiveRegistry.asObservable(); const subscribeCallback = jest.fn(); @@ -518,9 +487,9 @@ describe('addedFunctionsRegistry', () => { }); }); - it('should not register a function extension if it has an invalid fn function', () => { + it('should not register a function extension if it has an invalid fn function', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedFunctionsRegistry(); + const reactiveRegistry = await createRegistry(); const observable = reactiveRegistry.asObservable(); const subscribeCallback = jest.fn(); @@ -546,9 +515,9 @@ describe('addedFunctionsRegistry', () => { expect(registry).toEqual({}); }); - it('should not register a function extension if it has invalid properties (empty title)', () => { + it('should not register a function extension if it has invalid properties (empty title)', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedFunctionsRegistry(); + const reactiveRegistry = await createRegistry(); const observable = reactiveRegistry.asObservable(); const subscribeCallback = jest.fn(); @@ -574,7 +543,7 @@ describe('addedFunctionsRegistry', () => { it('should not be possible to register a function on a read-only registry', async () => { const pluginId = 'grafana-basic-app'; - const registry = new AddedFunctionsRegistry(); + const registry = await createRegistry(); const readOnlyRegistry = registry.readOnly(); expect(() => { @@ -597,7 +566,7 @@ describe('addedFunctionsRegistry', () => { it('should pass down fresh registrations to the read-only version of the registry', async () => { const pluginId = 'grafana-basic-app'; - const registry = new AddedFunctionsRegistry(); + const registry = await createRegistry(); const readOnlyRegistry = registry.readOnly(); const subscribeCallback = jest.fn(); let readOnlyState; @@ -633,7 +602,7 @@ describe('addedFunctionsRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new AddedFunctionsRegistry(); + const registry = await createRegistry(); const fnConfig = { title: 'Function 1', description: 'Function 1 description', @@ -641,9 +610,6 @@ describe('addedFunctionsRegistry', () => { fn: jest.fn().mockReturnValue({}), }; - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedFunctions = []; - registry.register({ pluginId, configs: [fnConfig], @@ -659,7 +625,7 @@ describe('addedFunctionsRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new AddedFunctionsRegistry(); + const registry = await createRegistry(); const fnConfig = { title: 'Function 1', description: 'Function 1 description', @@ -682,7 +648,7 @@ describe('addedFunctionsRegistry', () => { // Production mode jest.mocked(isGrafanaDevMode).mockReturnValue(false); - const registry = new AddedFunctionsRegistry(); + const registry = await createRegistry(); const fnConfig = { title: 'Function 1', description: 'Function 1 description', @@ -690,9 +656,6 @@ describe('addedFunctionsRegistry', () => { fn: jest.fn().mockReturnValue({}), }; - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedFunctions = []; - registry.register({ pluginId, configs: [fnConfig], @@ -708,16 +671,18 @@ describe('addedFunctionsRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new AddedFunctionsRegistry(); const fnConfig = { title: 'Function 1', description: 'Function 1 description', targets: ['grafana/dashboard/panel/menu'], fn: jest.fn().mockReturnValue({}), }; - - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedFunctions = [fnConfig]; + const { description, targets, title } = fnConfig; + const app = { + ...basicApp, + extensions: { ...basicApp.extensions, addedFunctions: [{ description, targets, title }] }, + }; + const registry = await createRegistry([app]); registry.register({ pluginId, @@ -732,7 +697,7 @@ describe('addedFunctionsRegistry', () => { describe('asObservableSlice', () => { it('should return the selected slice from the registry', async () => { - const registry = new AddedFunctionsRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/dashboard/panel/menu'; registry.register({ @@ -760,7 +725,7 @@ describe('addedFunctionsRegistry', () => { }); it('should return undefined when the selected key does not exist', async () => { - const registry = new AddedFunctionsRegistry(); + const registry = await createRegistry(); const observable = registry.asObservableSlice((state) => state['non-existent-key']).pipe(take(1)); await expect(observable).toEmitValuesWith((received) => { @@ -770,7 +735,7 @@ describe('addedFunctionsRegistry', () => { }); it('should only emit when the selected slice changes (distinctUntilChanged)', async () => { - const registry = new AddedFunctionsRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/dashboard/panel/menu'; const subscribeCallback = jest.fn(); @@ -833,7 +798,7 @@ describe('addedFunctionsRegistry', () => { }); it('should deep freeze the selected slice', async () => { - const registry = new AddedFunctionsRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/dashboard/panel/menu'; registry.register({ @@ -861,7 +826,7 @@ describe('addedFunctionsRegistry', () => { }); it('should work with read-only registries', async () => { - const registry = new AddedFunctionsRegistry(); + const registry = await createRegistry(); const readOnlyRegistry = registry.readOnly(); const extensionPointId = 'grafana/dashboard/panel/menu'; @@ -890,7 +855,7 @@ describe('addedFunctionsRegistry', () => { }); it('should emit immediately to new subscribers with the current slice value', async () => { - const registry = new AddedFunctionsRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/dashboard/panel/menu'; registry.register({ diff --git a/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.ts b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.ts index a57d4a83d3e..7b80912fabb 100644 --- a/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.ts +++ b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.ts @@ -1,7 +1,7 @@ import { isFunction } from 'lodash'; import { ReplaySubject } from 'rxjs'; -import { PluginExtensionAddedFunctionConfig } from '@grafana/data'; +import { AppPluginConfig, PluginExtensionAddedFunctionConfig } from '@grafana/data'; import * as errors from '../errors'; import { isGrafanaDevMode } from '../utils'; @@ -20,12 +20,13 @@ export type AddedFunctionsRegistryItem = { export class AddedFunctionsRegistry extends Registry { constructor( + apps: AppPluginConfig[], options: { registrySubject?: ReplaySubject>; initialState?: RegistryType; } = {} ) { - super(options); + super(apps, options); } mapToRegistry( @@ -49,7 +50,11 @@ export class AddedFunctionsRegistry extends Registry { }); describe('AddedLinksRegistry', () => { - const originalApps = config.apps; - const pluginId = 'grafana-basic-app'; - const appPluginConfig = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - extensions: { - addedLinks: [], - addedComponents: [], - addedFunctions: [], - exposedComponents: [], - extensionPoints: [], - }, - }; + const pluginId = basicApp.id; + const apps = [basicApp]; + const createRegistry = async (override: AppPluginConfig[] = apps) => new AddedLinksRegistry(override); - beforeEach(() => { + beforeEach(async () => { resetLogMock(log); jest.mocked(isGrafanaDevMode).mockReturnValue(false); - config.apps = { - [pluginId]: appPluginConfig, - }; - }); - - afterEach(() => { - config.apps = originalApps; }); it('should return empty registry when no extensions registered', async () => { - const addedLinksRegistry = new AddedLinksRegistry(); + const addedLinksRegistry = await createRegistry(); const observable = addedLinksRegistry.asObservable(); const registry = await firstValueFrom(observable); expect(registry).toEqual({}); }); it('should be possible to register link extensions in the registry', async () => { - const addedLinksRegistry = new AddedLinksRegistry(); + const addedLinksRegistry = await createRegistry(); addedLinksRegistry.register({ pluginId, @@ -129,7 +98,7 @@ describe('AddedLinksRegistry', () => { it('should be possible to asynchronously register link extensions for the same placement (different plugins)', async () => { const pluginId1 = 'grafana-basic-app'; const pluginId2 = 'grafana-basic-app2'; - const reactiveRegistry = new AddedLinksRegistry(); + const reactiveRegistry = await createRegistry(); // Register extensions for the first plugin reactiveRegistry.register({ @@ -201,7 +170,7 @@ describe('AddedLinksRegistry', () => { it('should be possible to asynchronously register link extensions for a different placement (different plugin)', async () => { const pluginId1 = 'grafana-basic-app'; const pluginId2 = 'grafana-basic-app2'; - const reactiveRegistry = new AddedLinksRegistry(); + const reactiveRegistry = await createRegistry(); // Register extensions for the first plugin reactiveRegistry.register({ @@ -275,7 +244,7 @@ describe('AddedLinksRegistry', () => { it('should be possible to asynchronously register link extensions for the same placement (same plugin)', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedLinksRegistry(); + const reactiveRegistry = await createRegistry(); // Register extensions for the first extension point reactiveRegistry.register({ @@ -333,7 +302,7 @@ describe('AddedLinksRegistry', () => { it('should be possible to asynchronously register link extensions for a different placement (same plugin)', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedLinksRegistry(); + const reactiveRegistry = await createRegistry(); // Register extensions for the first extension point reactiveRegistry.register({ @@ -393,7 +362,7 @@ describe('AddedLinksRegistry', () => { it('should notify subscribers when the registry changes', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedLinksRegistry(); + const reactiveRegistry = await createRegistry(); const observable = reactiveRegistry.asObservable(); const subscribeCallback = jest.fn(); @@ -459,7 +428,7 @@ describe('AddedLinksRegistry', () => { it('should give the last version of the registry for new subscribers', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedLinksRegistry(); + const reactiveRegistry = await createRegistry(); const observable = reactiveRegistry.asObservable(); const subscribeCallback = jest.fn(); @@ -496,9 +465,9 @@ describe('AddedLinksRegistry', () => { }); }); - it('should not register a link extension if it has an invalid configure() function', () => { + it('should not register a link extension if it has an invalid configure() function', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedLinksRegistry(); + const reactiveRegistry = await createRegistry(); const observable = reactiveRegistry.asObservable(); const subscribeCallback = jest.fn(); @@ -525,9 +494,9 @@ describe('AddedLinksRegistry', () => { expect(registry).toEqual({}); }); - it('should not register a link extension if it has invalid properties (empty title / description)', () => { + it('should not register a link extension if it has invalid properties (empty title / description)', async () => { const pluginId = 'grafana-basic-app'; - const reactiveRegistry = new AddedLinksRegistry(); + const reactiveRegistry = await createRegistry(); const observable = reactiveRegistry.asObservable(); const subscribeCallback = jest.fn(); @@ -555,7 +524,7 @@ describe('AddedLinksRegistry', () => { it('should not be possible to register a link on a read-only registry', async () => { const pluginId = 'grafana-basic-app'; - const registry = new AddedLinksRegistry(); + const registry = await createRegistry(); const readOnlyRegistry = registry.readOnly(); expect(() => { @@ -579,7 +548,7 @@ describe('AddedLinksRegistry', () => { it('should pass down fresh registrations to the read-only version of the registry', async () => { const pluginId = 'grafana-basic-app'; - const registry = new AddedLinksRegistry(); + const registry = await createRegistry(); const readOnlyRegistry = registry.readOnly(); const subscribeCallback = jest.fn(); let readOnlyState; @@ -616,7 +585,7 @@ describe('AddedLinksRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new AddedLinksRegistry(); + const registry = await createRegistry(); const linkConfig = { title: 'Link 1', description: 'Link 1 description', @@ -625,9 +594,6 @@ describe('AddedLinksRegistry', () => { configure: jest.fn().mockReturnValue({}), }; - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedLinks = []; - registry.register({ pluginId, configs: [linkConfig], @@ -643,7 +609,7 @@ describe('AddedLinksRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new AddedLinksRegistry(); + const registry = await createRegistry(); const linkConfig = { title: 'Link 1', description: 'Link 1 description', @@ -667,7 +633,7 @@ describe('AddedLinksRegistry', () => { // Production mode jest.mocked(isGrafanaDevMode).mockReturnValue(false); - const registry = new AddedLinksRegistry(); + const registry = await createRegistry(); const linkConfig = { title: 'Link 1', description: 'Link 1 description', @@ -676,9 +642,6 @@ describe('AddedLinksRegistry', () => { configure: jest.fn().mockReturnValue({}), }; - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedLinks = []; - registry.register({ pluginId, configs: [linkConfig], @@ -694,7 +657,6 @@ describe('AddedLinksRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new AddedLinksRegistry(); const linkConfig = { title: 'Link 1', description: 'Link 1 description', @@ -702,9 +664,12 @@ describe('AddedLinksRegistry', () => { targets: ['grafana/dashboard/panel/menu'], configure: jest.fn().mockReturnValue({}), }; - - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedLinks = [linkConfig]; + const { description, targets, title } = linkConfig; + const app = { + ...basicApp, + extensions: { ...basicApp.extensions, addedLinks: [{ description, targets, title }] }, + }; + const registry = await createRegistry([app]); registry.register({ pluginId, @@ -719,7 +684,7 @@ describe('AddedLinksRegistry', () => { describe('asObservableSlice', () => { it('should return the selected slice from the registry', async () => { - const registry = new AddedLinksRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/dashboard/panel/menu'; registry.register({ @@ -748,7 +713,7 @@ describe('AddedLinksRegistry', () => { }); it('should return undefined when the selected key does not exist', async () => { - const registry = new AddedLinksRegistry(); + const registry = await createRegistry(); const observable = registry.asObservableSlice((state) => state['non-existent-key']).pipe(take(1)); await expect(observable).toEmitValuesWith((received) => { @@ -758,7 +723,7 @@ describe('AddedLinksRegistry', () => { }); it('should only emit when the selected slice changes (distinctUntilChanged)', async () => { - const registry = new AddedLinksRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/dashboard/panel/menu'; const subscribeCallback = jest.fn(); @@ -824,7 +789,7 @@ describe('AddedLinksRegistry', () => { }); it('should deep freeze the selected slice', async () => { - const registry = new AddedLinksRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/dashboard/panel/menu'; registry.register({ @@ -853,7 +818,7 @@ describe('AddedLinksRegistry', () => { }); it('should work with read-only registries', async () => { - const registry = new AddedLinksRegistry(); + const registry = await createRegistry(); const readOnlyRegistry = registry.readOnly(); const extensionPointId = 'grafana/dashboard/panel/menu'; @@ -883,7 +848,7 @@ describe('AddedLinksRegistry', () => { }); it('should emit immediately to new subscribers with the current slice value', async () => { - const registry = new AddedLinksRegistry(); + const registry = await createRegistry(); const extensionPointId = 'grafana/dashboard/panel/menu'; registry.register({ diff --git a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts index c3dd9faf6f1..d538bcc8552 100644 --- a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts +++ b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts @@ -1,6 +1,6 @@ import { ReplaySubject } from 'rxjs'; -import { IconName, PluginExtensionAddedLinkConfig } from '@grafana/data'; +import { AppPluginConfig, IconName, PluginExtensionAddedLinkConfig } from '@grafana/data'; import { PluginAddedLinksConfigureFunc, PluginExtensionEventHelpers } from '@grafana/data/internal'; import * as errors from '../errors'; @@ -26,12 +26,13 @@ export type AddedLinkRegistryItem = { export class AddedLinksRegistry extends Registry { constructor( + apps: AppPluginConfig[], options: { registrySubject?: ReplaySubject>; initialState?: RegistryType; } = {} ) { - super(options); + super(apps, options); } mapToRegistry( @@ -66,7 +67,11 @@ export class AddedLinksRegistry extends Registry { }); describe('ExposedComponentsRegistry', () => { - const originalApps = config.apps; - const pluginId = 'grafana-basic-app'; - const appPluginConfig = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - extensions: { - addedLinks: [], - addedComponents: [], - addedFunctions: [], - exposedComponents: [], - extensionPoints: [], - }, - }; + const pluginId = basicApp.id; + const apps = [basicApp]; + const createRegistry = async (override: AppPluginConfig[] = apps) => new ExposedComponentsRegistry(override); beforeEach(() => { resetLogMock(log); jest.mocked(isGrafanaDevMode).mockReturnValue(false); - config.apps = { - [pluginId]: appPluginConfig, - }; - }); - - afterEach(() => { - config.apps = originalApps; }); it('should return empty registry when no exposed components have been registered', async () => { - const reactiveRegistry = new ExposedComponentsRegistry(); + const reactiveRegistry = await createRegistry(); const observable = reactiveRegistry.asObservable(); const registry = await firstValueFrom(observable); expect(registry).toEqual({}); @@ -80,7 +49,7 @@ describe('ExposedComponentsRegistry', () => { it('should be possible to register exposed components in the registry', async () => { const pluginId = 'grafana-basic-app'; const id = `${pluginId}/hello-world/v1`; - const reactiveRegistry = new ExposedComponentsRegistry(); + const reactiveRegistry = await createRegistry(); reactiveRegistry.register({ pluginId, @@ -110,7 +79,7 @@ describe('ExposedComponentsRegistry', () => { const id1 = `${pluginId}/hello-world1/v1`; const id2 = `${pluginId}/hello-world2/v1`; const id3 = `${pluginId}/hello-world3/v1`; - const reactiveRegistry = new ExposedComponentsRegistry(); + const reactiveRegistry = await createRegistry(); reactiveRegistry.register({ pluginId, @@ -151,7 +120,7 @@ describe('ExposedComponentsRegistry', () => { const id2 = `${pluginId1}/hello-world2/v1`; const id3 = `${pluginId2}/hello-world1/v1`; const id4 = `${pluginId2}/hello-world2/v1`; - const reactiveRegistry = new ExposedComponentsRegistry(); + const reactiveRegistry = await createRegistry(); reactiveRegistry.register({ pluginId: pluginId1, @@ -199,7 +168,7 @@ describe('ExposedComponentsRegistry', () => { }); it('should notify subscribers when the registry changes', async () => { - const registry = new ExposedComponentsRegistry(); + const registry = await createRegistry(); const observable = registry.asObservable(); const subscribeCallback = jest.fn(); @@ -241,7 +210,7 @@ describe('ExposedComponentsRegistry', () => { }); it('should give the last version of the registry for new subscribers', async () => { - const registry = new ExposedComponentsRegistry(); + const registry = await createRegistry(); const observable = registry.asObservable(); const subscribeCallback = jest.fn(); @@ -272,7 +241,7 @@ describe('ExposedComponentsRegistry', () => { }); it('should log an error if another component with the same id already exists in the registry', async () => { - const registry = new ExposedComponentsRegistry(); + const registry = await createRegistry(); registry.register({ pluginId: 'grafana-basic-app1', configs: [ @@ -312,7 +281,7 @@ describe('ExposedComponentsRegistry', () => { }); it('should skip registering component and log an error when id is not prefixed with plugin id', async () => { - const registry = new ExposedComponentsRegistry(); + const registry = await createRegistry(); registry.register({ pluginId: 'grafana-basic-app1', configs: [ @@ -333,7 +302,7 @@ describe('ExposedComponentsRegistry', () => { }); it('should not register component when title is missing', async () => { - const registry = new ExposedComponentsRegistry(); + const registry = await createRegistry(); registry.register({ pluginId: 'grafana-basic-app', @@ -355,7 +324,7 @@ describe('ExposedComponentsRegistry', () => { it('should not be possible to register a component on a read-only registry', async () => { const pluginId = 'grafana-basic-app'; - const registry = new ExposedComponentsRegistry(); + const registry = await createRegistry(); const readOnlyRegistry = registry.readOnly(); expect(() => { @@ -378,7 +347,7 @@ describe('ExposedComponentsRegistry', () => { it('should pass down fresh registrations to the read-only version of the registry', async () => { const pluginId = 'grafana-basic-app'; - const registry = new ExposedComponentsRegistry(); + const registry = await createRegistry(); const readOnlyRegistry = registry.readOnly(); const subscribeCallback = jest.fn(); let readOnlyState; @@ -414,7 +383,6 @@ describe('ExposedComponentsRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new ExposedComponentsRegistry(); const componentConfig = { id: `${pluginId}/exposed-component/v1`, title: 'Component title', @@ -422,9 +390,7 @@ describe('ExposedComponentsRegistry', () => { component: () => React.createElement('div', null, 'Hello World1'), }; - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.exposedComponents = []; - + const registry = await createRegistry(); registry.register({ pluginId, configs: [componentConfig], @@ -440,7 +406,6 @@ describe('ExposedComponentsRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new ExposedComponentsRegistry(); const componentConfig = { id: `${pluginId}/exposed-component/v1`, title: 'Component title', @@ -448,6 +413,7 @@ describe('ExposedComponentsRegistry', () => { component: () => React.createElement('div', null, 'Hello World1'), }; + const registry = await createRegistry(); registry.register({ pluginId: 'grafana', configs: [componentConfig], @@ -463,7 +429,6 @@ describe('ExposedComponentsRegistry', () => { // Production mode jest.mocked(isGrafanaDevMode).mockReturnValue(false); - const registry = new ExposedComponentsRegistry(); const componentConfig = { id: `${pluginId}/exposed-component/v1`, title: 'Component title', @@ -471,9 +436,7 @@ describe('ExposedComponentsRegistry', () => { component: () => React.createElement('div', null, 'Hello World1'), }; - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.exposedComponents = []; - + const registry = await createRegistry(); registry.register({ pluginId, configs: [componentConfig], @@ -489,17 +452,19 @@ describe('ExposedComponentsRegistry', () => { // Enabling dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); - const registry = new ExposedComponentsRegistry(); const componentConfig = { id: `${pluginId}/exposed-component/v1`, title: 'Component title', description: 'Component description', component: () => React.createElement('div', null, 'Hello World1'), }; + const { description, id, title } = componentConfig; + const app = { + ...basicApp, + extensions: { ...basicApp.extensions, exposedComponents: [{ description, id, title }] }, + }; - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.exposedComponents = [componentConfig]; - + const registry = await createRegistry([app]); registry.register({ pluginId, configs: [componentConfig], @@ -513,7 +478,7 @@ describe('ExposedComponentsRegistry', () => { describe('asObservableSlice', () => { it('should return the selected exposed component from the registry', async () => { - const registry = new ExposedComponentsRegistry(); + const registry = await createRegistry(); const componentId = 'test-plugin/exposed-component/v1'; registry.register({ @@ -540,7 +505,7 @@ describe('ExposedComponentsRegistry', () => { }); it('should deep freeze exposed components', async () => { - const registry = new ExposedComponentsRegistry(); + const registry = await createRegistry(); const componentId = 'test-plugin/exposed-component/v1'; registry.register({ @@ -566,7 +531,7 @@ describe('ExposedComponentsRegistry', () => { }); it('should only emit when the selected exposed component changes', async () => { - const registry = new ExposedComponentsRegistry(); + const registry = await createRegistry(); const componentId1 = 'test-plugin/component1/v1'; const componentId2 = 'test-plugin/component2/v1'; const subscribeCallback = jest.fn(); diff --git a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts index 9c13260518a..32f63910da7 100644 --- a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts +++ b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts @@ -1,6 +1,6 @@ import { ReplaySubject } from 'rxjs'; -import { PluginExtensionExposedComponentConfig } from '@grafana/data'; +import { AppPluginConfig, PluginExtensionExposedComponentConfig } from '@grafana/data'; import * as errors from '../errors'; import { isGrafanaDevMode } from '../utils'; @@ -22,12 +22,13 @@ export class ExposedComponentsRegistry extends Registry< PluginExtensionExposedComponentConfig > { constructor( + apps: AppPluginConfig[], options: { registrySubject?: ReplaySubject>; initialState?: RegistryType; } = {} ) { - super(options); + super(apps, options); } mapToRegistry( @@ -65,7 +66,7 @@ export class ExposedComponentsRegistry extends Registry< if ( pluginId !== 'grafana' && isGrafanaDevMode() && - isExposedComponentMetaInfoMissing(pluginId, config, pointIdLog) + isExposedComponentMetaInfoMissing(pluginId, config, pointIdLog, this.apps) ) { continue; } @@ -80,7 +81,7 @@ export class ExposedComponentsRegistry extends Registry< // Returns a read-only version of the registry. readOnly() { - return new ExposedComponentsRegistry({ + return new ExposedComponentsRegistry(this.apps, { registrySubject: this.registrySubject, }); } diff --git a/public/app/features/plugins/extensions/registry/Registry.test.ts b/public/app/features/plugins/extensions/registry/Registry.test.ts index 34e84b36886..b9a04dd72bf 100644 --- a/public/app/features/plugins/extensions/registry/Registry.test.ts +++ b/public/app/features/plugins/extensions/registry/Registry.test.ts @@ -5,7 +5,7 @@ import { AddedComponentsRegistry } from './AddedComponentsRegistry'; describe('Registry.asObservableSlice ', () => { describe('Shared behaviour', () => { it('should handle selecting a non-existent key that later gets added', async () => { - const registry = new AddedComponentsRegistry(); + const registry = new AddedComponentsRegistry([]); const extensionPointId = 'grafana/alerting/home'; const subscribeCallback = jest.fn(); @@ -35,7 +35,7 @@ describe('Registry.asObservableSlice ', () => { }); it('should handle multiple subscribers independently', async () => { - const registry = new AddedComponentsRegistry(); + const registry = new AddedComponentsRegistry([]); const extensionPointId1 = 'grafana/alerting/home'; const extensionPointId2 = 'grafana/other/point'; const callback1 = jest.fn(); diff --git a/public/app/features/plugins/extensions/registry/Registry.ts b/public/app/features/plugins/extensions/registry/Registry.ts index d37e5276bc6..fe881ae1f89 100644 --- a/public/app/features/plugins/extensions/registry/Registry.ts +++ b/public/app/features/plugins/extensions/registry/Registry.ts @@ -1,5 +1,7 @@ import { Observable, ReplaySubject, Subject, distinctUntilChanged, firstValueFrom, map, scan, startWith } from 'rxjs'; +import { AppPluginConfig } from '@grafana/data'; + import { ExtensionsLog, log } from '../logs/log'; import { deepFreeze } from '../utils'; @@ -25,11 +27,14 @@ export abstract class Registry>; - constructor(options: { - registrySubject?: ReplaySubject>; - initialState?: RegistryType; - log?: ExtensionsLog; - }) { + constructor( + protected apps: AppPluginConfig[], + options: { + registrySubject?: ReplaySubject>; + initialState?: RegistryType; + log?: ExtensionsLog; + } + ) { this.resultSubject = new Subject>(); this.logger = options.log ?? log; this.isReadOnly = false; diff --git a/public/app/features/plugins/extensions/registry/setup.test.ts b/public/app/features/plugins/extensions/registry/setup.test.ts new file mode 100644 index 00000000000..46b85304b11 --- /dev/null +++ b/public/app/features/plugins/extensions/registry/setup.test.ts @@ -0,0 +1,72 @@ +import { MonitoringLogger } from '@grafana/runtime'; +import { getAppPluginMetas, invalidateCache, setLogger } from '@grafana/runtime/internal'; + +import { getPluginExtensionRegistries } from './setup'; + +jest.mock('@grafana/runtime/internal', () => ({ + ...jest.requireActual('@grafana/runtime/internal'), + getAppPluginMetas: jest.fn(), +})); + +const getAppPluginMetasMock = jest.mocked(getAppPluginMetas); +let logger: MonitoringLogger; + +describe('getPluginExtensionRegistries', () => { + beforeEach(() => { + jest.resetAllMocks(); + invalidateCache(); + getAppPluginMetasMock.mockResolvedValue([]); + logger = { + logDebug: jest.fn(), + logError: jest.fn(), + logInfo: jest.fn(), + logMeasurement: jest.fn(), + logWarning: jest.fn(), + }; + setLogger(logger); + }); + + test('should only call getAppPluginMetas once', async () => { + const promise1 = getPluginExtensionRegistries(); + const promise2 = getPluginExtensionRegistries(); + + await Promise.all([promise1, promise2]); + + expect(getAppPluginMetasMock).toHaveBeenCalledTimes(1); + }); + + test('should return the same promise instance for concurrent calls', async () => { + const promise1 = getPluginExtensionRegistries(); + const promise2 = getPluginExtensionRegistries(); + const promise3 = getPluginExtensionRegistries(); + + expect(promise1).toStrictEqual(promise2); + expect(promise2).toStrictEqual(promise3); + expect(promise3).toStrictEqual(promise1); + + const [first, second, third] = await Promise.all([promise1, promise2, promise3]); + + expect(first).toBe(second); + expect(second).toBe(third); + expect(third).toBe(first); + }); + + test('should not cache promise if getAppPluginMetas throws', async () => { + getAppPluginMetasMock.mockRejectedValueOnce(new Error('Some error')); + + const first = await getPluginExtensionRegistries(); + const second = await getPluginExtensionRegistries(); + const third = await getPluginExtensionRegistries(); + + expect(getAppPluginMetasMock).toHaveBeenCalledTimes(2); // first + second (because first throws), third is cached + expect(first).not.toBe(second); + expect(first).not.toBe(third); + expect(second).toBe(third); + expect(logger.logError).toHaveBeenCalledTimes(1); + expect(logger.logError).toHaveBeenCalledWith(new Error('Something failed while resolving a cached promise'), { + message: 'Some error', + stack: expect.any(String), + key: 'initPluginExtensionRegistries', + }); + }); +}); diff --git a/public/app/features/plugins/extensions/registry/setup.ts b/public/app/features/plugins/extensions/registry/setup.ts index dff6ef976bc..8e2778359b4 100644 --- a/public/app/features/plugins/extensions/registry/setup.ts +++ b/public/app/features/plugins/extensions/registry/setup.ts @@ -1,4 +1,6 @@ -import { PluginExtensionExposedComponents } from '@grafana/data'; +/* eslint-disable @grafana/i18n/no-untranslated-strings */ +import { AppPluginConfig, PluginExtensionExposedComponents } from '@grafana/data'; +import { getAppPluginMetas, getCachedPromise } from '@grafana/runtime/internal'; import CentralAlertHistorySceneExposedComponent from 'app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistorySceneExposedComponent'; import { CreateAlertFromPanelExposedComponent } from 'app/features/alerting/unified/extensions/CreateAlertFromPanelExposedComponent'; import { AddToDashboardFormExposedComponent } from 'app/features/dashboard-scene/addToDashboard/AddToDashboardFormExposedComponent'; @@ -12,50 +14,69 @@ import { AddedLinksRegistry } from './AddedLinksRegistry'; import { ExposedComponentsRegistry } from './ExposedComponentsRegistry'; import { PluginExtensionRegistries } from './types'; -export const addedComponentsRegistry = new AddedComponentsRegistry(); -export const exposedComponentsRegistry = new ExposedComponentsRegistry(); -export const addedLinksRegistry = new AddedLinksRegistry(); -export const addedFunctionsRegistry = new AddedFunctionsRegistry(); -export const pluginExtensionRegistries: PluginExtensionRegistries = { - addedComponentsRegistry, - exposedComponentsRegistry, - addedLinksRegistry, - addedFunctionsRegistry, -}; +function initRegistries(apps: AppPluginConfig[]): PluginExtensionRegistries { + const addedComponentsRegistry = new AddedComponentsRegistry(apps); + const exposedComponentsRegistry = new ExposedComponentsRegistry(apps); + const addedLinksRegistry = new AddedLinksRegistry(apps); + const addedFunctionsRegistry = new AddedFunctionsRegistry(apps); + return { addedComponentsRegistry, addedFunctionsRegistry, addedLinksRegistry, exposedComponentsRegistry }; +} -// Registering core extension links -addedLinksRegistry.register({ - pluginId: 'grafana', - configs: getCoreExtensionConfigurations(), -}); +function registerCoreExtensions({ addedLinksRegistry, exposedComponentsRegistry }: PluginExtensionRegistries) { + // Registering core extension links + addedLinksRegistry.register({ + pluginId: 'grafana', + configs: getCoreExtensionConfigurations(), + }); -// Registering core exposed components -exposedComponentsRegistry.register({ - pluginId: 'grafana', - configs: [ - { - id: PluginExtensionExposedComponents.CentralAlertHistorySceneV1, - title: 'Central alert history scene', - description: 'Central alert history scene', - component: CentralAlertHistorySceneExposedComponent, - }, - { - id: PluginExtensionExposedComponents.AddToDashboardFormV1, - title: 'Add to dashboard form', - description: 'Add to dashboard form', - component: AddToDashboardFormExposedComponent, - }, - { - id: PluginExtensionExposedComponents.CreateAlertFromPanelV1, - title: 'Create alert from panel', - description: 'Modal to create an alert rule from panel data', - component: CreateAlertFromPanelExposedComponent, - }, - { - id: PluginExtensionExposedComponents.OpenQueryLibraryV1, - title: 'Access to the Query Library', - description: 'Access to the Query Library', - component: OpenQueryLibraryExposedComponent, - }, - ], -}); + // Registering core exposed components + exposedComponentsRegistry.register({ + pluginId: 'grafana', + configs: [ + { + id: PluginExtensionExposedComponents.CentralAlertHistorySceneV1, + title: 'Central alert history scene', + description: 'Central alert history scene', + component: CentralAlertHistorySceneExposedComponent, + }, + { + id: PluginExtensionExposedComponents.AddToDashboardFormV1, + title: 'Add to dashboard form', + description: 'Add to dashboard form', + component: AddToDashboardFormExposedComponent, + }, + { + id: PluginExtensionExposedComponents.CreateAlertFromPanelV1, + title: 'Create alert from panel', + description: 'Modal to create an alert rule from panel data', + component: CreateAlertFromPanelExposedComponent, + }, + { + id: PluginExtensionExposedComponents.OpenQueryLibraryV1, + title: 'Access to the Query Library', + description: 'Access to the Query Library', + component: OpenQueryLibraryExposedComponent, + }, + ], + }); +} + +async function initPluginExtensionRegistries(): Promise { + const apps = await getAppPluginMetas(); + const registries = initRegistries(apps); + registerCoreExtensions(registries); + + return registries; +} + +/** + * Gets the plugin extension registries, initializing them on first call. + * This function is safe to call concurrently - multiple simultaneous calls will + * all receive the same Promise instance, ensuring only one initialization. + * If initialization (including getAppPluginMetas) fails, the error is logged and + * empty plugin extension registries are returned as a fallback. + * @returns Promise resolving to the plugin extension registries + */ +export async function getPluginExtensionRegistries(): Promise { + return getCachedPromise(initPluginExtensionRegistries, { defaultValue: initRegistries([]) }); +} diff --git a/public/app/features/plugins/extensions/registry/useRegistrySlice.test.tsx b/public/app/features/plugins/extensions/registry/useRegistrySlice.test.tsx index 3985345faa0..74f93a5b206 100644 --- a/public/app/features/plugins/extensions/registry/useRegistrySlice.test.tsx +++ b/public/app/features/plugins/extensions/registry/useRegistrySlice.test.tsx @@ -21,10 +21,10 @@ describe('useRegistrySlice', () => { beforeEach(() => { registries = { - addedComponentsRegistry: new AddedComponentsRegistry(), - exposedComponentsRegistry: new ExposedComponentsRegistry(), - addedLinksRegistry: new AddedLinksRegistry(), - addedFunctionsRegistry: new AddedFunctionsRegistry(), + addedComponentsRegistry: new AddedComponentsRegistry([]), + exposedComponentsRegistry: new ExposedComponentsRegistry([]), + addedLinksRegistry: new AddedLinksRegistry([]), + addedFunctionsRegistry: new AddedFunctionsRegistry([]), }; wrapper = ({ children }: { children: React.ReactNode }) => ( diff --git a/public/app/features/plugins/extensions/test-fixtures/config.apps.ts b/public/app/features/plugins/extensions/test-fixtures/config.apps.ts index 51448ca7f2e..f18ee84fedf 100644 --- a/public/app/features/plugins/extensions/test-fixtures/config.apps.ts +++ b/public/app/features/plugins/extensions/test-fixtures/config.apps.ts @@ -1,9 +1,7 @@ -import { cloneDeep } from 'lodash'; - import { AngularMeta, AppPluginConfig, PluginLoadingStrategy } from '@grafana/data'; import { AppPluginMetas } from '@grafana/runtime/internal'; -const app: AppPluginConfig = cloneDeep({ +const app: AppPluginConfig = structuredClone({ id: 'myorg-someplugin-app', path: 'public/plugins/myorg-someplugin-app/module.js', version: '1.0.0', @@ -29,7 +27,7 @@ const app: AppPluginConfig = cloneDeep({ buildMode: 'production', }); -export const metas: AppPluginMetas = cloneDeep({ +export const metas: AppPluginMetas = structuredClone({ 'grafana-exploretraces-app': { id: 'grafana-exploretraces-app', path: 'public/plugins/grafana-exploretraces-app/module.js', @@ -483,7 +481,7 @@ export const metas: AppPluginMetas = cloneDeep({ export const apps = Object.values(metas); -export const genericAppPluginConfig: Omit = { +export const genericAppPluginConfig: Omit = structuredClone({ path: '', version: '', preload: false, @@ -506,4 +504,9 @@ export const genericAppPluginConfig: Omit = { exposedComponents: [], extensionPoints: [], }, -}; +}); + +export const basicApp: AppPluginConfig = structuredClone({ + ...genericAppPluginConfig, + id: 'grafana-basic-app', +}); diff --git a/public/app/features/plugins/extensions/usePluginComponent.test.tsx b/public/app/features/plugins/extensions/usePluginComponent.test.tsx index 44f35114b96..cbdb3ea5c79 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.test.tsx @@ -1,7 +1,7 @@ import { act, render, renderHook, screen, waitFor } from '@testing-library/react'; import type { JSX } from 'react'; -import { PluginContextProvider, PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data'; +import { AppPluginConfig, PluginContextProvider, PluginMeta, PluginType } from '@grafana/data'; import { config } from '@grafana/runtime'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; @@ -12,6 +12,7 @@ import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry'; import { AddedLinksRegistry } from './registry/AddedLinksRegistry'; import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry'; import { PluginExtensionRegistries } from './registry/types'; +import { basicApp } from './test-fixtures/config.apps'; import { useLoadAppPlugins } from './useLoadAppPlugins'; import { usePluginComponent } from './usePluginComponent'; import { isGrafanaDevMode } from './utils'; @@ -51,8 +52,7 @@ describe('usePluginComponent()', () => { let registries: PluginExtensionRegistries; let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; let pluginMeta: PluginMeta; - const originalApps = config.apps; - const pluginId = 'myorg-extensions-app'; + const pluginId = basicApp.id; const exposedComponentId = `${pluginId}/exposed-component/v1`; const exposedComponentConfig = { id: exposedComponentId, @@ -61,39 +61,23 @@ describe('usePluginComponent()', () => { component: () =>
Hello World
, }; const appPluginConfig = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, + ...basicApp, extensions: { - addedLinks: [], - addedComponents: [], - addedFunctions: [], + ...basicApp.extensions, // This is necessary, so we can register exposed components to the registry during the tests // (Otherwise the registry would reject it in the imitated production mode) exposedComponents: [exposedComponentConfig], - extensionPoints: [], }, }; + let apps: AppPluginConfig[]; beforeEach(() => { + apps = [appPluginConfig]; registries = { - addedComponentsRegistry: new AddedComponentsRegistry(), - exposedComponentsRegistry: new ExposedComponentsRegistry(), - addedLinksRegistry: new AddedLinksRegistry(), - addedFunctionsRegistry: new AddedFunctionsRegistry(), + addedComponentsRegistry: new AddedComponentsRegistry(apps), + exposedComponentsRegistry: new ExposedComponentsRegistry(apps), + addedLinksRegistry: new AddedLinksRegistry(apps), + addedFunctionsRegistry: new AddedFunctionsRegistry(apps), }; jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false }); jest.mocked(isGrafanaDevMode).mockReturnValue(false); @@ -135,19 +119,11 @@ describe('usePluginComponent()', () => { }, }; - config.apps = { - [pluginId]: appPluginConfig, - }; - wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); }); - afterEach(() => { - config.apps = originalApps; - }); - it('should return null if there are no component exposed for the id', () => { const { result } = renderHook(() => usePluginComponent('foo/bar'), { wrapper }); @@ -383,7 +359,7 @@ describe('usePluginComponent()', () => { // Should log an error in dev mode expect(log.error).toHaveBeenCalledWith( - 'Attempted to mutate object property "c" from extension with id myorg-extensions-app and version unknown', + 'Attempted to mutate object property "c" from extension with id grafana-basic-app and version unknown', { stack: expect.any(String), } @@ -439,7 +415,7 @@ describe('usePluginComponent()', () => { // Should log a warning expect(log.warning).toHaveBeenCalledWith( - 'Attempted to mutate object property "c" from extension with id myorg-extensions-app and version unknown', + 'Attempted to mutate object property "c" from extension with id grafana-basic-app and version unknown', { stack: expect.any(String), } diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index f9c41cdc9b4..156417b1a5c 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -1,13 +1,7 @@ import { act, render, renderHook, screen } from '@testing-library/react'; import React, { type JSX } from 'react'; -import { - PluginContextProvider, - PluginExtensionPoints, - PluginLoadingStrategy, - PluginMeta, - PluginType, -} from '@grafana/data'; +import { AppPluginConfig, PluginContextProvider, PluginExtensionPoints, PluginMeta, PluginType } from '@grafana/data'; import { config } from '@grafana/runtime'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; @@ -19,6 +13,7 @@ import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry'; import { AddedLinksRegistry } from './registry/AddedLinksRegistry'; import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry'; import { PluginExtensionRegistries } from './registry/types'; +import { basicApp } from './test-fixtures/config.apps'; import { useLoadAppPlugins } from './useLoadAppPlugins'; import { usePluginComponents } from './usePluginComponents'; import { isGrafanaDevMode } from './utils'; @@ -62,9 +57,10 @@ describe('usePluginComponents()', () => { let registries: PluginExtensionRegistries; let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; let pluginMeta: PluginMeta; - const pluginId = 'myorg-extensions-app'; + const pluginId = basicApp.id; const extensionPointId = `${pluginId}/extension-point/v1`; const originalBuildInfoEnv = config.buildInfo.env; + let apps: AppPluginConfig[]; beforeEach(() => { config.buildInfo.env = originalBuildInfoEnv; @@ -72,11 +68,12 @@ describe('usePluginComponents()', () => { jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false }); resetLogMock(log); + apps = [basicApp]; registries = { - addedComponentsRegistry: new AddedComponentsRegistry(), - exposedComponentsRegistry: new ExposedComponentsRegistry(), - addedLinksRegistry: new AddedLinksRegistry(), - addedFunctionsRegistry: new AddedFunctionsRegistry(), + addedComponentsRegistry: new AddedComponentsRegistry(apps), + exposedComponentsRegistry: new ExposedComponentsRegistry(apps), + addedLinksRegistry: new AddedLinksRegistry(apps), + addedFunctionsRegistry: new AddedFunctionsRegistry(apps), }; pluginMeta = { @@ -115,32 +112,6 @@ describe('usePluginComponents()', () => { }, }; - config.apps[pluginId] = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - extensions: { - addedLinks: [], - addedComponents: [], - addedFunctions: [], - exposedComponents: [], - extensionPoints: [], - }, - }; - wrapper = ({ children }: { children: React.ReactNode }) => ( {children} @@ -235,14 +206,14 @@ describe('usePluginComponents()', () => { pluginId, title: '1', description: '1', - id: '-1921123020', + id: '1982424218', type: 'component', }); expect(result.current.components[1].meta).toEqual({ pluginId, title: '2', description: '2', - id: '-1921123019', + id: '1982424219', type: 'component', }); }); @@ -303,7 +274,7 @@ describe('usePluginComponents()', () => { // Should also render the component if it wants to change the props expect(() => render()).not.toThrow(); expect(log.error).toHaveBeenCalledWith( - `Attempted to mutate object property "foo4" from extension with id myorg-extensions-app and version unknown`, + `Attempted to mutate object property "foo4" from extension with id grafana-basic-app and version unknown`, { stack: expect.any(String), } @@ -367,7 +338,7 @@ describe('usePluginComponents()', () => { // Should also render the component if it wants to change the props expect(() => render()).not.toThrow(); expect(log.warning).toHaveBeenCalledWith( - `Attempted to mutate object property "foo4" from extension with id myorg-extensions-app and version unknown`, + `Attempted to mutate object property "foo4" from extension with id grafana-basic-app and version unknown`, { stack: expect.any(String), } @@ -506,8 +477,9 @@ describe('usePluginComponents()', () => { component: () =>
Component
, }; - // The `AddedComponentsRegistry` is validating if the link is registered in the plugin metadata (config.apps). - config.apps[pluginId].extensions.addedComponents = [componentConfig]; + registries.addedComponentsRegistry = new AddedComponentsRegistry([ + { ...apps[0], extensions: { ...apps[0].extensions, addedComponents: [componentConfig] } }, + ]); wrapper = ({ children }: { children: React.ReactNode }) => ( { let registries: PluginExtensionRegistries; let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; let pluginMeta: PluginMeta; - const pluginId = 'myorg-extensions-app'; + const pluginId = basicApp.id; const extensionPointId = `${pluginId}/extension-point/v1`; + let apps: AppPluginConfig[]; beforeEach(() => { jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false }); jest.mocked(isGrafanaDevMode).mockReturnValue(false); + apps = [basicApp]; registries = { - addedComponentsRegistry: new AddedComponentsRegistry(), - exposedComponentsRegistry: new ExposedComponentsRegistry(), - addedLinksRegistry: new AddedLinksRegistry(), - addedFunctionsRegistry: new AddedFunctionsRegistry(), + addedComponentsRegistry: new AddedComponentsRegistry(apps), + exposedComponentsRegistry: new ExposedComponentsRegistry(apps), + addedLinksRegistry: new AddedLinksRegistry(apps), + addedFunctionsRegistry: new AddedFunctionsRegistry(apps), }; resetLogMock(log); @@ -107,32 +103,6 @@ describe('usePluginFunctions()', () => { }, }; - config.apps[pluginId] = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - extensions: { - addedLinks: [], - addedComponents: [], - addedFunctions: [], - exposedComponents: [], - extensionPoints: [], - }, - }; - wrapper = ({ children }: { children: React.ReactNode }) => ( {children} @@ -328,8 +298,9 @@ describe('usePluginFunctions()', () => { fn: () => 'function1', }; - // The `AddedFunctionsRegistry` is validating if the function is registered in the plugin metadata (config.apps). - config.apps[pluginId].extensions.addedFunctions = [functionConfig]; + registries.addedFunctionsRegistry = new AddedFunctionsRegistry([ + { ...apps[0], extensions: { ...apps[0].extensions!, addedFunctions: [functionConfig] } }, + ]); wrapper = ({ children }: { children: React.ReactNode }) => ( { let registries: PluginExtensionRegistries; let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; let pluginMeta: PluginMeta; - const pluginId = 'myorg-extensions-app'; + const pluginId = basicApp.id; const extensionPointId = `${pluginId}/extension-point/v1`; + let apps: AppPluginConfig[]; beforeEach(() => { jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false }); jest.mocked(isGrafanaDevMode).mockReturnValue(false); + + apps = [basicApp]; registries = { - addedComponentsRegistry: new AddedComponentsRegistry(), - exposedComponentsRegistry: new ExposedComponentsRegistry(), - addedLinksRegistry: new AddedLinksRegistry(), - addedFunctionsRegistry: new AddedFunctionsRegistry(), + addedComponentsRegistry: new AddedComponentsRegistry(apps), + exposedComponentsRegistry: new ExposedComponentsRegistry(apps), + addedLinksRegistry: new AddedLinksRegistry(apps), + addedFunctionsRegistry: new AddedFunctionsRegistry(apps), }; resetLogMock(log); @@ -107,32 +104,6 @@ describe('usePluginLinks()', () => { }, }; - config.apps[pluginId] = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - extensions: { - addedLinks: [], - addedComponents: [], - addedFunctions: [], - exposedComponents: [], - extensionPoints: [], - }, - }; - wrapper = ({ children }: { children: React.ReactNode }) => ( {children} @@ -268,8 +239,9 @@ describe('usePluginLinks()', () => { path: `/a/${pluginId}/2`, }; - // The `AddedLinksRegistry` is validating if the link is registered in the plugin metadata (config.apps). - config.apps[pluginId].extensions.addedLinks = [linkConfig]; + registries.addedLinksRegistry = new AddedLinksRegistry([ + { ...apps[0], extensions: { ...apps[0].extensions!, addedLinks: [linkConfig] } }, + ]); wrapper = ({ children }: { children: React.ReactNode }) => ( = {}, + extensionOverrides: Partial = {} +): AppPluginConfig { + return { + ...basicApp, + extensions: { + ...basicApp.extensions, + ...extensionOverrides, + }, + id: pluginId, + ...overrides, + }; +} + +function createPluginContext( + pluginId: string, + overrides: { + extensions?: Partial; + dependencies?: Partial; + } = {} +): PluginContextType { + return { + meta: { + id: pluginId, + name: 'Extensions App', + type: PluginType.app, + module: '', + baseUrl: '', + info: { + author: { + name: 'MyOrg', + }, + description: 'App for testing extensions', + links: [], + logos: { + large: '', + small: '', + }, + screenshots: [], + updated: '2023-10-26T18:25:01Z', + version: '1.0.0', + }, + extensions: { + addedLinks: [], + addedComponents: [], + exposedComponents: [], + extensionPoints: [], + addedFunctions: [], + ...overrides.extensions, + }, + dependencies: { + grafanaVersion: '8.0.0', + plugins: [], + extensions: { + exposedComponents: [], + }, + ...overrides.dependencies, + }, + }, + }; +} + describe('Plugin Extension Validators', () => { describe('assertConfigureIsValid()', () => { it('should NOT throw an error if the configure() function is missing', () => { @@ -130,7 +196,7 @@ describe('Plugin Extension Validators', () => { expect(isReactComponent(wrapped)).toBe(true); }); - it('should return FALSE if we pass in a valid React component', () => { + it('should return FALSE if we pass in an invalid React component', () => { expect(isReactComponent('Foo bar')).toBe(false); expect(isReactComponent(123)).toBe(false); expect(isReactComponent(false)).toBe(false); @@ -209,7 +275,7 @@ describe('Plugin Extension Validators', () => { ).toBe(false); }); - it('should return FALSE true if the extension point id is set by a core plugin', () => { + it('should return TRUE if the extension point id is set by a core plugin', () => { expect( isExtensionPointIdValid({ extensionPointId: 'traces', @@ -223,54 +289,17 @@ describe('Plugin Extension Validators', () => { }); describe('isAddedLinkMetaInfoMissing()', () => { - const originalApps = config.apps; - const pluginId = 'myorg-extensions-app'; - const appPluginConfig = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - extensions: { - addedLinks: [], - addedComponents: [], - exposedComponents: [], - extensionPoints: [], - addedFunctions: [], - }, - }; const extensionConfig = { targets: [PluginExtensionPoints.DashboardPanelMenu], title: 'Link title', description: 'Link description', }; - beforeEach(() => { - config.apps = { - [pluginId]: appPluginConfig, - }; - }); - - afterEach(() => { - config.apps = originalApps; - }); - it('should return FALSE if the meta-info in the plugin.json is correct', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks.push(extensionConfig); + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedLinks: [extensionConfig] })]; - const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); + const returnValue = isAddedLinkMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps); expect(returnValue).toBe(false); expect(log.error).toHaveBeenCalledTimes(0); @@ -278,9 +307,9 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the app config is not found', () => { const log = createLogMock(); - delete config.apps[pluginId]; + const apps: AppPluginConfig[] = []; - const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); + const returnValue = isAddedLinkMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps); expect(returnValue).toBe(true); expect(log.error).toHaveBeenCalledTimes(1); @@ -289,9 +318,9 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the link has no meta-info in the plugin.json', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks = []; + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedLinks: [] })]; - const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); + const returnValue = isAddedLinkMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps); expect(returnValue).toBe(true); expect(log.error).toHaveBeenCalledTimes(1); @@ -302,15 +331,16 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the "targets" do not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks.push(extensionConfig); + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedLinks: [extensionConfig] })]; const returnValue = isAddedLinkMetaInfoMissing( - pluginId, + PLUGIN_ID, { ...extensionConfig, targets: [PluginExtensionPoints.DashboardPanelMenu, PluginExtensionPoints.ExploreToolbarAction], }, - log + log, + apps ); expect(returnValue).toBe(true); @@ -322,15 +352,16 @@ describe('Plugin Extension Validators', () => { it('should return FALSE and log a warning if the "description" does not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks.push(extensionConfig); + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedLinks: [extensionConfig] })]; const returnValue = isAddedLinkMetaInfoMissing( - pluginId, + PLUGIN_ID, { ...extensionConfig, description: 'Link description UPDATED', }, - log + log, + apps ); expect(returnValue).toBe(false); @@ -340,14 +371,13 @@ describe('Plugin Extension Validators', () => { it('should return FALSE with links with the same title but different targets', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks.push(extensionConfig); const extensionConfig2 = { ...extensionConfig, targets: [PluginExtensionPoints.ExploreToolbarAction], }; - config.apps[pluginId].extensions.addedLinks.push(extensionConfig2); + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedLinks: [extensionConfig, extensionConfig2] })]; - const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig2, log); + const returnValue = isAddedLinkMetaInfoMissing(PLUGIN_ID, extensionConfig2, log, apps); expect(returnValue).toBe(false); expect(log.error).toHaveBeenCalledTimes(0); @@ -355,33 +385,6 @@ describe('Plugin Extension Validators', () => { }); describe('isAddedComponentMetaInfoMissing()', () => { - const originalApps = config.apps; - const pluginId = 'myorg-extensions-app'; - const appPluginConfig = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - extensions: { - addedLinks: [], - addedComponents: [], - exposedComponents: [], - extensionPoints: [], - addedFunctions: [], - }, - }; const extensionConfig = { targets: [PluginExtensionPoints.DashboardPanelMenu], title: 'Component title', @@ -389,21 +392,11 @@ describe('Plugin Extension Validators', () => { component: () =>
Component content
, }; - beforeEach(() => { - config.apps = { - [pluginId]: appPluginConfig, - }; - }); - - afterEach(() => { - config.apps = originalApps; - }); - it('should return FALSE if the meta-info in the plugin.json is correct', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents.push(extensionConfig); + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedComponents: [extensionConfig] })]; - const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); + const returnValue = isAddedComponentMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps); expect(returnValue).toBe(false); expect(log.error).toHaveBeenCalledTimes(0); @@ -411,9 +404,9 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the app config is not found', () => { const log = createLogMock(); - delete config.apps[pluginId]; + const apps: AppPluginConfig[] = []; - const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); + const returnValue = isAddedComponentMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps); expect(returnValue).toBe(true); expect(log.error).toHaveBeenCalledTimes(1); @@ -422,9 +415,9 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the Component has no meta-info in the plugin.json', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents = []; + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedComponents: [] })]; - const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); + const returnValue = isAddedComponentMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps); expect(returnValue).toBe(true); expect(log.error).toHaveBeenCalledTimes(1); @@ -435,15 +428,16 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the "targets" do not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents.push(extensionConfig); + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedComponents: [extensionConfig] })]; const returnValue = isAddedComponentMetaInfoMissing( - pluginId, + PLUGIN_ID, { ...extensionConfig, targets: [PluginExtensionPoints.ExploreToolbarAction], }, - log + log, + apps ); expect(returnValue).toBe(true); @@ -454,15 +448,16 @@ describe('Plugin Extension Validators', () => { it('should return FALSE and log a warning if the "description" does not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents.push(extensionConfig); + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedComponents: [extensionConfig] })]; const returnValue = isAddedComponentMetaInfoMissing( - pluginId, + PLUGIN_ID, { ...extensionConfig, description: 'UPDATED', }, - log + log, + apps ); expect(returnValue).toBe(false); @@ -472,14 +467,13 @@ describe('Plugin Extension Validators', () => { it('should return FALSE with components with the same title but different targets', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents.push(extensionConfig); const extensionConfig2 = { ...extensionConfig, targets: [PluginExtensionPoints.ExploreToolbarAction], }; - config.apps[pluginId].extensions.addedComponents.push(extensionConfig2); + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedComponents: [extensionConfig, extensionConfig2] })]; - const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig2, log); + const returnValue = isAddedComponentMetaInfoMissing(PLUGIN_ID, extensionConfig2, log, apps); expect(returnValue).toBe(false); expect(log.error).toHaveBeenCalledTimes(0); @@ -487,55 +481,18 @@ describe('Plugin Extension Validators', () => { }); describe('isExposedComponentMetaInfoMissing()', () => { - const originalApps = config.apps; - const pluginId = 'myorg-extensions-app'; - const appPluginConfig = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - extensions: { - addedLinks: [], - addedComponents: [], - exposedComponents: [], - extensionPoints: [], - addedFunctions: [], - }, - }; const exposedComponentConfig = { - id: `${pluginId}/component/v1`, + id: `${PLUGIN_ID}/component/v1`, title: 'Exposed component', description: 'Exposed component description', component: () =>
Component content
, }; - beforeEach(() => { - config.apps = { - [pluginId]: appPluginConfig, - }; - }); - - afterEach(() => { - config.apps = originalApps; - }); - it('should return FALSE if the meta-info in the plugin.json is correct', () => { const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { exposedComponents: [exposedComponentConfig] })]; - const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); + const returnValue = isExposedComponentMetaInfoMissing(PLUGIN_ID, exposedComponentConfig, log, apps); expect(returnValue).toBe(false); expect(log.warning).toHaveBeenCalledTimes(0); @@ -543,9 +500,9 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the app config is not found', () => { const log = createLogMock(); - delete config.apps[pluginId]; + const apps: AppPluginConfig[] = []; - const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); + const returnValue = isExposedComponentMetaInfoMissing(PLUGIN_ID, exposedComponentConfig, log, apps); expect(returnValue).toBe(true); expect(log.error).toHaveBeenCalledTimes(1); @@ -554,9 +511,9 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the exposed component has no meta-info in the plugin.json', () => { const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents = []; + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { exposedComponents: [] })]; - const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); + const returnValue = isExposedComponentMetaInfoMissing(PLUGIN_ID, exposedComponentConfig, log, apps); expect(returnValue).toBe(true); expect(log.error).toHaveBeenCalledTimes(1); @@ -567,15 +524,16 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the title does not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { exposedComponents: [exposedComponentConfig] })]; const returnValue = isExposedComponentMetaInfoMissing( - pluginId, + PLUGIN_ID, { ...exposedComponentConfig, title: 'UPDATED', }, - log + log, + apps ); expect(returnValue).toBe(true); @@ -587,15 +545,16 @@ describe('Plugin Extension Validators', () => { it('should return FALSE and log a warning if the "description" does not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); + const apps = [createAppPluginConfig(PLUGIN_ID, {}, { exposedComponents: [exposedComponentConfig] })]; const returnValue = isExposedComponentMetaInfoMissing( - pluginId, + PLUGIN_ID, { ...exposedComponentConfig, description: 'UPDATED', }, - log + log, + apps ); expect(returnValue).toBe(false); @@ -605,14 +564,15 @@ describe('Plugin Extension Validators', () => { it('should return FALSE with components with the same title but different targets', () => { const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); const exposedComponentConfig2 = { ...exposedComponentConfig, targets: [PluginExtensionPoints.ExploreToolbarAction], }; - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig2); + const apps = [ + createAppPluginConfig(PLUGIN_ID, {}, { exposedComponents: [exposedComponentConfig, exposedComponentConfig2] }), + ]; - const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig2, log); + const returnValue = isExposedComponentMetaInfoMissing(PLUGIN_ID, exposedComponentConfig2, log, apps); expect(returnValue).toBe(false); expect(log.error).toHaveBeenCalledTimes(0); @@ -620,113 +580,54 @@ describe('Plugin Extension Validators', () => { }); describe('isExposedComponentDependencyMissing()', () => { - let pluginContext: PluginContextType; - const pluginId = 'myorg-extensions-app'; - const exposedComponentId = `${pluginId}/component/v1`; - - beforeEach(() => { - pluginContext = { - meta: { - id: pluginId, - name: 'Extensions App', - type: PluginType.app, - module: '', - baseUrl: '', - info: { - author: { - name: 'MyOrg', - }, - description: 'App for testing extensions', - links: [], - logos: { - large: '', - small: '', - }, - screenshots: [], - updated: '2023-10-26T18:25:01Z', - version: '1.0.0', - }, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - }, - }; - }); + const exposedComponentId = `${PLUGIN_ID}/component/v1`; it('should return FALSE if the meta-info in the plugin.json is correct', () => { - pluginContext.meta.dependencies?.extensions.exposedComponents.push(exposedComponentId); + const pluginContext = createPluginContext(PLUGIN_ID, { + dependencies: { + extensions: { + exposedComponents: [exposedComponentId], + }, + }, + }); + const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext); + expect(returnValue).toBe(false); }); it('should return TRUE if the dependencies are missing', () => { + const pluginContext = createPluginContext(PLUGIN_ID); delete pluginContext.meta.dependencies; + const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext); + expect(returnValue).toBe(true); }); it('should return TRUE if the exposed component id is not specified in the list of dependencies', () => { + const pluginContext = createPluginContext(PLUGIN_ID); + const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext); + expect(returnValue).toBe(true); }); }); describe('isExtensionPointMetaInfoMissing()', () => { - let pluginContext: PluginContextType; - const pluginId = 'myorg-extensions-app'; - const extensionPointId = `${pluginId}/extension-point/v1`; + const extensionPointId = `${PLUGIN_ID}/extension-point/v1`; const extensionPointConfig = { id: extensionPointId, title: 'Extension point title', description: 'Extension point description', }; - beforeEach(() => { - pluginContext = { - meta: { - id: pluginId, - name: 'Extensions App', - type: PluginType.app, - module: '', - baseUrl: '', - info: { - author: { - name: 'MyOrg', - }, - description: 'App for testing extensions', - links: [], - logos: { - large: '', - small: '', - }, - screenshots: [], - updated: '2023-10-26T18:25:01Z', - version: '1.0.0', - }, - extensions: { - addedLinks: [], - addedComponents: [], - exposedComponents: [], - extensionPoints: [], - addedFunctions: [], - }, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - }, - }; - }); - it('should return FALSE if the meta-info in the plugin.json is correct', () => { - pluginContext.meta.extensions?.extensionPoints.push(extensionPointConfig); + const pluginContext = createPluginContext(PLUGIN_ID, { + extensions: { + extensionPoints: [extensionPointConfig], + }, + }); const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext); @@ -734,7 +635,10 @@ describe('Plugin Extension Validators', () => { }); it('should return TRUE if the extension point id is not recorded in the plugin.json', () => { + const pluginContext = createPluginContext(PLUGIN_ID); + const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext); + expect(returnValue).toBe(true); }); }); diff --git a/public/app/features/plugins/extensions/validators.ts b/public/app/features/plugins/extensions/validators.ts index 07029a9a1e9..eb2258ac3e6 100644 --- a/public/app/features/plugins/extensions/validators.ts +++ b/public/app/features/plugins/extensions/validators.ts @@ -8,9 +8,10 @@ import { type PluginExtensionAddedFunctionConfig, PluginExtensionPoints, PluginExtensionPointPatterns, + AppPluginConfig, } from '@grafana/data'; import { PluginAddedLinksConfigureFunc } from '@grafana/data/internal'; -import { config, isPluginExtensionLink } from '@grafana/runtime'; +import { isPluginExtensionLink } from '@grafana/runtime'; import * as errors from './errors'; import { ExtensionsLog } from './logs/log'; @@ -148,10 +149,11 @@ export const isExposedComponentDependencyMissing = (id: string, pluginContext: P export const isAddedLinkMetaInfoMissing = ( pluginId: string, metaInfo: PluginExtensionAddedLinkConfig, - log: ExtensionsLog + log: ExtensionsLog, + apps: AppPluginConfig[] ) => { const logPrefix = 'Could not register link extension. Reason:'; - const app = config.apps[pluginId]; + const app = apps.find((a) => a.id === pluginId); const pluginJsonMetaInfo = app ? app.extensions.addedLinks.filter(({ title }) => title === metaInfo.title) : null; if (!app) { @@ -180,10 +182,11 @@ export const isAddedLinkMetaInfoMissing = ( export const isAddedFunctionMetaInfoMissing = ( pluginId: string, metaInfo: PluginExtensionAddedFunctionConfig, - log: ExtensionsLog + log: ExtensionsLog, + apps: AppPluginConfig[] ) => { const logPrefix = 'Could not register function extension. Reason:'; - const app = config.apps[pluginId]; + const app = apps.find((a) => a.id === pluginId); const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.filter(({ title }) => title === metaInfo.title) : null; if (!app) { @@ -212,10 +215,11 @@ export const isAddedFunctionMetaInfoMissing = ( export const isAddedComponentMetaInfoMissing = ( pluginId: string, metaInfo: PluginExtensionAddedComponentConfig, - log: ExtensionsLog + log: ExtensionsLog, + apps: AppPluginConfig[] ) => { const logPrefix = 'Could not register component extension. Reason:'; - const app = config.apps[pluginId]; + const app = apps.find((a) => a.id === pluginId); const pluginJsonMetaInfo = app ? app.extensions.addedComponents.filter(({ title }) => title === metaInfo.title) : null; @@ -246,10 +250,11 @@ export const isAddedComponentMetaInfoMissing = ( export const isExposedComponentMetaInfoMissing = ( pluginId: string, metaInfo: PluginExtensionExposedComponentConfig, - log: ExtensionsLog + log: ExtensionsLog, + apps: AppPluginConfig[] ) => { const logPrefix = 'Could not register exposed component extension. Reason:'; - const app = config.apps[pluginId]; + const app = apps.find((a) => a.id === pluginId); const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.filter(({ id }) => id === metaInfo.id) : null; if (!app) { diff --git a/public/app/features/plugins/importer/pluginImporter.test.ts b/public/app/features/plugins/importer/pluginImporter.test.ts index 63061f8016d..bbd8343b630 100644 --- a/public/app/features/plugins/importer/pluginImporter.test.ts +++ b/public/app/features/plugins/importer/pluginImporter.test.ts @@ -10,21 +10,50 @@ import { PluginType, } from '@grafana/data'; -import { - addedComponentsRegistry, - addedFunctionsRegistry, - addedLinksRegistry, - exposedComponentsRegistry, -} from '../extensions/registry/setup'; +import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsRegistry'; +import { AddedFunctionsRegistry } from '../extensions/registry/AddedFunctionsRegistry'; +import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry'; +import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry'; import { pluginsLogger } from '../utils'; import * as importPluginModule from './importPluginModule'; import { pluginImporter, clearCaches } from './pluginImporter'; +jest.mock('../extensions/registry/setup', () => ({ + ...jest.requireActual('../extensions/registry/setup'), + getPluginExtensionRegistries: jest.fn(), +})); + +const { getPluginExtensionRegistries } = jest.requireMock('../extensions/registry/setup'); +const getPluginExtensionRegistriesMock = jest.mocked(getPluginExtensionRegistries); + describe('pluginImporter', () => { + let exposedComponentsRegistry: ExposedComponentsRegistry; + let addedComponentsRegistry: AddedComponentsRegistry; + let addedLinksRegistry: AddedLinksRegistry; + let addedFunctionsRegistry: AddedFunctionsRegistry; + beforeEach(() => { jest.clearAllMocks(); clearCaches(); + addedComponentsRegistry = new AddedComponentsRegistry([]); + addedFunctionsRegistry = new AddedFunctionsRegistry([]); + addedLinksRegistry = new AddedLinksRegistry([]); + exposedComponentsRegistry = new ExposedComponentsRegistry([]); + + addedComponentsRegistry.register = jest.fn(); + addedFunctionsRegistry.register = jest.fn(); + addedLinksRegistry.register = jest.fn(); + exposedComponentsRegistry.register = jest.fn(); + + const registries = { + addedComponentsRegistry, + addedFunctionsRegistry, + addedLinksRegistry, + exposedComponentsRegistry, + }; + + getPluginExtensionRegistriesMock.mockResolvedValue(registries); }); describe('importPanel', () => { @@ -217,12 +246,7 @@ describe('pluginImporter', () => { addedFunctionConfigs: [{}], }, }; - const exposedComponentsRegistrySpy = jest - .spyOn(exposedComponentsRegistry, 'register') - .mockImplementation(() => {}); - const addedComponentsRegistrySpy = jest.spyOn(addedComponentsRegistry, 'register').mockImplementation(() => {}); - const addedLinksRegistrySpy = jest.spyOn(addedLinksRegistry, 'register').mockImplementation(() => {}); - const addedFunctionsRegistrySpy = jest.spyOn(addedFunctionsRegistry, 'register').mockImplementation(() => {}); + const spy = jest.spyOn(importPluginModule, 'importPluginModule').mockResolvedValue({ ...plugin }); const result = await pluginImporter.importApp({ ...appPlugin }); @@ -237,10 +261,23 @@ describe('pluginImporter', () => { }); expect(init).toHaveBeenCalledWith({ ...appPlugin }); expect(setComponentsFromLegacyExports).toHaveBeenCalledWith({ ...plugin }); - expect(exposedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); - expect(addedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); - expect(addedLinksRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); - expect(addedFunctionsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); + expect(addedComponentsRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [{}], + }); + expect(addedLinksRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [{}], + }); + expect(addedFunctionsRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [{}], + }); + + expect(exposedComponentsRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [{}], + }); expect(result).toEqual({ ...appPlugin, @@ -268,12 +305,7 @@ describe('pluginImporter', () => { addedFunctionConfigs: [{}], }, }; - const exposedComponentsRegistrySpy = jest - .spyOn(exposedComponentsRegistry, 'register') - .mockImplementation(() => {}); - const addedComponentsRegistrySpy = jest.spyOn(addedComponentsRegistry, 'register').mockImplementation(() => {}); - const addedLinksRegistrySpy = jest.spyOn(addedLinksRegistry, 'register').mockImplementation(() => {}); - const addedFunctionsRegistrySpy = jest.spyOn(addedFunctionsRegistry, 'register').mockImplementation(() => {}); + const spy = jest.spyOn(importPluginModule, 'importPluginModule').mockResolvedValue({ ...plugin }); const meta = { ...appPlugin, loadingStrategy: PluginLoadingStrategy.script }; @@ -289,10 +321,23 @@ describe('pluginImporter', () => { }); expect(init).toHaveBeenCalledWith({ ...meta }); expect(setComponentsFromLegacyExports).toHaveBeenCalledWith({ ...plugin }); - expect(exposedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); - expect(addedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); - expect(addedLinksRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); - expect(addedFunctionsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); + expect(addedComponentsRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [{}], + }); + expect(addedLinksRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [{}], + }); + expect(addedFunctionsRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [{}], + }); + + expect(exposedComponentsRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [{}], + }); expect(result).toEqual({ ...appPlugin, @@ -307,12 +352,6 @@ describe('pluginImporter', () => { }); it('should import an empty app plugin if missing exported plugin', async () => { - const exposedComponentsRegistrySpy = jest - .spyOn(exposedComponentsRegistry, 'register') - .mockImplementation(() => {}); - const addedComponentsRegistrySpy = jest.spyOn(addedComponentsRegistry, 'register').mockImplementation(() => {}); - const addedLinksRegistrySpy = jest.spyOn(addedLinksRegistry, 'register').mockImplementation(() => {}); - const addedFunctionsRegistrySpy = jest.spyOn(addedFunctionsRegistry, 'register').mockImplementation(() => {}); const spy = jest.spyOn(importPluginModule, 'importPluginModule').mockResolvedValue({}); const result = await pluginImporter.importApp({ ...appPlugin }); @@ -325,10 +364,24 @@ describe('pluginImporter', () => { moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, }); - expect(exposedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] }); - expect(addedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] }); - expect(addedLinksRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] }); - expect(addedFunctionsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] }); + + expect(addedComponentsRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [], + }); + expect(addedLinksRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [], + }); + expect(addedFunctionsRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [], + }); + + expect(exposedComponentsRegistry.register).toHaveBeenCalledWith({ + pluginId: 'test-plugin', + configs: [], + }); expect(result).toEqual({ ...new AppPlugin(), meta: { ...appPlugin } }); }); diff --git a/public/app/features/plugins/importer/pluginImporter.ts b/public/app/features/plugins/importer/pluginImporter.ts index cf2b407021b..5534b87e615 100644 --- a/public/app/features/plugins/importer/pluginImporter.ts +++ b/public/app/features/plugins/importer/pluginImporter.ts @@ -16,12 +16,7 @@ import { config } from '@grafana/runtime'; import { GenericDataSourcePlugin } from 'app/features/datasources/types'; import { getPanelPluginLoadError } from 'app/features/panel/components/PanelPluginError'; -import { - addedComponentsRegistry, - addedFunctionsRegistry, - addedLinksRegistry, - exposedComponentsRegistry, -} from '../extensions/registry/setup'; +import { getPluginExtensionRegistries } from '../extensions/registry/setup'; import { pluginsLogger } from '../utils'; import { importPluginModule } from './importPluginModule'; @@ -98,6 +93,8 @@ const appPluginPostImport: PostImportStrategy = async plugin.meta = meta; plugin.setComponentsFromLegacyExports(pluginExports); + const { exposedComponentsRegistry, addedComponentsRegistry, addedFunctionsRegistry, addedLinksRegistry } = + await getPluginExtensionRegistries(); exposedComponentsRegistry.register({ pluginId: meta.id, configs: plugin.exposedComponentConfigs || [] }); addedComponentsRegistry.register({ pluginId: meta.id, configs: plugin.addedComponentConfigs || [] }); addedLinksRegistry.register({ pluginId: meta.id, configs: plugin.addedLinkConfigs || [] });