mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugin extensions: Introduce new registry for added components (#91877)
* add added component registry * fix broken test * add tests for usePluginComponents hook * readd expose components * add type assertion exceptions to betterer results * use new addedComponent registry in legacy endpoints * remove unused code * cleanup * revert test code * remove commented code * wrap in try catch * pr feedback
This commit is contained in:
parent
419edef4dc
commit
b648ce3acf
@ -208,7 +208,8 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"]
|
||||
],
|
||||
"packages/grafana-data/src/types/config.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
@ -542,9 +543,11 @@ exports[`better eslint`] = {
|
||||
"packages/grafana-runtime/src/services/pluginExtensions/usePluginComponent.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"packages/grafana-runtime/src/services/pluginExtensions/usePluginComponents.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
@ -4988,6 +4991,9 @@ exports[`better eslint`] = {
|
||||
"public/app/features/plugins/extensions/getPluginExtensions.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/plugins/extensions/usePluginComponents.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/plugins/loader/sharedDependencies.ts:5381": [
|
||||
[0, 0, 0, "* import is invalid because \'Layout,HorizontalGroup,VerticalGroup\' from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"]
|
||||
],
|
||||
|
@ -556,6 +556,7 @@ export {
|
||||
type PluginExtensionCommandPaletteContext,
|
||||
type PluginExtensionOpenModalOptions,
|
||||
type PluginExposedComponentConfig,
|
||||
type PluginAddedComponentConfig,
|
||||
} from './types/pluginExtensions';
|
||||
export {
|
||||
type ScopeDashboardBindingSpec,
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
PluginExtensionComponentConfig,
|
||||
PluginExposedComponentConfig,
|
||||
PluginExtensionConfig,
|
||||
PluginAddedComponentConfig,
|
||||
} from './pluginExtensions';
|
||||
|
||||
/**
|
||||
@ -58,6 +59,7 @@ export interface AppPluginMeta<T extends KeyValue = KeyValue> extends PluginMeta
|
||||
|
||||
export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppPluginMeta<T>> {
|
||||
private _exposedComponentConfigs: PluginExposedComponentConfig[] = [];
|
||||
private _addedComponentConfigs: PluginAddedComponentConfig[] = [];
|
||||
private _extensionConfigs: PluginExtensionConfig[] = [];
|
||||
|
||||
// Content under: /a/${plugin-id}/*
|
||||
@ -104,6 +106,10 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
||||
return this._exposedComponentConfigs;
|
||||
}
|
||||
|
||||
get addedComponentConfigs() {
|
||||
return this._addedComponentConfigs;
|
||||
}
|
||||
|
||||
get extensionConfigs() {
|
||||
return this._extensionConfigs;
|
||||
}
|
||||
@ -128,22 +134,8 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
||||
return this;
|
||||
}
|
||||
|
||||
addComponent<Props = {}>(
|
||||
extensionConfig: { targets: string | string[] } & Omit<
|
||||
PluginExtensionComponentConfig<Props>,
|
||||
'type' | 'extensionPointId'
|
||||
>
|
||||
) {
|
||||
const { targets, ...extension } = extensionConfig;
|
||||
const targetsArray = Array.isArray(targets) ? targets : [targets];
|
||||
|
||||
targetsArray.forEach((target) => {
|
||||
this._extensionConfigs.push({
|
||||
...extension,
|
||||
extensionPointId: target,
|
||||
type: PluginExtensionTypes.component,
|
||||
} as PluginExtensionComponentConfig);
|
||||
});
|
||||
addComponent<Props = {}>(addedComponentConfig: PluginAddedComponentConfig<Props>) {
|
||||
this._addedComponentConfigs.push(addedComponentConfig as PluginAddedComponentConfig);
|
||||
|
||||
return this;
|
||||
}
|
||||
@ -168,6 +160,7 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
||||
this.addComponent({
|
||||
targets: [extension.extensionPointId],
|
||||
...extension,
|
||||
component: extension.component as ComponentType,
|
||||
});
|
||||
|
||||
return this;
|
||||
|
@ -95,6 +95,28 @@ export type PluginExtensionComponentConfig<Props = {}> = {
|
||||
extensionPointId: string;
|
||||
};
|
||||
|
||||
export type PluginAddedComponentConfig<Props = {}> = {
|
||||
/**
|
||||
* The target extension points where the component will be added
|
||||
*/
|
||||
targets: string | string[];
|
||||
|
||||
/**
|
||||
* The title of the component
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* A short description of the component
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* The React component that will added to the target extension points
|
||||
*/
|
||||
component: React.ComponentType<Props>;
|
||||
};
|
||||
|
||||
export type PluginExposedComponentConfig<Props = {}> = {
|
||||
/**
|
||||
* The unique identifier of the component
|
||||
|
@ -26,11 +26,11 @@ export {
|
||||
usePluginExtensions,
|
||||
usePluginLinkExtensions,
|
||||
usePluginComponentExtensions,
|
||||
usePluginComponents,
|
||||
usePluginLinks,
|
||||
} from './pluginExtensions/usePluginExtensions';
|
||||
|
||||
export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent';
|
||||
export { setPluginComponentsHook, usePluginComponents } from './pluginExtensions/usePluginComponents';
|
||||
|
||||
export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils';
|
||||
export { setCurrentUser } from './user';
|
||||
|
@ -16,6 +16,11 @@ export type GetPluginExtensionsOptions = {
|
||||
limitPerPlugin?: number;
|
||||
};
|
||||
|
||||
export type UsePluginComponentOptions = {
|
||||
extensionPointId: string;
|
||||
limitPerPlugin?: number;
|
||||
};
|
||||
|
||||
export type GetPluginExtensionsResult<T = PluginExtension> = {
|
||||
extensions: T[];
|
||||
};
|
||||
@ -30,6 +35,11 @@ export type UsePluginComponentResult<Props = {}> = {
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export type UsePluginComponentsResult<Props = {}> = {
|
||||
components: Array<React.ComponentType<Props>>;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
let singleton: GetPluginExtensions | undefined;
|
||||
|
||||
export function setPluginExtensionGetter(instance: GetPluginExtensions): void {
|
||||
|
@ -0,0 +1,24 @@
|
||||
import { GetPluginExtensionsOptions, UsePluginComponentsResult } from './getPluginExtensions';
|
||||
|
||||
export type UsePluginComponents<Props extends object = {}> = (
|
||||
options: GetPluginExtensionsOptions
|
||||
) => UsePluginComponentsResult<Props>;
|
||||
|
||||
let singleton: UsePluginComponents | undefined;
|
||||
|
||||
export function setPluginComponentsHook(hook: UsePluginComponents): void {
|
||||
// We allow overriding the registry in tests
|
||||
if (singleton && process.env.NODE_ENV !== 'test') {
|
||||
throw new Error('setPluginComponentsHook() function should only be called once, when Grafana is starting.');
|
||||
}
|
||||
singleton = hook;
|
||||
}
|
||||
|
||||
export function usePluginComponents<Props extends object = {}>(
|
||||
options: GetPluginExtensionsOptions
|
||||
): UsePluginComponentsResult<Props> {
|
||||
if (!singleton) {
|
||||
throw new Error('setPluginComponentsHook(options) can only be used after the Grafana instance has started.');
|
||||
}
|
||||
return singleton(options) as UsePluginComponentsResult<Props>;
|
||||
}
|
@ -39,22 +39,6 @@ export function usePluginLinks(options: GetPluginExtensionsOptions): {
|
||||
}, [extensions, isLoading]);
|
||||
}
|
||||
|
||||
export function usePluginComponents<Props = {}>(
|
||||
options: GetPluginExtensionsOptions
|
||||
): { components: Array<React.ComponentType<Props>>; isLoading: boolean } {
|
||||
const { extensions, isLoading } = usePluginExtensions(options);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
components: extensions
|
||||
.filter(isPluginExtensionComponent)
|
||||
.map(({ component }) => component as React.ComponentType<Props>),
|
||||
isLoading,
|
||||
}),
|
||||
[extensions, isLoading]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use usePluginLinks() instead.
|
||||
*/
|
||||
|
@ -38,6 +38,7 @@ import {
|
||||
setReturnToPreviousHook,
|
||||
setPluginExtensionsHook,
|
||||
setPluginComponentHook,
|
||||
setPluginComponentsHook,
|
||||
setCurrentUser,
|
||||
setChromeHeaderHeightHook,
|
||||
} from '@grafana/runtime';
|
||||
@ -85,8 +86,10 @@ import { DatasourceSrv } from './features/plugins/datasource_srv';
|
||||
import { getCoreExtensionConfigurations } from './features/plugins/extensions/getCoreExtensionConfigurations';
|
||||
import { createPluginExtensionsGetter } from './features/plugins/extensions/getPluginExtensions';
|
||||
import { ReactivePluginExtensionsRegistry } from './features/plugins/extensions/reactivePluginExtensionRegistry';
|
||||
import { AddedComponentsRegistry } from './features/plugins/extensions/registry/AddedComponentsRegistry';
|
||||
import { ExposedComponentsRegistry } from './features/plugins/extensions/registry/ExposedComponentsRegistry';
|
||||
import { createUsePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
||||
import { createUsePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
||||
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
|
||||
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
|
||||
import { preloadPlugins } from './features/plugins/pluginPreloader';
|
||||
@ -210,15 +213,18 @@ export class GrafanaApp {
|
||||
initWindowRuntime();
|
||||
|
||||
// Initialize plugin extensions
|
||||
const extensionsRegistry = new ReactivePluginExtensionsRegistry();
|
||||
extensionsRegistry.register({
|
||||
const pluginExtensionsRegistries = {
|
||||
extensionsRegistry: new ReactivePluginExtensionsRegistry(),
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
};
|
||||
pluginExtensionsRegistries.extensionsRegistry.register({
|
||||
pluginId: 'grafana',
|
||||
extensionConfigs: getCoreExtensionConfigurations(),
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const exposedComponentsRegistry = new ExposedComponentsRegistry();
|
||||
|
||||
if (contextSrv.user.orgRole !== '') {
|
||||
// The "cloud-home-app" is registering banners once it's loaded, and this can cause a rerender in the AppChrome if it's loaded after the Grafana app init.
|
||||
// TODO: remove the following exception once the issue mentioned above is fixed.
|
||||
@ -226,18 +232,24 @@ export class GrafanaApp {
|
||||
const awaitedAppPlugins = Object.values(config.apps).filter((app) => awaitedAppPluginIds.includes(app.id));
|
||||
const appPlugins = Object.values(config.apps).filter((app) => !awaitedAppPluginIds.includes(app.id));
|
||||
|
||||
preloadPlugins(appPlugins, extensionsRegistry, exposedComponentsRegistry);
|
||||
await preloadPlugins(
|
||||
awaitedAppPlugins,
|
||||
extensionsRegistry,
|
||||
exposedComponentsRegistry,
|
||||
'frontend_awaited_plugins_preload'
|
||||
);
|
||||
preloadPlugins(appPlugins, pluginExtensionsRegistries);
|
||||
await preloadPlugins(awaitedAppPlugins, pluginExtensionsRegistries, 'frontend_awaited_plugins_preload');
|
||||
}
|
||||
|
||||
setPluginExtensionGetter(createPluginExtensionsGetter(extensionsRegistry));
|
||||
setPluginExtensionsHook(createUsePluginExtensions(extensionsRegistry));
|
||||
setPluginComponentHook(createUsePluginComponent(exposedComponentsRegistry));
|
||||
setPluginExtensionGetter(
|
||||
createPluginExtensionsGetter(
|
||||
pluginExtensionsRegistries.extensionsRegistry,
|
||||
pluginExtensionsRegistries.addedComponentsRegistry
|
||||
)
|
||||
);
|
||||
setPluginExtensionsHook(
|
||||
createUsePluginExtensions(
|
||||
pluginExtensionsRegistries.extensionsRegistry,
|
||||
pluginExtensionsRegistries.addedComponentsRegistry
|
||||
)
|
||||
);
|
||||
setPluginComponentHook(createUsePluginComponent(pluginExtensionsRegistries.exposedComponentsRegistry));
|
||||
setPluginComponentsHook(createUsePluginComponents(pluginExtensionsRegistries.addedComponentsRegistry));
|
||||
|
||||
// initialize chrome service
|
||||
const queryParams = locationService.getSearchObject();
|
||||
|
@ -1,10 +1,16 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { PluginExtensionComponentConfig, PluginExtensionLinkConfig, PluginExtensionTypes } from '@grafana/data';
|
||||
import {
|
||||
PluginAddedComponentConfig,
|
||||
PluginExtensionComponentConfig,
|
||||
PluginExtensionLinkConfig,
|
||||
PluginExtensionTypes,
|
||||
} from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
|
||||
import { getPluginExtensions } from './getPluginExtensions';
|
||||
import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry';
|
||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||
import { isReadOnlyProxy } from './utils';
|
||||
import { assertPluginExtensionLink } from './validators';
|
||||
|
||||
@ -15,18 +21,30 @@ jest.mock('@grafana/runtime', () => {
|
||||
};
|
||||
});
|
||||
|
||||
function createPluginExtensionRegistry(preloadResults: Array<{ pluginId: string; extensionConfigs: any[] }>) {
|
||||
async function createRegistries(
|
||||
preloadResults: Array<{
|
||||
pluginId: string;
|
||||
addedComponentConfigs: PluginAddedComponentConfig[];
|
||||
extensionConfigs: any[];
|
||||
}>
|
||||
) {
|
||||
const registry = new ReactivePluginExtensionsRegistry();
|
||||
const addedComponentsRegistry = new AddedComponentsRegistry();
|
||||
|
||||
for (const { pluginId, extensionConfigs } of preloadResults) {
|
||||
for (const { pluginId, extensionConfigs, addedComponentConfigs } of preloadResults) {
|
||||
registry.register({
|
||||
pluginId,
|
||||
extensionConfigs,
|
||||
exposedComponentConfigs: [],
|
||||
extensionConfigs,
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
addedComponentsRegistry.register({
|
||||
pluginId,
|
||||
configs: addedComponentConfigs,
|
||||
});
|
||||
}
|
||||
|
||||
return registry.getRegistry();
|
||||
return { registry: await registry.getRegistry(), addedComponentsRegistry: await addedComponentsRegistry.getState() };
|
||||
}
|
||||
|
||||
describe('getPluginExtensions()', () => {
|
||||
@ -69,8 +87,13 @@ describe('getPluginExtensions()', () => {
|
||||
});
|
||||
|
||||
test('should return the extensions for the given placement', async () => {
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 });
|
||||
const registries = await createRegistries([
|
||||
{ pluginId, extensionConfigs: [link1, link2], addedComponentConfigs: [] },
|
||||
]);
|
||||
const { extensions } = getPluginExtensions({
|
||||
...registries,
|
||||
extensionPointId: extensionPoint1,
|
||||
});
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0]).toEqual(
|
||||
@ -86,10 +109,13 @@ describe('getPluginExtensions()', () => {
|
||||
|
||||
test('should not limit the number of extensions per plugin by default', async () => {
|
||||
// Registering 3 extensions for the same plugin for the same placement
|
||||
const registry = await createPluginExtensionRegistry([
|
||||
{ pluginId, extensionConfigs: [link1, link1, link1, link2] },
|
||||
const registries = await createRegistries([
|
||||
{ pluginId, extensionConfigs: [link1, link1, link1, link2], addedComponentConfigs: [] },
|
||||
]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 });
|
||||
const { extensions } = getPluginExtensions({
|
||||
...registries,
|
||||
extensionPointId: extensionPoint1,
|
||||
});
|
||||
|
||||
expect(extensions).toHaveLength(3);
|
||||
expect(extensions[0]).toEqual(
|
||||
@ -104,10 +130,11 @@ describe('getPluginExtensions()', () => {
|
||||
});
|
||||
|
||||
test('should be possible to limit the number of extensions per plugin for a given placement', async () => {
|
||||
const registry = await createPluginExtensionRegistry([
|
||||
{ pluginId, extensionConfigs: [link1, link1, link1, link2] },
|
||||
const registries = await createRegistries([
|
||||
{ pluginId, extensionConfigs: [link1, link1, link1, link2], addedComponentConfigs: [] },
|
||||
{
|
||||
pluginId: 'my-plugin',
|
||||
addedComponentConfigs: [],
|
||||
extensionConfigs: [
|
||||
{ ...link1, path: '/a/my-plugin/declare-incident' },
|
||||
{ ...link1, path: '/a/my-plugin/declare-incident' },
|
||||
@ -118,7 +145,11 @@ describe('getPluginExtensions()', () => {
|
||||
]);
|
||||
|
||||
// Limit to 1 extension per plugin
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1, limitPerPlugin: 1 });
|
||||
const { extensions } = getPluginExtensions({
|
||||
...registries,
|
||||
extensionPointId: extensionPoint1,
|
||||
limitPerPlugin: 1,
|
||||
});
|
||||
|
||||
expect(extensions).toHaveLength(2);
|
||||
expect(extensions[0]).toEqual(
|
||||
@ -133,17 +164,22 @@ describe('getPluginExtensions()', () => {
|
||||
});
|
||||
|
||||
test('should return with an empty list if there are no extensions registered for a placement yet', async () => {
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: 'placement-with-no-extensions' });
|
||||
const registries = await createRegistries([
|
||||
{ pluginId, extensionConfigs: [link1, link2], addedComponentConfigs: [] },
|
||||
]);
|
||||
const { extensions } = getPluginExtensions({
|
||||
...registries,
|
||||
extensionPointId: 'placement-with-no-extensions',
|
||||
});
|
||||
|
||||
expect(extensions).toEqual([]);
|
||||
});
|
||||
|
||||
test('should pass the context to the configure() function', async () => {
|
||||
const context = { title: 'New title from the context!' };
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
|
||||
getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 });
|
||||
getPluginExtensions({ ...registries, context, extensionPointId: extensionPoint2 });
|
||||
|
||||
expect(link2.configure).toHaveBeenCalledTimes(1);
|
||||
expect(link2.configure).toHaveBeenCalledWith(context);
|
||||
@ -158,8 +194,11 @@ describe('getPluginExtensions()', () => {
|
||||
category: 'Machine Learning',
|
||||
}));
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
const { extensions } = getPluginExtensions({
|
||||
...registries,
|
||||
extensionPointId: extensionPoint2,
|
||||
});
|
||||
const [extension] = extensions;
|
||||
|
||||
assertPluginExtensionLink(extension);
|
||||
@ -181,8 +220,11 @@ describe('getPluginExtensions()', () => {
|
||||
category: 'Machine Learning',
|
||||
}));
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
const { extensions } = getPluginExtensions({
|
||||
...registries,
|
||||
extensionPointId: extensionPoint2,
|
||||
});
|
||||
const [extension] = extensions;
|
||||
|
||||
assertPluginExtensionLink(extension);
|
||||
@ -206,8 +248,11 @@ describe('getPluginExtensions()', () => {
|
||||
title: 'test',
|
||||
}));
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
const { extensions } = getPluginExtensions({
|
||||
...registries,
|
||||
extensionPointId: extensionPoint2,
|
||||
});
|
||||
const [extension] = extensions;
|
||||
|
||||
expect(link2.configure).toHaveBeenCalledTimes(1);
|
||||
@ -220,8 +265,12 @@ describe('getPluginExtensions()', () => {
|
||||
});
|
||||
test('should pass a read only context to the configure() function', async () => {
|
||||
const context = { title: 'New title from the context!' };
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
const { extensions } = getPluginExtensions({
|
||||
...registries,
|
||||
context,
|
||||
extensionPointId: extensionPoint2,
|
||||
});
|
||||
const [extension] = extensions;
|
||||
const readOnlyContext = (link2.configure as jest.Mock).mock.calls[0][0];
|
||||
|
||||
@ -240,10 +289,10 @@ describe('getPluginExtensions()', () => {
|
||||
throw new Error('Something went wrong!');
|
||||
});
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
|
||||
expect(() => {
|
||||
getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
||||
getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(link2.configure).toHaveBeenCalledTimes(1);
|
||||
@ -259,9 +308,17 @@ describe('getPluginExtensions()', () => {
|
||||
path: 'invalid-path',
|
||||
}));
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]);
|
||||
const { extensions: extensionsAtPlacement1 } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 });
|
||||
const { extensions: extensionsAtPlacement2 } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([
|
||||
{ pluginId, extensionConfigs: [link1, link2], addedComponentConfigs: [] },
|
||||
]);
|
||||
const { extensions: extensionsAtPlacement1 } = getPluginExtensions({
|
||||
...registries,
|
||||
extensionPointId: extensionPoint1,
|
||||
});
|
||||
const { extensions: extensionsAtPlacement2 } = getPluginExtensions({
|
||||
...registries,
|
||||
extensionPointId: extensionPoint2,
|
||||
});
|
||||
|
||||
expect(extensionsAtPlacement1).toHaveLength(0);
|
||||
expect(extensionsAtPlacement2).toHaveLength(0);
|
||||
@ -279,8 +336,8 @@ describe('getPluginExtensions()', () => {
|
||||
|
||||
link2.configure = jest.fn().mockImplementation(() => overrides);
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||
|
||||
expect(extensions).toHaveLength(0);
|
||||
expect(link2.configure).toHaveBeenCalledTimes(1);
|
||||
@ -290,8 +347,8 @@ describe('getPluginExtensions()', () => {
|
||||
test('should skip the extension if the configure() function returns a promise', async () => {
|
||||
link2.configure = jest.fn().mockImplementation(() => Promise.resolve({}));
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||
|
||||
expect(extensions).toHaveLength(0);
|
||||
expect(link2.configure).toHaveBeenCalledTimes(1);
|
||||
@ -301,8 +358,8 @@ describe('getPluginExtensions()', () => {
|
||||
test('should skip (hide) the extension if the configure() function returns undefined', async () => {
|
||||
link2.configure = jest.fn().mockImplementation(() => undefined);
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||
|
||||
expect(extensions).toHaveLength(0);
|
||||
expect(global.console.warn).toHaveBeenCalledTimes(0); // As this is intentional, no warning should be logged
|
||||
@ -315,8 +372,8 @@ describe('getPluginExtensions()', () => {
|
||||
});
|
||||
|
||||
const context = {};
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||
const [extension] = extensions;
|
||||
|
||||
assertPluginExtensionLink(extension);
|
||||
@ -338,8 +395,8 @@ describe('getPluginExtensions()', () => {
|
||||
link2.path = undefined;
|
||||
link2.onClick = jest.fn().mockRejectedValue(new Error('testing'));
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||
const [extension] = extensions;
|
||||
|
||||
assertPluginExtensionLink(extension);
|
||||
@ -357,8 +414,8 @@ describe('getPluginExtensions()', () => {
|
||||
throw new Error('Something went wrong!');
|
||||
});
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||
const [extension] = extensions;
|
||||
|
||||
assertPluginExtensionLink(extension);
|
||||
@ -375,8 +432,8 @@ describe('getPluginExtensions()', () => {
|
||||
link2.path = undefined;
|
||||
link2.onClick = jest.fn();
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
const { extensions } = getPluginExtensions({ ...registries, context, extensionPointId: extensionPoint2 });
|
||||
const [extension] = extensions;
|
||||
|
||||
assertPluginExtensionLink(extension);
|
||||
@ -398,8 +455,8 @@ describe('getPluginExtensions()', () => {
|
||||
array: ['a'],
|
||||
};
|
||||
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
||||
getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 });
|
||||
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||
getPluginExtensions({ ...registries, context, extensionPointId: extensionPoint2 });
|
||||
|
||||
expect(() => {
|
||||
context.title = 'Updating the title';
|
||||
@ -411,7 +468,7 @@ describe('getPluginExtensions()', () => {
|
||||
test('should report interaction when onClick is triggered', async () => {
|
||||
const reportInteractionMock = jest.mocked(reportInteraction);
|
||||
|
||||
const registry = await createPluginExtensionRegistry([
|
||||
const registries = await createRegistries([
|
||||
{
|
||||
pluginId,
|
||||
extensionConfigs: [
|
||||
@ -421,9 +478,10 @@ describe('getPluginExtensions()', () => {
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
],
|
||||
addedComponentConfigs: [],
|
||||
},
|
||||
]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 });
|
||||
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint1 });
|
||||
const [extension] = extensions;
|
||||
|
||||
assertPluginExtensionLink(extension);
|
||||
@ -440,17 +498,65 @@ describe('getPluginExtensions()', () => {
|
||||
});
|
||||
|
||||
test('should be possible to register and get component type extensions', async () => {
|
||||
const extension = component1;
|
||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [extension] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extension.extensionPointId });
|
||||
const registries = await createRegistries([
|
||||
{
|
||||
pluginId,
|
||||
extensionConfigs: [],
|
||||
addedComponentConfigs: [
|
||||
{
|
||||
...component1,
|
||||
targets: component1.extensionPointId,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: component1.extensionPointId });
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
pluginId,
|
||||
type: PluginExtensionTypes.component,
|
||||
title: extension.title,
|
||||
description: extension.description,
|
||||
title: component1.title,
|
||||
description: component1.description,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should honour the limitPerPlugin also for component extensions', async () => {
|
||||
const registries = await createRegistries([
|
||||
{
|
||||
pluginId,
|
||||
extensionConfigs: [],
|
||||
addedComponentConfigs: [
|
||||
{
|
||||
...component1,
|
||||
targets: component1.extensionPointId,
|
||||
},
|
||||
{
|
||||
title: 'Component 2',
|
||||
description: 'Component 2 description',
|
||||
targets: component1.extensionPointId,
|
||||
component: (context) => {
|
||||
return <div>Hello world2!</div>;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const { extensions } = getPluginExtensions({
|
||||
...registries,
|
||||
limitPerPlugin: 1,
|
||||
extensionPointId: component1.extensionPointId,
|
||||
});
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
pluginId,
|
||||
type: PluginExtensionTypes.component,
|
||||
title: component1.title,
|
||||
description: component1.description,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -11,38 +11,38 @@ import {
|
||||
import { GetPluginExtensions, reportInteraction } from '@grafana/runtime';
|
||||
|
||||
import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry';
|
||||
import type { PluginExtensionRegistry } from './types';
|
||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||
import type { AddedComponentsRegistryState, PluginExtensionRegistry } from './types';
|
||||
import {
|
||||
isPluginExtensionLinkConfig,
|
||||
getReadOnlyProxy,
|
||||
logWarning,
|
||||
generateExtensionId,
|
||||
getEventHelpers,
|
||||
isPluginExtensionComponentConfig,
|
||||
wrapWithPluginContext,
|
||||
} from './utils';
|
||||
import {
|
||||
assertIsReactComponent,
|
||||
assertIsNotPromise,
|
||||
assertLinkPathIsValid,
|
||||
assertStringProps,
|
||||
isPromise,
|
||||
} from './validators';
|
||||
import { assertIsNotPromise, assertLinkPathIsValid, assertStringProps, isPromise } from './validators';
|
||||
|
||||
type GetExtensions = ({
|
||||
context,
|
||||
extensionPointId,
|
||||
limitPerPlugin,
|
||||
registry,
|
||||
addedComponentsRegistry,
|
||||
}: {
|
||||
context?: object | Record<string | symbol, unknown>;
|
||||
extensionPointId: string;
|
||||
limitPerPlugin?: number;
|
||||
registry: PluginExtensionRegistry;
|
||||
addedComponentsRegistry: AddedComponentsRegistryState;
|
||||
}) => { extensions: PluginExtension[] };
|
||||
|
||||
export function createPluginExtensionsGetter(extensionRegistry: ReactivePluginExtensionsRegistry): GetPluginExtensions {
|
||||
export function createPluginExtensionsGetter(
|
||||
extensionRegistry: ReactivePluginExtensionsRegistry,
|
||||
addedComponentRegistry: AddedComponentsRegistry
|
||||
): GetPluginExtensions {
|
||||
let registry: PluginExtensionRegistry = { id: '', extensions: {} };
|
||||
let addedComponentsRegistryState: AddedComponentsRegistryState = {};
|
||||
|
||||
// Create a subscription to keep an copy of the registry state for use in the non-async
|
||||
// plugin extensions getter.
|
||||
@ -50,11 +50,22 @@ export function createPluginExtensionsGetter(extensionRegistry: ReactivePluginEx
|
||||
registry = r;
|
||||
});
|
||||
|
||||
return (options) => getPluginExtensions({ ...options, registry });
|
||||
addedComponentRegistry.asObservable().subscribe((r) => {
|
||||
addedComponentsRegistryState = r;
|
||||
});
|
||||
|
||||
return (options) =>
|
||||
getPluginExtensions({ ...options, registry, addedComponentsRegistry: addedComponentsRegistryState });
|
||||
}
|
||||
|
||||
// Returns with a list of plugin extensions for the given extension point
|
||||
export const getPluginExtensions: GetExtensions = ({ context, extensionPointId, limitPerPlugin, registry }) => {
|
||||
export const getPluginExtensions: GetExtensions = ({
|
||||
context,
|
||||
extensionPointId,
|
||||
limitPerPlugin,
|
||||
registry,
|
||||
addedComponentsRegistry,
|
||||
}) => {
|
||||
const frozenContext = context ? getReadOnlyProxy(context) : {};
|
||||
const registryItems = registry.extensions[extensionPointId] ?? [];
|
||||
// We don't return the extensions separated by type, because in that case it would be much harder to define a sort-order for them.
|
||||
@ -103,23 +114,40 @@ export const getPluginExtensions: GetExtensions = ({ context, extensionPointId,
|
||||
extensions.push(extension);
|
||||
extensionsByPlugin[pluginId] += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logWarning(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// COMPONENT
|
||||
if (isPluginExtensionComponentConfig(extensionConfig)) {
|
||||
assertIsReactComponent(extensionConfig.component);
|
||||
if (extensionPointId in addedComponentsRegistry) {
|
||||
try {
|
||||
const addedComponents = addedComponentsRegistry[extensionPointId];
|
||||
for (const addedComponent of addedComponents) {
|
||||
// Only limit if the `limitPerPlugin` is set
|
||||
if (limitPerPlugin && extensionsByPlugin[addedComponent.pluginId] >= limitPerPlugin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (extensionsByPlugin[addedComponent.pluginId] === undefined) {
|
||||
extensionsByPlugin[addedComponent.pluginId] = 0;
|
||||
}
|
||||
const extension: PluginExtensionComponent = {
|
||||
id: generateExtensionId(registryItem.pluginId, extensionConfig),
|
||||
id: generateExtensionId(addedComponent.pluginId, {
|
||||
...addedComponent,
|
||||
extensionPointId,
|
||||
type: PluginExtensionTypes.component,
|
||||
}),
|
||||
type: PluginExtensionTypes.component,
|
||||
pluginId: registryItem.pluginId,
|
||||
|
||||
title: extensionConfig.title,
|
||||
description: extensionConfig.description,
|
||||
component: wrapWithPluginContext(pluginId, extensionConfig.component),
|
||||
pluginId: addedComponent.pluginId,
|
||||
title: addedComponent.title,
|
||||
description: addedComponent.description,
|
||||
component: wrapWithPluginContext(addedComponent.pluginId, addedComponent.component),
|
||||
};
|
||||
|
||||
extensions.push(extension);
|
||||
extensionsByPlugin[pluginId] += 1;
|
||||
extensionsByPlugin[addedComponent.pluginId] += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
|
@ -40,6 +40,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const registry = await reactiveRegistry.getRegistry();
|
||||
@ -66,6 +67,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const registry1 = await reactiveRegistry.getRegistry();
|
||||
@ -86,6 +88,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const registry2 = await reactiveRegistry.getRegistry();
|
||||
@ -120,6 +123,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const registry = await reactiveRegistry.getRegistry();
|
||||
@ -173,6 +177,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const registry1 = await reactiveRegistry.getRegistry();
|
||||
@ -207,6 +212,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const registry2 = await reactiveRegistry.getRegistry();
|
||||
@ -258,6 +264,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const registry1 = await reactiveRegistry.getRegistry();
|
||||
@ -292,6 +299,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const registry2 = await reactiveRegistry.getRegistry();
|
||||
@ -344,6 +352,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
// Register extensions to a different extension point
|
||||
@ -360,6 +369,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const registry2 = await reactiveRegistry.getRegistry();
|
||||
@ -410,6 +420,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
// Register extensions to a different extension point
|
||||
@ -426,6 +437,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const registry2 = await reactiveRegistry.getRegistry();
|
||||
@ -482,6 +494,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||
@ -500,6 +513,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
expect(subscribeCallback).toHaveBeenCalledTimes(3);
|
||||
@ -553,6 +567,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
observable.subscribe(subscribeCallback);
|
||||
@ -597,6 +612,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
expect(consoleWarn).toHaveBeenCalled();
|
||||
@ -657,6 +673,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
expect(consoleWarn).toHaveBeenCalled();
|
||||
@ -687,6 +704,7 @@ describe('createPluginExtensionsRegistry', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
expect(consoleWarn).toHaveBeenCalled();
|
||||
|
@ -0,0 +1,380 @@
|
||||
import React from 'react';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
||||
|
||||
describe('AddedComponentsRegistry', () => {
|
||||
const consoleWarn = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
global.console.warn = consoleWarn;
|
||||
consoleWarn.mockReset();
|
||||
});
|
||||
|
||||
it('should return empty registry when no extensions registered', async () => {
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const registry = await firstValueFrom(observable);
|
||||
expect(registry).toEqual({});
|
||||
});
|
||||
|
||||
it('should be possible to register added components in the registry', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const id = `${pluginId}/hello-world/v1`;
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
|
||||
reactiveRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
targets: [id],
|
||||
title: 'not important',
|
||||
description: 'not important',
|
||||
component: () => React.createElement('div', null, 'Hello World'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const registry = await reactiveRegistry.getState();
|
||||
|
||||
expect(Object.keys(registry)).toHaveLength(1);
|
||||
expect(registry[id][0]).toMatchObject({
|
||||
pluginId,
|
||||
title: 'not important',
|
||||
description: 'not important',
|
||||
});
|
||||
});
|
||||
it('should be possible to asynchronously register component extensions for the same extension point (different plugins)', async () => {
|
||||
const pluginId1 = 'grafana-basic-app';
|
||||
const pluginId2 = 'grafana-basic-app2';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
|
||||
// Register extensions for the first plugin
|
||||
reactiveRegistry.register({
|
||||
pluginId: pluginId1,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const registry1 = await reactiveRegistry.getState();
|
||||
expect(Object.keys(registry1)).toHaveLength(1);
|
||||
expect(registry1['grafana/alerting/home'][0]).toMatchObject({
|
||||
pluginId: pluginId1,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
});
|
||||
|
||||
// Register an extension component for the second plugin to the same extension point
|
||||
reactiveRegistry.register({
|
||||
pluginId: pluginId2,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const registry2 = await reactiveRegistry.getState();
|
||||
expect(Object.keys(registry2)).toHaveLength(1);
|
||||
expect(registry2['grafana/alerting/home']).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId1,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId2,
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should be possible to asynchronously register component extensions for a different extension points (different plugin)', async () => {
|
||||
const pluginId1 = 'grafana-basic-app';
|
||||
const pluginId2 = 'grafana-basic-app2';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
|
||||
// Register extensions for the first plugin
|
||||
reactiveRegistry.register({
|
||||
pluginId: pluginId1,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const registry1 = await reactiveRegistry.getState();
|
||||
expect(registry1).toEqual({
|
||||
'grafana/alerting/home': expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId1,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
// Register an extension component for the second plugin to a different extension point
|
||||
reactiveRegistry.register({
|
||||
pluginId: pluginId2,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
targets: ['grafana/user/profile/tab'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const registry2 = await reactiveRegistry.getState();
|
||||
|
||||
expect(registry2).toEqual({
|
||||
'grafana/alerting/home': expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId1,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
}),
|
||||
]),
|
||||
'grafana/user/profile/tab': expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId2,
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should be possible to asynchronously register component extensions for the same extension point (same plugin)', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
|
||||
// Register extensions for the first extension point
|
||||
reactiveRegistry.register({
|
||||
pluginId: pluginId,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
{
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World2'),
|
||||
},
|
||||
],
|
||||
});
|
||||
const registry1 = await reactiveRegistry.getState();
|
||||
expect(registry1).toEqual({
|
||||
'grafana/alerting/home': expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId,
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should be possible to register one extension component targeting multiple extension points', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
|
||||
reactiveRegistry.register({
|
||||
pluginId: pluginId,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['grafana/alerting/home', 'grafana/user/profile/tab'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
const registry1 = await reactiveRegistry.getState();
|
||||
expect(registry1).toEqual({
|
||||
'grafana/alerting/home': expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
}),
|
||||
]),
|
||||
'grafana/user/profile/tab': expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should notify subscribers when the registry changes', async () => {
|
||||
const pluginId1 = 'grafana-basic-app';
|
||||
const pluginId2 = 'another-plugin';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
observable.subscribe(subscribeCallback);
|
||||
|
||||
reactiveRegistry.register({
|
||||
pluginId: pluginId1,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||
|
||||
reactiveRegistry.register({
|
||||
pluginId: pluginId2,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
targets: ['grafana/user/profile/tab'],
|
||||
component: () => React.createElement('div', null, 'Hello World2'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(subscribeCallback).toHaveBeenCalledTimes(3);
|
||||
|
||||
const registry = subscribeCallback.mock.calls[2][0];
|
||||
|
||||
expect(registry).toEqual({
|
||||
'grafana/alerting/home': expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId1,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
}),
|
||||
]),
|
||||
'grafana/user/profile/tab': expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId2,
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip registering component and log a warning when id is not prefixed with plugin id or grafana', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
registry.register({
|
||||
pluginId: 'grafana-basic-app',
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(consoleWarn).toHaveBeenCalledWith(
|
||||
"[Plugin Extensions] Could not register added component with id 'alerting/home'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id or grafana. e.g '<grafana|myorg-basic-app>/my-component-id/v1'."
|
||||
);
|
||||
const currentState = await registry.getState();
|
||||
expect(Object.keys(currentState)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should log a warning when exposed component id is not suffixed with component version', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
registry.register({
|
||||
pluginId: 'grafana-basic-app',
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(consoleWarn).toHaveBeenCalledWith(
|
||||
"[Plugin Extensions] Added component with id 'grafana/alerting/home' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'."
|
||||
);
|
||||
const currentState = await registry.getState();
|
||||
expect(Object.keys(currentState)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not register component when description is missing', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
registry.register({
|
||||
pluginId: 'grafana-basic-app',
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: '',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(consoleWarn).toHaveBeenCalledWith(
|
||||
"[Plugin Extensions] Could not register added component with title 'Component 1 title'. Reason: Description is missing."
|
||||
);
|
||||
const currentState = await registry.getState();
|
||||
expect(Object.keys(currentState)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not register component when title is missing', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
registry.register({
|
||||
pluginId: 'grafana-basic-app',
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: '',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(consoleWarn).toHaveBeenCalledWith(
|
||||
"[Plugin Extensions] Could not register added component with title 'Component 1 title'. Reason: Description is missing."
|
||||
);
|
||||
|
||||
const currentState = await registry.getState();
|
||||
expect(Object.keys(currentState)).toHaveLength(0);
|
||||
});
|
||||
});
|
@ -0,0 +1,78 @@
|
||||
import { PluginAddedComponentConfig } from '@grafana/data';
|
||||
|
||||
import { logWarning, wrapWithPluginContext } from '../utils';
|
||||
import { extensionPointEndsWithVersion, isExtensionPointIdValid, isReactComponent } from '../validators';
|
||||
|
||||
import { PluginExtensionConfigs, Registry, RegistryType } from './Registry';
|
||||
|
||||
export type AddedComponentRegistryItem<Props = {}> = {
|
||||
pluginId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
component: React.ComponentType<Props>;
|
||||
};
|
||||
|
||||
export class AddedComponentsRegistry extends Registry<AddedComponentRegistryItem[], PluginAddedComponentConfig> {
|
||||
constructor(initialState: RegistryType<AddedComponentRegistryItem[]> = {}) {
|
||||
super({
|
||||
initialState,
|
||||
});
|
||||
}
|
||||
|
||||
mapToRegistry(
|
||||
registry: RegistryType<AddedComponentRegistryItem[]>,
|
||||
item: PluginExtensionConfigs<PluginAddedComponentConfig>
|
||||
): RegistryType<AddedComponentRegistryItem[]> {
|
||||
const { pluginId, configs } = item;
|
||||
|
||||
for (const config of configs) {
|
||||
if (!isReactComponent(config.component)) {
|
||||
logWarning(
|
||||
`Could not register added component with title '${config.title}'. Reason: The provided component is not a valid React component.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!config.title) {
|
||||
logWarning(`Could not register added component with title '${config.title}'. Reason: Title is missing.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!config.description) {
|
||||
logWarning(`Could not register added component with title '${config.title}'. Reason: Description is missing.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionPointIds = Array.isArray(config.targets) ? config.targets : [config.targets];
|
||||
for (const extensionPointId of extensionPointIds) {
|
||||
if (!isExtensionPointIdValid(pluginId, extensionPointId)) {
|
||||
logWarning(
|
||||
`Could not register added component with id '${extensionPointId}'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id or grafana. e.g '<grafana|myorg-basic-app>/my-component-id/v1'.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!extensionPointEndsWithVersion(extensionPointId)) {
|
||||
logWarning(
|
||||
`Added component with id '${extensionPointId}' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'.`
|
||||
);
|
||||
}
|
||||
|
||||
const result = {
|
||||
pluginId,
|
||||
component: wrapWithPluginContext(pluginId, config.component),
|
||||
description: config.description,
|
||||
title: config.title,
|
||||
};
|
||||
|
||||
if (!(extensionPointId in registry)) {
|
||||
registry[extensionPointId] = [result];
|
||||
} else {
|
||||
registry[extensionPointId].push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
}
|
@ -40,11 +40,9 @@ describe('ExposedComponentsRegistry', () => {
|
||||
expect(Object.keys(registry)).toHaveLength(1);
|
||||
expect(registry[id]).toMatchObject({
|
||||
pluginId,
|
||||
config: {
|
||||
id,
|
||||
title: 'not important',
|
||||
description: 'not important',
|
||||
},
|
||||
id,
|
||||
title: 'not important',
|
||||
description: 'not important',
|
||||
});
|
||||
});
|
||||
|
||||
@ -82,9 +80,9 @@ describe('ExposedComponentsRegistry', () => {
|
||||
const registry = await reactiveRegistry.getState();
|
||||
|
||||
expect(Object.keys(registry)).toHaveLength(3);
|
||||
expect(registry[id1]).toMatchObject({ config: { id: id1 }, pluginId });
|
||||
expect(registry[id2]).toMatchObject({ config: { id: id2 }, pluginId });
|
||||
expect(registry[id3]).toMatchObject({ config: { id: id3 }, pluginId });
|
||||
expect(registry[id1]).toMatchObject({ id: id1, pluginId });
|
||||
expect(registry[id2]).toMatchObject({ id: id2, pluginId });
|
||||
expect(registry[id3]).toMatchObject({ id: id3, pluginId });
|
||||
});
|
||||
|
||||
it('should be possible to register multiple exposed components from multiple plugins', async () => {
|
||||
@ -135,10 +133,10 @@ describe('ExposedComponentsRegistry', () => {
|
||||
const registry = await reactiveRegistry.getState();
|
||||
|
||||
expect(Object.keys(registry)).toHaveLength(4);
|
||||
expect(registry[id1]).toMatchObject({ config: { id: id1 }, pluginId: pluginId1 });
|
||||
expect(registry[id2]).toMatchObject({ config: { id: id2 }, pluginId: pluginId1 });
|
||||
expect(registry[id3]).toMatchObject({ config: { id: id3 }, pluginId: pluginId2 });
|
||||
expect(registry[id4]).toMatchObject({ config: { id: id4 }, pluginId: pluginId2 });
|
||||
expect(registry[id1]).toMatchObject({ id: id1, pluginId: pluginId1 });
|
||||
expect(registry[id2]).toMatchObject({ id: id2, pluginId: pluginId1 });
|
||||
expect(registry[id3]).toMatchObject({ id: id3, pluginId: pluginId2 });
|
||||
expect(registry[id4]).toMatchObject({ id: id4, pluginId: pluginId2 });
|
||||
});
|
||||
|
||||
it('should notify subscribers when the registry changes', async () => {
|
||||
@ -208,11 +206,9 @@ describe('ExposedComponentsRegistry', () => {
|
||||
|
||||
expect(mock['grafana-basic-app/hello-world/v1']).toMatchObject({
|
||||
pluginId: 'grafana-basic-app',
|
||||
config: {
|
||||
id: 'grafana-basic-app/hello-world/v1',
|
||||
title: 'not important',
|
||||
description: 'not important',
|
||||
},
|
||||
id: 'grafana-basic-app/hello-world/v1',
|
||||
title: 'not important',
|
||||
description: 'not important',
|
||||
});
|
||||
});
|
||||
|
||||
@ -234,9 +230,7 @@ describe('ExposedComponentsRegistry', () => {
|
||||
expect(Object.keys(currentState1)).toHaveLength(1);
|
||||
expect(currentState1['grafana-basic-app1/hello-world/v1']).toMatchObject({
|
||||
pluginId: 'grafana-basic-app1',
|
||||
config: {
|
||||
id: 'grafana-basic-app1/hello-world/v1',
|
||||
},
|
||||
id: 'grafana-basic-app1/hello-world/v1',
|
||||
});
|
||||
|
||||
registry.register({
|
||||
|
@ -1,20 +1,28 @@
|
||||
import { PluginExposedComponentConfig } from '@grafana/data';
|
||||
|
||||
import { logWarning } from '../utils';
|
||||
import { extensionPointEndsWithVersion } from '../validators';
|
||||
|
||||
import { Registry, RegistryType, PluginExtensionConfigs } from './Registry';
|
||||
|
||||
export class ExposedComponentsRegistry extends Registry<PluginExposedComponentConfig> {
|
||||
constructor(initialState: RegistryType<PluginExposedComponentConfig> = {}) {
|
||||
export type ExposedComponentRegistryItem<Props = {}> = {
|
||||
pluginId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
component: React.ComponentType<Props>;
|
||||
};
|
||||
|
||||
export class ExposedComponentsRegistry extends Registry<ExposedComponentRegistryItem, PluginExposedComponentConfig> {
|
||||
constructor(initialState: RegistryType<ExposedComponentRegistryItem> = {}) {
|
||||
super({
|
||||
initialState,
|
||||
});
|
||||
}
|
||||
|
||||
mapToRegistry(
|
||||
registry: RegistryType<PluginExposedComponentConfig>,
|
||||
registry: RegistryType<ExposedComponentRegistryItem>,
|
||||
{ pluginId, configs }: PluginExtensionConfigs<PluginExposedComponentConfig>
|
||||
): RegistryType<PluginExposedComponentConfig> {
|
||||
): RegistryType<ExposedComponentRegistryItem> {
|
||||
if (!configs) {
|
||||
return registry;
|
||||
}
|
||||
@ -29,7 +37,7 @@ export class ExposedComponentsRegistry extends Registry<PluginExposedComponentCo
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!id.match(/.*\/v\d+$/)) {
|
||||
if (!extensionPointEndsWithVersion(id)) {
|
||||
logWarning(
|
||||
`Exposed component with id '${id}' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'.`
|
||||
);
|
||||
@ -52,7 +60,7 @@ export class ExposedComponentsRegistry extends Registry<PluginExposedComponentCo
|
||||
continue;
|
||||
}
|
||||
|
||||
registry[id] = { config, pluginId };
|
||||
registry[id] = { ...config, pluginId };
|
||||
}
|
||||
|
||||
return registry;
|
||||
|
@ -7,28 +7,23 @@ export type PluginExtensionConfigs<T> = {
|
||||
configs: T[];
|
||||
};
|
||||
|
||||
export type RegistryItem<T> = {
|
||||
pluginId: string;
|
||||
config: T;
|
||||
};
|
||||
|
||||
export type RegistryType<T> = Record<string | symbol, RegistryItem<T>>;
|
||||
export type RegistryType<T> = Record<string | symbol, T>;
|
||||
|
||||
type ConstructorOptions<T> = {
|
||||
initialState: RegistryType<T>;
|
||||
};
|
||||
|
||||
// This is the base-class used by the separate specific registries.
|
||||
export abstract class Registry<T> {
|
||||
private resultSubject: Subject<PluginExtensionConfigs<T>>;
|
||||
private registrySubject: ReplaySubject<RegistryType<T>>;
|
||||
export abstract class Registry<TRegistryValue, TMapType> {
|
||||
private resultSubject: Subject<PluginExtensionConfigs<TMapType>>;
|
||||
private registrySubject: ReplaySubject<RegistryType<TRegistryValue>>;
|
||||
|
||||
constructor(options: ConstructorOptions<T>) {
|
||||
constructor(options: ConstructorOptions<TRegistryValue>) {
|
||||
const { initialState } = options;
|
||||
this.resultSubject = new Subject<PluginExtensionConfigs<T>>();
|
||||
this.resultSubject = new Subject<PluginExtensionConfigs<TMapType>>();
|
||||
// This is the subject that we expose.
|
||||
// (It will buffer the last value on the stream - the registry - and emit it to new subscribers immediately.)
|
||||
this.registrySubject = new ReplaySubject<RegistryType<T>>(1);
|
||||
this.registrySubject = new ReplaySubject<RegistryType<TRegistryValue>>(1);
|
||||
|
||||
this.resultSubject
|
||||
.pipe(
|
||||
@ -41,17 +36,20 @@ export abstract class Registry<T> {
|
||||
.subscribe(this.registrySubject);
|
||||
}
|
||||
|
||||
abstract mapToRegistry(registry: RegistryType<T>, item: PluginExtensionConfigs<T>): RegistryType<T>;
|
||||
abstract mapToRegistry(
|
||||
registry: RegistryType<TRegistryValue>,
|
||||
item: PluginExtensionConfigs<TMapType>
|
||||
): RegistryType<TRegistryValue>;
|
||||
|
||||
register(result: PluginExtensionConfigs<T>): void {
|
||||
register(result: PluginExtensionConfigs<TMapType>): void {
|
||||
this.resultSubject.next(result);
|
||||
}
|
||||
|
||||
asObservable(): Observable<RegistryType<T>> {
|
||||
asObservable(): Observable<RegistryType<TRegistryValue>> {
|
||||
return this.registrySubject.asObservable();
|
||||
}
|
||||
|
||||
getState(): Promise<RegistryType<T>> {
|
||||
getState(): Promise<RegistryType<TRegistryValue>> {
|
||||
return firstValueFrom(this.asObservable());
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import type { PluginExtensionConfig } from '@grafana/data';
|
||||
|
||||
import { AddedComponentRegistryItem } from './registry/AddedComponentsRegistry';
|
||||
import { RegistryType } from './registry/Registry';
|
||||
|
||||
// The information that is stored in the registry
|
||||
export type PluginExtensionRegistryItem = {
|
||||
// Any additional meta information that we would like to store about the extension in the registry
|
||||
@ -13,3 +16,5 @@ export type PluginExtensionRegistry = {
|
||||
id: string;
|
||||
extensions: Record<string, PluginExtensionRegistryItem[]>;
|
||||
};
|
||||
|
||||
export type AddedComponentsRegistryState = RegistryType<Array<AddedComponentRegistryItem<{}>>>;
|
||||
|
@ -26,7 +26,7 @@ export function createUsePluginComponent(registry: ExposedComponentsRegistry) {
|
||||
|
||||
return {
|
||||
isLoading: false,
|
||||
component: wrapWithPluginContext(registryItem.pluginId, registryItem.config.component),
|
||||
component: wrapWithPluginContext(registryItem.pluginId, registryItem.component),
|
||||
};
|
||||
}, [id, registry]);
|
||||
};
|
||||
|
@ -0,0 +1,171 @@
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||
import { createUsePluginComponents } from './usePluginComponents';
|
||||
|
||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||
getPluginSettings: jest.fn().mockResolvedValue({
|
||||
id: 'my-app-plugin',
|
||||
enabled: true,
|
||||
jsonData: {},
|
||||
type: 'panel',
|
||||
name: 'My App Plugin',
|
||||
module: 'app/plugins/my-app-plugin/module',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('usePluginComponents()', () => {
|
||||
let registry: AddedComponentsRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new AddedComponentsRegistry();
|
||||
});
|
||||
|
||||
it('should return an empty array if there are no extensions registered for the extension point', () => {
|
||||
const usePluginComponents = createUsePluginComponents(registry);
|
||||
const { result } = renderHook(() =>
|
||||
usePluginComponents({
|
||||
extensionPointId: 'foo/bar',
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.components).toEqual([]);
|
||||
});
|
||||
|
||||
it('should only return the plugin extension components for the given extension point ids', async () => {
|
||||
const extensionPointId = 'plugins/foo/bar/v1';
|
||||
const pluginId = 'my-app-plugin';
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
targets: extensionPointId,
|
||||
title: '1',
|
||||
description: '1',
|
||||
component: () => <div>Hello World1</div>,
|
||||
},
|
||||
{
|
||||
targets: extensionPointId,
|
||||
title: '2',
|
||||
description: '2',
|
||||
component: () => <div>Hello World2</div>,
|
||||
},
|
||||
{
|
||||
targets: 'plugins/another-extension/v1',
|
||||
title: '3',
|
||||
description: '3',
|
||||
component: () => <div>Hello World3</div>,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const usePluginComponents = createUsePluginComponents(registry);
|
||||
const { result } = renderHook(() => usePluginComponents({ extensionPointId }));
|
||||
|
||||
expect(result.current.components.length).toBe(2);
|
||||
|
||||
act(() => {
|
||||
render(result.current.components.map((Component, index) => <Component key={index} />));
|
||||
});
|
||||
expect(await screen.findByText('Hello World1')).toBeVisible();
|
||||
expect(await screen.findByText('Hello World2')).toBeVisible();
|
||||
expect(await screen.queryByText('Hello World3')).toBeNull();
|
||||
});
|
||||
|
||||
it('should dynamically update the extensions registered for a certain extension point', () => {
|
||||
const extensionPointId = 'plugins/foo/bar/v1';
|
||||
const pluginId = 'my-app-plugin';
|
||||
const usePluginComponents = createUsePluginComponents(registry);
|
||||
let { result, rerender } = renderHook(() => usePluginComponents({ extensionPointId }));
|
||||
|
||||
// No extensions yet
|
||||
expect(result.current.components.length).toBe(0);
|
||||
|
||||
// Add extensions to the registry
|
||||
act(() => {
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
targets: extensionPointId,
|
||||
title: '1',
|
||||
description: '1',
|
||||
component: () => <div>Hello World1</div>,
|
||||
},
|
||||
{
|
||||
targets: extensionPointId,
|
||||
title: '2',
|
||||
description: '2',
|
||||
component: () => <div>Hello World2</div>,
|
||||
},
|
||||
{
|
||||
targets: 'plugins/another-extension/v1',
|
||||
title: '3',
|
||||
description: '3',
|
||||
component: () => <div>Hello World3</div>,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// Check if the hook returns the new extensions
|
||||
rerender();
|
||||
|
||||
expect(result.current.components.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should only render the hook once', () => {
|
||||
const spy = jest.spyOn(registry, 'asObservable');
|
||||
const extensionPointId = 'plugins/foo/bar';
|
||||
const usePluginComponents = createUsePluginComponents(registry);
|
||||
|
||||
renderHook(() => usePluginComponents({ extensionPointId }));
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should honour the limitPerPlugin arg if its set', () => {
|
||||
const extensionPointId = 'plugins/foo/bar/v1';
|
||||
const plugins = ['my-app-plugin1', 'my-app-plugin2', 'my-app-plugin3'];
|
||||
const usePluginComponents = createUsePluginComponents(registry);
|
||||
let { result, rerender } = renderHook(() => usePluginComponents({ extensionPointId, limitPerPlugin: 2 }));
|
||||
|
||||
// No extensions yet
|
||||
expect(result.current.components.length).toBe(0);
|
||||
|
||||
// Add extensions to the registry
|
||||
act(() => {
|
||||
for (let pluginId of plugins) {
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
targets: extensionPointId,
|
||||
title: '1',
|
||||
description: '1',
|
||||
component: () => <div>Hello World1</div>,
|
||||
},
|
||||
{
|
||||
targets: extensionPointId,
|
||||
title: '2',
|
||||
description: '2',
|
||||
component: () => <div>Hello World2</div>,
|
||||
},
|
||||
{
|
||||
targets: extensionPointId,
|
||||
title: '3',
|
||||
description: '3',
|
||||
component: () => <div>Hello World3</div>,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Check if the hook returns the new extensions
|
||||
rerender();
|
||||
|
||||
expect(result.current.components.length).toBe(6);
|
||||
});
|
||||
});
|
@ -0,0 +1,53 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
|
||||
import {
|
||||
UsePluginComponentOptions,
|
||||
UsePluginComponentsResult,
|
||||
} from '@grafana/runtime/src/services/pluginExtensions/getPluginExtensions';
|
||||
|
||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||
|
||||
// Returns an array of component extensions for the given extension point
|
||||
export function createUsePluginComponents(registry: AddedComponentsRegistry) {
|
||||
const observableRegistry = registry.asObservable();
|
||||
|
||||
return function usePluginComponents<Props extends object = {}>({
|
||||
limitPerPlugin,
|
||||
extensionPointId,
|
||||
}: UsePluginComponentOptions): UsePluginComponentsResult<Props> {
|
||||
const registry = useObservable(observableRegistry);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!registry || !registry[extensionPointId]) {
|
||||
return {
|
||||
isLoading: false,
|
||||
components: [],
|
||||
};
|
||||
}
|
||||
const components: Array<React.ComponentType<Props>> = [];
|
||||
const registryItems = registry[extensionPointId];
|
||||
const extensionsByPlugin: Record<string, number> = {};
|
||||
for (const registryItem of registryItems) {
|
||||
const { pluginId } = registryItem;
|
||||
|
||||
// Only limit if the `limitPerPlugin` is set
|
||||
if (limitPerPlugin && extensionsByPlugin[pluginId] >= limitPerPlugin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (extensionsByPlugin[pluginId] === undefined) {
|
||||
extensionsByPlugin[pluginId] = 0;
|
||||
}
|
||||
|
||||
components.push(registryItem.component as React.ComponentType<Props>);
|
||||
extensionsByPlugin[pluginId] += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading: false,
|
||||
components,
|
||||
};
|
||||
}, [extensionPointId, limitPerPlugin, registry]);
|
||||
};
|
||||
}
|
@ -4,17 +4,20 @@ import { renderHook } from '@testing-library/react-hooks';
|
||||
import { PluginExtensionTypes } from '@grafana/data';
|
||||
|
||||
import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry';
|
||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||
import { createUsePluginExtensions } from './usePluginExtensions';
|
||||
|
||||
describe('usePluginExtensions()', () => {
|
||||
let reactiveRegistry: ReactivePluginExtensionsRegistry;
|
||||
let addedComponentsRegistry: AddedComponentsRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
reactiveRegistry = new ReactivePluginExtensionsRegistry();
|
||||
addedComponentsRegistry = new AddedComponentsRegistry();
|
||||
});
|
||||
|
||||
it('should return an empty array if there are no extensions registered for the extension point', () => {
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||
const { result } = renderHook(() =>
|
||||
usePluginExtensions({
|
||||
extensionPointId: 'foo/bar',
|
||||
@ -24,7 +27,7 @@ describe('usePluginExtensions()', () => {
|
||||
expect(result.current.extensions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return the plugin extensions from the registry', () => {
|
||||
it('should return the plugin link extensions from the registry', () => {
|
||||
const extensionPointId = 'plugins/foo/bar';
|
||||
const pluginId = 'my-app-plugin';
|
||||
|
||||
@ -47,9 +50,10 @@ describe('usePluginExtensions()', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||
const { result } = renderHook(() => usePluginExtensions({ extensionPointId }));
|
||||
|
||||
expect(result.current.extensions.length).toBe(2);
|
||||
@ -57,10 +61,63 @@ describe('usePluginExtensions()', () => {
|
||||
expect(result.current.extensions[1].title).toBe('2');
|
||||
});
|
||||
|
||||
it('should return the plugin component extensions from the registry', () => {
|
||||
const linkExtensionPointId = 'plugins/foo/bar';
|
||||
const componentExtensionPointId = 'plugins/component/bar/v1';
|
||||
const pluginId = 'my-app-plugin';
|
||||
|
||||
reactiveRegistry.register({
|
||||
pluginId,
|
||||
extensionConfigs: [
|
||||
{
|
||||
type: PluginExtensionTypes.link,
|
||||
extensionPointId: linkExtensionPointId,
|
||||
title: '1',
|
||||
description: '1',
|
||||
path: `/a/${pluginId}/2`,
|
||||
},
|
||||
{
|
||||
type: PluginExtensionTypes.link,
|
||||
extensionPointId: linkExtensionPointId,
|
||||
title: '2',
|
||||
description: '2',
|
||||
path: `/a/${pluginId}/2`,
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
|
||||
addedComponentsRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
targets: componentExtensionPointId,
|
||||
title: 'Component 1',
|
||||
description: '1',
|
||||
component: () => <div>Hello World1</div>,
|
||||
},
|
||||
{
|
||||
targets: componentExtensionPointId,
|
||||
title: 'Component 2',
|
||||
description: '2',
|
||||
component: () => <div>Hello World2</div>,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||
const { result } = renderHook(() => usePluginExtensions({ extensionPointId: componentExtensionPointId }));
|
||||
|
||||
expect(result.current.extensions.length).toBe(2);
|
||||
expect(result.current.extensions[0].title).toBe('Component 1');
|
||||
expect(result.current.extensions[1].title).toBe('Component 2');
|
||||
});
|
||||
|
||||
it('should dynamically update the extensions registered for a certain extension point', () => {
|
||||
const extensionPointId = 'plugins/foo/bar';
|
||||
const pluginId = 'my-app-plugin';
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||
let { result, rerender } = renderHook(() => usePluginExtensions({ extensionPointId }));
|
||||
|
||||
// No extensions yet
|
||||
@ -87,6 +144,7 @@ describe('usePluginExtensions()', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
});
|
||||
|
||||
@ -101,7 +159,7 @@ describe('usePluginExtensions()', () => {
|
||||
it('should only render the hook once', () => {
|
||||
const spy = jest.spyOn(reactiveRegistry, 'asObservable');
|
||||
const extensionPointId = 'plugins/foo/bar';
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||
|
||||
renderHook(() => usePluginExtensions({ extensionPointId }));
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
@ -110,7 +168,7 @@ describe('usePluginExtensions()', () => {
|
||||
it('should return the same extensions object if the context object is the same', () => {
|
||||
const extensionPointId = 'plugins/foo/bar';
|
||||
const pluginId = 'my-app-plugin';
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||
|
||||
// Add extensions to the registry
|
||||
act(() => {
|
||||
@ -133,6 +191,7 @@ describe('usePluginExtensions()', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
});
|
||||
|
||||
@ -146,7 +205,7 @@ describe('usePluginExtensions()', () => {
|
||||
it('should return a new extensions object if the context object is different', () => {
|
||||
const extensionPointId = 'plugins/foo/bar';
|
||||
const pluginId = 'my-app-plugin';
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||
|
||||
// Add extensions to the registry
|
||||
act(() => {
|
||||
@ -169,6 +228,7 @@ describe('usePluginExtensions()', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
});
|
||||
|
||||
@ -182,7 +242,7 @@ describe('usePluginExtensions()', () => {
|
||||
const extensionPointId = 'plugins/foo/bar';
|
||||
const pluginId = 'my-app-plugin';
|
||||
const context = {};
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||
|
||||
// Add the first extension
|
||||
act(() => {
|
||||
@ -198,6 +258,7 @@ describe('usePluginExtensions()', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
});
|
||||
|
||||
@ -219,6 +280,7 @@ describe('usePluginExtensions()', () => {
|
||||
},
|
||||
],
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -5,9 +5,14 @@ import { GetPluginExtensionsOptions, UsePluginExtensionsResult } from '@grafana/
|
||||
|
||||
import { getPluginExtensions } from './getPluginExtensions';
|
||||
import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry';
|
||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||
|
||||
export function createUsePluginExtensions(extensionsRegistry: ReactivePluginExtensionsRegistry) {
|
||||
export function createUsePluginExtensions(
|
||||
extensionsRegistry: ReactivePluginExtensionsRegistry,
|
||||
addedComponentsRegistry: AddedComponentsRegistry
|
||||
) {
|
||||
const observableRegistry = extensionsRegistry.asObservable();
|
||||
const observableAddedComponentRegistry = addedComponentsRegistry.asObservable();
|
||||
const cache: {
|
||||
id: string;
|
||||
extensions: Record<string, { context: GetPluginExtensionsOptions['context']; extensions: PluginExtension[] }>;
|
||||
@ -18,8 +23,9 @@ export function createUsePluginExtensions(extensionsRegistry: ReactivePluginExte
|
||||
|
||||
return function usePluginExtensions(options: GetPluginExtensionsOptions): UsePluginExtensionsResult<PluginExtension> {
|
||||
const registry = useObservable(observableRegistry);
|
||||
const addedComponentsRegistry = useObservable(observableAddedComponentRegistry);
|
||||
|
||||
if (!registry) {
|
||||
if (!registry || !addedComponentsRegistry) {
|
||||
return { extensions: [], isLoading: false };
|
||||
}
|
||||
|
||||
@ -39,7 +45,11 @@ export function createUsePluginExtensions(extensionsRegistry: ReactivePluginExte
|
||||
};
|
||||
}
|
||||
|
||||
const { extensions } = getPluginExtensions({ ...options, registry });
|
||||
const { extensions } = getPluginExtensions({
|
||||
...options,
|
||||
registry,
|
||||
addedComponentsRegistry,
|
||||
});
|
||||
|
||||
cache.extensions[key] = {
|
||||
context: options.context,
|
||||
|
@ -5,7 +5,6 @@ import { useAsync } from 'react-use';
|
||||
|
||||
import {
|
||||
type PluginExtensionLinkConfig,
|
||||
type PluginExtensionComponentConfig,
|
||||
type PluginExtensionConfig,
|
||||
type PluginExtensionEventHelpers,
|
||||
PluginExtensionTypes,
|
||||
@ -31,12 +30,6 @@ export function isPluginExtensionLinkConfig(
|
||||
return typeof extension === 'object' && 'type' in extension && extension['type'] === PluginExtensionTypes.link;
|
||||
}
|
||||
|
||||
export function isPluginExtensionComponentConfig<Props extends object>(
|
||||
extension: PluginExtensionConfig | undefined | PluginExtensionComponentConfig<Props>
|
||||
): extension is PluginExtensionComponentConfig<Props> {
|
||||
return typeof extension === 'object' && 'type' in extension && extension['type'] === PluginExtensionTypes.component;
|
||||
}
|
||||
|
||||
export function handleErrorsInFn(fn: Function, errorMessagePrefix = '') {
|
||||
return (...args: unknown[]) => {
|
||||
try {
|
||||
|
@ -6,7 +6,7 @@ import type {
|
||||
} from '@grafana/data';
|
||||
import { isPluginExtensionLink } from '@grafana/runtime';
|
||||
|
||||
import { isPluginExtensionComponentConfig, isPluginExtensionLinkConfig, logWarning } from './utils';
|
||||
import { isPluginExtensionLinkConfig, logWarning } from './utils';
|
||||
|
||||
export function assertPluginExtensionLink(
|
||||
extension: PluginExtension | undefined,
|
||||
@ -41,7 +41,7 @@ export function assertIsReactComponent(component: React.ComponentType) {
|
||||
}
|
||||
|
||||
export function assertExtensionPointIdIsValid(pluginId: string, extension: PluginExtensionConfig) {
|
||||
if (!isExtensionPointIdValid(pluginId, extension)) {
|
||||
if (!isExtensionPointIdValid(pluginId, extension.extensionPointId)) {
|
||||
throw new Error(
|
||||
`Invalid extension "${extension.title}". The extensionPointId should start with either "grafana/", "plugins/" or "capabilities/${pluginId}" (currently: "${extension.extensionPointId}"). Skipping the extension.`
|
||||
);
|
||||
@ -76,14 +76,18 @@ export function isLinkPathValid(pluginId: string, path: string) {
|
||||
return Boolean(typeof path === 'string' && path.length > 0 && path.startsWith(`/a/${pluginId}/`));
|
||||
}
|
||||
|
||||
export function isExtensionPointIdValid(pluginId: string, extension: PluginExtensionConfig) {
|
||||
export function isExtensionPointIdValid(pluginId: string, extensionPointId: string) {
|
||||
return Boolean(
|
||||
extension.extensionPointId?.startsWith('grafana/') ||
|
||||
extension.extensionPointId?.startsWith('plugins/') ||
|
||||
extension.extensionPointId?.startsWith(`capabilities/${pluginId}/`)
|
||||
extensionPointId.startsWith('grafana/') ||
|
||||
extensionPointId?.startsWith('plugins/') ||
|
||||
extensionPointId?.startsWith(pluginId)
|
||||
);
|
||||
}
|
||||
|
||||
export function extensionPointEndsWithVersion(extensionPointId: string) {
|
||||
return extensionPointId.match(/.*\/v\d+$/);
|
||||
}
|
||||
|
||||
export function isConfigureFnValid(extension: PluginExtensionLinkConfig) {
|
||||
return extension.configure ? typeof extension.configure === 'function' : true;
|
||||
}
|
||||
@ -111,11 +115,6 @@ export function isPluginExtensionConfigValid(pluginId: string, extension: Plugin
|
||||
}
|
||||
}
|
||||
|
||||
// Component
|
||||
if (isPluginExtensionComponentConfig(extension)) {
|
||||
assertIsReactComponent(extension.component);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
|
@ -1,9 +1,11 @@
|
||||
import type { PluginExposedComponentConfig, PluginExtensionConfig } from '@grafana/data';
|
||||
import { PluginAddedComponentConfig } from '@grafana/data/src/types/pluginExtensions';
|
||||
import type { AppPluginConfig } from '@grafana/runtime';
|
||||
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
|
||||
import { ReactivePluginExtensionsRegistry } from './extensions/reactivePluginExtensionRegistry';
|
||||
import { AddedComponentsRegistry } from './extensions/registry/AddedComponentsRegistry';
|
||||
import { ExposedComponentsRegistry } from './extensions/registry/ExposedComponentsRegistry';
|
||||
import * as pluginLoader from './plugin_loader';
|
||||
|
||||
@ -12,12 +14,18 @@ export type PluginPreloadResult = {
|
||||
error?: unknown;
|
||||
extensionConfigs: PluginExtensionConfig[];
|
||||
exposedComponentConfigs: PluginExposedComponentConfig[];
|
||||
addedComponentConfigs: PluginAddedComponentConfig[];
|
||||
};
|
||||
|
||||
type PluginExtensionRegistries = {
|
||||
extensionsRegistry: ReactivePluginExtensionsRegistry;
|
||||
addedComponentsRegistry: AddedComponentsRegistry;
|
||||
exposedComponentsRegistry: ExposedComponentsRegistry;
|
||||
};
|
||||
|
||||
export async function preloadPlugins(
|
||||
apps: AppPluginConfig[] = [],
|
||||
registry: ReactivePluginExtensionsRegistry,
|
||||
exposedComponentsRegistry: ExposedComponentsRegistry,
|
||||
registries: PluginExtensionRegistries,
|
||||
eventName = 'frontend_plugins_preload'
|
||||
) {
|
||||
startMeasure(eventName);
|
||||
@ -30,12 +38,15 @@ export async function preloadPlugins(
|
||||
continue;
|
||||
}
|
||||
|
||||
registry.register(preloadedPlugin);
|
||||
|
||||
exposedComponentsRegistry.register({
|
||||
registries.extensionsRegistry.register(preloadedPlugin);
|
||||
registries.exposedComponentsRegistry.register({
|
||||
pluginId: preloadedPlugin.pluginId,
|
||||
configs: preloadedPlugin.exposedComponentConfigs,
|
||||
});
|
||||
registries.addedComponentsRegistry.register({
|
||||
pluginId: preloadedPlugin.pluginId,
|
||||
configs: preloadedPlugin.addedComponentConfigs,
|
||||
});
|
||||
}
|
||||
|
||||
stopMeasure(eventName);
|
||||
@ -51,16 +62,16 @@ async function preload(config: AppPluginConfig): Promise<PluginPreloadResult> {
|
||||
isAngular: config.angular.detected,
|
||||
pluginId,
|
||||
});
|
||||
const { extensionConfigs = [], exposedComponentConfigs = [] } = plugin;
|
||||
const { extensionConfigs = [], exposedComponentConfigs = [], addedComponentConfigs = [] } = plugin;
|
||||
|
||||
// Fetching meta-information for the preloaded app plugin and caching it for later.
|
||||
// (The function below returns a promise, but it's not awaited for a reason: we don't want to block the preload process, we would only like to cache the result for later.)
|
||||
getPluginSettings(pluginId);
|
||||
|
||||
return { pluginId, extensionConfigs, exposedComponentConfigs };
|
||||
return { pluginId, extensionConfigs, exposedComponentConfigs, addedComponentConfigs };
|
||||
} catch (error) {
|
||||
console.error(`[Plugins] Failed to preload plugin: ${path} (version: ${version})`, error);
|
||||
return { pluginId, extensionConfigs: [], error, exposedComponentConfigs: [] };
|
||||
return { pluginId, extensionConfigs: [], error, exposedComponentConfigs: [], addedComponentConfigs: [] };
|
||||
} finally {
|
||||
stopMeasure(`frontend_plugin_preload_${pluginId}`);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user