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, "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.", "1"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
[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": [
|
"packages/grafana-data/src/types/config.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[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": [
|
"packages/grafana-runtime/src/services/pluginExtensions/usePluginComponent.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[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": [
|
"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.", "0"]
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
|
||||||
],
|
],
|
||||||
"packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [
|
"packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[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": [
|
"public/app/features/plugins/extensions/getPluginExtensions.test.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[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": [
|
"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"]
|
[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 PluginExtensionCommandPaletteContext,
|
||||||
type PluginExtensionOpenModalOptions,
|
type PluginExtensionOpenModalOptions,
|
||||||
type PluginExposedComponentConfig,
|
type PluginExposedComponentConfig,
|
||||||
|
type PluginAddedComponentConfig,
|
||||||
} from './types/pluginExtensions';
|
} from './types/pluginExtensions';
|
||||||
export {
|
export {
|
||||||
type ScopeDashboardBindingSpec,
|
type ScopeDashboardBindingSpec,
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
PluginExtensionComponentConfig,
|
PluginExtensionComponentConfig,
|
||||||
PluginExposedComponentConfig,
|
PluginExposedComponentConfig,
|
||||||
PluginExtensionConfig,
|
PluginExtensionConfig,
|
||||||
|
PluginAddedComponentConfig,
|
||||||
} from './pluginExtensions';
|
} 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>> {
|
export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppPluginMeta<T>> {
|
||||||
private _exposedComponentConfigs: PluginExposedComponentConfig[] = [];
|
private _exposedComponentConfigs: PluginExposedComponentConfig[] = [];
|
||||||
|
private _addedComponentConfigs: PluginAddedComponentConfig[] = [];
|
||||||
private _extensionConfigs: PluginExtensionConfig[] = [];
|
private _extensionConfigs: PluginExtensionConfig[] = [];
|
||||||
|
|
||||||
// Content under: /a/${plugin-id}/*
|
// Content under: /a/${plugin-id}/*
|
||||||
@ -104,6 +106,10 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
|||||||
return this._exposedComponentConfigs;
|
return this._exposedComponentConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get addedComponentConfigs() {
|
||||||
|
return this._addedComponentConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
get extensionConfigs() {
|
get extensionConfigs() {
|
||||||
return this._extensionConfigs;
|
return this._extensionConfigs;
|
||||||
}
|
}
|
||||||
@ -128,22 +134,8 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
addComponent<Props = {}>(
|
addComponent<Props = {}>(addedComponentConfig: PluginAddedComponentConfig<Props>) {
|
||||||
extensionConfig: { targets: string | string[] } & Omit<
|
this._addedComponentConfigs.push(addedComponentConfig as PluginAddedComponentConfig);
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -168,6 +160,7 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
|||||||
this.addComponent({
|
this.addComponent({
|
||||||
targets: [extension.extensionPointId],
|
targets: [extension.extensionPointId],
|
||||||
...extension,
|
...extension,
|
||||||
|
component: extension.component as ComponentType,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
@ -95,6 +95,28 @@ export type PluginExtensionComponentConfig<Props = {}> = {
|
|||||||
extensionPointId: string;
|
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 = {}> = {
|
export type PluginExposedComponentConfig<Props = {}> = {
|
||||||
/**
|
/**
|
||||||
* The unique identifier of the component
|
* The unique identifier of the component
|
||||||
|
@ -26,11 +26,11 @@ export {
|
|||||||
usePluginExtensions,
|
usePluginExtensions,
|
||||||
usePluginLinkExtensions,
|
usePluginLinkExtensions,
|
||||||
usePluginComponentExtensions,
|
usePluginComponentExtensions,
|
||||||
usePluginComponents,
|
|
||||||
usePluginLinks,
|
usePluginLinks,
|
||||||
} from './pluginExtensions/usePluginExtensions';
|
} from './pluginExtensions/usePluginExtensions';
|
||||||
|
|
||||||
export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent';
|
export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent';
|
||||||
|
export { setPluginComponentsHook, usePluginComponents } from './pluginExtensions/usePluginComponents';
|
||||||
|
|
||||||
export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils';
|
export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils';
|
||||||
export { setCurrentUser } from './user';
|
export { setCurrentUser } from './user';
|
||||||
|
@ -16,6 +16,11 @@ export type GetPluginExtensionsOptions = {
|
|||||||
limitPerPlugin?: number;
|
limitPerPlugin?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UsePluginComponentOptions = {
|
||||||
|
extensionPointId: string;
|
||||||
|
limitPerPlugin?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type GetPluginExtensionsResult<T = PluginExtension> = {
|
export type GetPluginExtensionsResult<T = PluginExtension> = {
|
||||||
extensions: T[];
|
extensions: T[];
|
||||||
};
|
};
|
||||||
@ -30,6 +35,11 @@ export type UsePluginComponentResult<Props = {}> = {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UsePluginComponentsResult<Props = {}> = {
|
||||||
|
components: Array<React.ComponentType<Props>>;
|
||||||
|
isLoading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
let singleton: GetPluginExtensions | undefined;
|
let singleton: GetPluginExtensions | undefined;
|
||||||
|
|
||||||
export function setPluginExtensionGetter(instance: GetPluginExtensions): void {
|
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]);
|
}, [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.
|
* @deprecated Use usePluginLinks() instead.
|
||||||
*/
|
*/
|
||||||
|
@ -38,6 +38,7 @@ import {
|
|||||||
setReturnToPreviousHook,
|
setReturnToPreviousHook,
|
||||||
setPluginExtensionsHook,
|
setPluginExtensionsHook,
|
||||||
setPluginComponentHook,
|
setPluginComponentHook,
|
||||||
|
setPluginComponentsHook,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
setChromeHeaderHeightHook,
|
setChromeHeaderHeightHook,
|
||||||
} from '@grafana/runtime';
|
} from '@grafana/runtime';
|
||||||
@ -85,8 +86,10 @@ import { DatasourceSrv } from './features/plugins/datasource_srv';
|
|||||||
import { getCoreExtensionConfigurations } from './features/plugins/extensions/getCoreExtensionConfigurations';
|
import { getCoreExtensionConfigurations } from './features/plugins/extensions/getCoreExtensionConfigurations';
|
||||||
import { createPluginExtensionsGetter } from './features/plugins/extensions/getPluginExtensions';
|
import { createPluginExtensionsGetter } from './features/plugins/extensions/getPluginExtensions';
|
||||||
import { ReactivePluginExtensionsRegistry } from './features/plugins/extensions/reactivePluginExtensionRegistry';
|
import { ReactivePluginExtensionsRegistry } from './features/plugins/extensions/reactivePluginExtensionRegistry';
|
||||||
|
import { AddedComponentsRegistry } from './features/plugins/extensions/registry/AddedComponentsRegistry';
|
||||||
import { ExposedComponentsRegistry } from './features/plugins/extensions/registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './features/plugins/extensions/registry/ExposedComponentsRegistry';
|
||||||
import { createUsePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
import { createUsePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
||||||
|
import { createUsePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
||||||
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
|
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
|
||||||
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
|
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
|
||||||
import { preloadPlugins } from './features/plugins/pluginPreloader';
|
import { preloadPlugins } from './features/plugins/pluginPreloader';
|
||||||
@ -210,15 +213,18 @@ export class GrafanaApp {
|
|||||||
initWindowRuntime();
|
initWindowRuntime();
|
||||||
|
|
||||||
// Initialize plugin extensions
|
// Initialize plugin extensions
|
||||||
const extensionsRegistry = new ReactivePluginExtensionsRegistry();
|
const pluginExtensionsRegistries = {
|
||||||
extensionsRegistry.register({
|
extensionsRegistry: new ReactivePluginExtensionsRegistry(),
|
||||||
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
|
};
|
||||||
|
pluginExtensionsRegistries.extensionsRegistry.register({
|
||||||
pluginId: 'grafana',
|
pluginId: 'grafana',
|
||||||
extensionConfigs: getCoreExtensionConfigurations(),
|
extensionConfigs: getCoreExtensionConfigurations(),
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const exposedComponentsRegistry = new ExposedComponentsRegistry();
|
|
||||||
|
|
||||||
if (contextSrv.user.orgRole !== '') {
|
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.
|
// 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.
|
// 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 awaitedAppPlugins = Object.values(config.apps).filter((app) => awaitedAppPluginIds.includes(app.id));
|
||||||
const appPlugins = 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);
|
preloadPlugins(appPlugins, pluginExtensionsRegistries);
|
||||||
await preloadPlugins(
|
await preloadPlugins(awaitedAppPlugins, pluginExtensionsRegistries, 'frontend_awaited_plugins_preload');
|
||||||
awaitedAppPlugins,
|
|
||||||
extensionsRegistry,
|
|
||||||
exposedComponentsRegistry,
|
|
||||||
'frontend_awaited_plugins_preload'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setPluginExtensionGetter(createPluginExtensionsGetter(extensionsRegistry));
|
setPluginExtensionGetter(
|
||||||
setPluginExtensionsHook(createUsePluginExtensions(extensionsRegistry));
|
createPluginExtensionsGetter(
|
||||||
setPluginComponentHook(createUsePluginComponent(exposedComponentsRegistry));
|
pluginExtensionsRegistries.extensionsRegistry,
|
||||||
|
pluginExtensionsRegistries.addedComponentsRegistry
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setPluginExtensionsHook(
|
||||||
|
createUsePluginExtensions(
|
||||||
|
pluginExtensionsRegistries.extensionsRegistry,
|
||||||
|
pluginExtensionsRegistries.addedComponentsRegistry
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setPluginComponentHook(createUsePluginComponent(pluginExtensionsRegistries.exposedComponentsRegistry));
|
||||||
|
setPluginComponentsHook(createUsePluginComponents(pluginExtensionsRegistries.addedComponentsRegistry));
|
||||||
|
|
||||||
// initialize chrome service
|
// initialize chrome service
|
||||||
const queryParams = locationService.getSearchObject();
|
const queryParams = locationService.getSearchObject();
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import * as React from 'react';
|
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 { reportInteraction } from '@grafana/runtime';
|
||||||
|
|
||||||
import { getPluginExtensions } from './getPluginExtensions';
|
import { getPluginExtensions } from './getPluginExtensions';
|
||||||
import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry';
|
import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry';
|
||||||
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
import { isReadOnlyProxy } from './utils';
|
import { isReadOnlyProxy } from './utils';
|
||||||
import { assertPluginExtensionLink } from './validators';
|
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 registry = new ReactivePluginExtensionsRegistry();
|
||||||
|
const addedComponentsRegistry = new AddedComponentsRegistry();
|
||||||
|
|
||||||
for (const { pluginId, extensionConfigs } of preloadResults) {
|
for (const { pluginId, extensionConfigs, addedComponentConfigs } of preloadResults) {
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
extensionConfigs,
|
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
extensionConfigs,
|
||||||
|
addedComponentConfigs: [],
|
||||||
|
});
|
||||||
|
addedComponentsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: addedComponentConfigs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return registry.getRegistry();
|
return { registry: await registry.getRegistry(), addedComponentsRegistry: await addedComponentsRegistry.getState() };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('getPluginExtensions()', () => {
|
describe('getPluginExtensions()', () => {
|
||||||
@ -69,8 +87,13 @@ describe('getPluginExtensions()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should return the extensions for the given placement', async () => {
|
test('should return the extensions for the given placement', async () => {
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]);
|
const registries = await createRegistries([
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 });
|
{ pluginId, extensionConfigs: [link1, link2], addedComponentConfigs: [] },
|
||||||
|
]);
|
||||||
|
const { extensions } = getPluginExtensions({
|
||||||
|
...registries,
|
||||||
|
extensionPointId: extensionPoint1,
|
||||||
|
});
|
||||||
|
|
||||||
expect(extensions).toHaveLength(1);
|
expect(extensions).toHaveLength(1);
|
||||||
expect(extensions[0]).toEqual(
|
expect(extensions[0]).toEqual(
|
||||||
@ -86,10 +109,13 @@ describe('getPluginExtensions()', () => {
|
|||||||
|
|
||||||
test('should not limit the number of extensions per plugin by default', async () => {
|
test('should not limit the number of extensions per plugin by default', async () => {
|
||||||
// Registering 3 extensions for the same plugin for the same placement
|
// Registering 3 extensions for the same plugin for the same placement
|
||||||
const registry = await createPluginExtensionRegistry([
|
const registries = await createRegistries([
|
||||||
{ pluginId, extensionConfigs: [link1, link1, link1, link2] },
|
{ 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).toHaveLength(3);
|
||||||
expect(extensions[0]).toEqual(
|
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 () => {
|
test('should be possible to limit the number of extensions per plugin for a given placement', async () => {
|
||||||
const registry = await createPluginExtensionRegistry([
|
const registries = await createRegistries([
|
||||||
{ pluginId, extensionConfigs: [link1, link1, link1, link2] },
|
{ pluginId, extensionConfigs: [link1, link1, link1, link2], addedComponentConfigs: [] },
|
||||||
{
|
{
|
||||||
pluginId: 'my-plugin',
|
pluginId: 'my-plugin',
|
||||||
|
addedComponentConfigs: [],
|
||||||
extensionConfigs: [
|
extensionConfigs: [
|
||||||
{ ...link1, path: '/a/my-plugin/declare-incident' },
|
{ ...link1, path: '/a/my-plugin/declare-incident' },
|
||||||
{ ...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
|
// 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).toHaveLength(2);
|
||||||
expect(extensions[0]).toEqual(
|
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 () => {
|
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 registries = await createRegistries([
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: 'placement-with-no-extensions' });
|
{ pluginId, extensionConfigs: [link1, link2], addedComponentConfigs: [] },
|
||||||
|
]);
|
||||||
|
const { extensions } = getPluginExtensions({
|
||||||
|
...registries,
|
||||||
|
extensionPointId: 'placement-with-no-extensions',
|
||||||
|
});
|
||||||
|
|
||||||
expect(extensions).toEqual([]);
|
expect(extensions).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should pass the context to the configure() function', async () => {
|
test('should pass the context to the configure() function', async () => {
|
||||||
const context = { title: 'New title from the context!' };
|
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).toHaveBeenCalledTimes(1);
|
||||||
expect(link2.configure).toHaveBeenCalledWith(context);
|
expect(link2.configure).toHaveBeenCalledWith(context);
|
||||||
@ -158,8 +194,11 @@ describe('getPluginExtensions()', () => {
|
|||||||
category: 'Machine Learning',
|
category: 'Machine Learning',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
const { extensions } = getPluginExtensions({
|
||||||
|
...registries,
|
||||||
|
extensionPointId: extensionPoint2,
|
||||||
|
});
|
||||||
const [extension] = extensions;
|
const [extension] = extensions;
|
||||||
|
|
||||||
assertPluginExtensionLink(extension);
|
assertPluginExtensionLink(extension);
|
||||||
@ -181,8 +220,11 @@ describe('getPluginExtensions()', () => {
|
|||||||
category: 'Machine Learning',
|
category: 'Machine Learning',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
const { extensions } = getPluginExtensions({
|
||||||
|
...registries,
|
||||||
|
extensionPointId: extensionPoint2,
|
||||||
|
});
|
||||||
const [extension] = extensions;
|
const [extension] = extensions;
|
||||||
|
|
||||||
assertPluginExtensionLink(extension);
|
assertPluginExtensionLink(extension);
|
||||||
@ -206,8 +248,11 @@ describe('getPluginExtensions()', () => {
|
|||||||
title: 'test',
|
title: 'test',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
const { extensions } = getPluginExtensions({
|
||||||
|
...registries,
|
||||||
|
extensionPointId: extensionPoint2,
|
||||||
|
});
|
||||||
const [extension] = extensions;
|
const [extension] = extensions;
|
||||||
|
|
||||||
expect(link2.configure).toHaveBeenCalledTimes(1);
|
expect(link2.configure).toHaveBeenCalledTimes(1);
|
||||||
@ -220,8 +265,12 @@ describe('getPluginExtensions()', () => {
|
|||||||
});
|
});
|
||||||
test('should pass a read only context to the configure() function', async () => {
|
test('should pass a read only context to the configure() function', async () => {
|
||||||
const context = { title: 'New title from the context!' };
|
const context = { title: 'New title from the context!' };
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
const { extensions } = getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 });
|
const { extensions } = getPluginExtensions({
|
||||||
|
...registries,
|
||||||
|
context,
|
||||||
|
extensionPointId: extensionPoint2,
|
||||||
|
});
|
||||||
const [extension] = extensions;
|
const [extension] = extensions;
|
||||||
const readOnlyContext = (link2.configure as jest.Mock).mock.calls[0][0];
|
const readOnlyContext = (link2.configure as jest.Mock).mock.calls[0][0];
|
||||||
|
|
||||||
@ -240,10 +289,10 @@ describe('getPluginExtensions()', () => {
|
|||||||
throw new Error('Something went wrong!');
|
throw new Error('Something went wrong!');
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
|
|
||||||
expect(link2.configure).toHaveBeenCalledTimes(1);
|
expect(link2.configure).toHaveBeenCalledTimes(1);
|
||||||
@ -259,9 +308,17 @@ describe('getPluginExtensions()', () => {
|
|||||||
path: 'invalid-path',
|
path: 'invalid-path',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]);
|
const registries = await createRegistries([
|
||||||
const { extensions: extensionsAtPlacement1 } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 });
|
{ pluginId, extensionConfigs: [link1, link2], addedComponentConfigs: [] },
|
||||||
const { extensions: extensionsAtPlacement2 } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
]);
|
||||||
|
const { extensions: extensionsAtPlacement1 } = getPluginExtensions({
|
||||||
|
...registries,
|
||||||
|
extensionPointId: extensionPoint1,
|
||||||
|
});
|
||||||
|
const { extensions: extensionsAtPlacement2 } = getPluginExtensions({
|
||||||
|
...registries,
|
||||||
|
extensionPointId: extensionPoint2,
|
||||||
|
});
|
||||||
|
|
||||||
expect(extensionsAtPlacement1).toHaveLength(0);
|
expect(extensionsAtPlacement1).toHaveLength(0);
|
||||||
expect(extensionsAtPlacement2).toHaveLength(0);
|
expect(extensionsAtPlacement2).toHaveLength(0);
|
||||||
@ -279,8 +336,8 @@ describe('getPluginExtensions()', () => {
|
|||||||
|
|
||||||
link2.configure = jest.fn().mockImplementation(() => overrides);
|
link2.configure = jest.fn().mockImplementation(() => overrides);
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||||
|
|
||||||
expect(extensions).toHaveLength(0);
|
expect(extensions).toHaveLength(0);
|
||||||
expect(link2.configure).toHaveBeenCalledTimes(1);
|
expect(link2.configure).toHaveBeenCalledTimes(1);
|
||||||
@ -290,8 +347,8 @@ describe('getPluginExtensions()', () => {
|
|||||||
test('should skip the extension if the configure() function returns a promise', async () => {
|
test('should skip the extension if the configure() function returns a promise', async () => {
|
||||||
link2.configure = jest.fn().mockImplementation(() => Promise.resolve({}));
|
link2.configure = jest.fn().mockImplementation(() => Promise.resolve({}));
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||||
|
|
||||||
expect(extensions).toHaveLength(0);
|
expect(extensions).toHaveLength(0);
|
||||||
expect(link2.configure).toHaveBeenCalledTimes(1);
|
expect(link2.configure).toHaveBeenCalledTimes(1);
|
||||||
@ -301,8 +358,8 @@ describe('getPluginExtensions()', () => {
|
|||||||
test('should skip (hide) the extension if the configure() function returns undefined', async () => {
|
test('should skip (hide) the extension if the configure() function returns undefined', async () => {
|
||||||
link2.configure = jest.fn().mockImplementation(() => undefined);
|
link2.configure = jest.fn().mockImplementation(() => undefined);
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||||
|
|
||||||
expect(extensions).toHaveLength(0);
|
expect(extensions).toHaveLength(0);
|
||||||
expect(global.console.warn).toHaveBeenCalledTimes(0); // As this is intentional, no warning should be logged
|
expect(global.console.warn).toHaveBeenCalledTimes(0); // As this is intentional, no warning should be logged
|
||||||
@ -315,8 +372,8 @@ describe('getPluginExtensions()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const context = {};
|
const context = {};
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||||
const [extension] = extensions;
|
const [extension] = extensions;
|
||||||
|
|
||||||
assertPluginExtensionLink(extension);
|
assertPluginExtensionLink(extension);
|
||||||
@ -338,8 +395,8 @@ describe('getPluginExtensions()', () => {
|
|||||||
link2.path = undefined;
|
link2.path = undefined;
|
||||||
link2.onClick = jest.fn().mockRejectedValue(new Error('testing'));
|
link2.onClick = jest.fn().mockRejectedValue(new Error('testing'));
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||||
const [extension] = extensions;
|
const [extension] = extensions;
|
||||||
|
|
||||||
assertPluginExtensionLink(extension);
|
assertPluginExtensionLink(extension);
|
||||||
@ -357,8 +414,8 @@ describe('getPluginExtensions()', () => {
|
|||||||
throw new Error('Something went wrong!');
|
throw new Error('Something went wrong!');
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
|
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 });
|
||||||
const [extension] = extensions;
|
const [extension] = extensions;
|
||||||
|
|
||||||
assertPluginExtensionLink(extension);
|
assertPluginExtensionLink(extension);
|
||||||
@ -375,8 +432,8 @@ describe('getPluginExtensions()', () => {
|
|||||||
link2.path = undefined;
|
link2.path = undefined;
|
||||||
link2.onClick = jest.fn();
|
link2.onClick = jest.fn();
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
|
const registries = await createRegistries([{ pluginId, extensionConfigs: [link2], addedComponentConfigs: [] }]);
|
||||||
const { extensions } = getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 });
|
const { extensions } = getPluginExtensions({ ...registries, context, extensionPointId: extensionPoint2 });
|
||||||
const [extension] = extensions;
|
const [extension] = extensions;
|
||||||
|
|
||||||
assertPluginExtensionLink(extension);
|
assertPluginExtensionLink(extension);
|
||||||
@ -398,8 +455,8 @@ describe('getPluginExtensions()', () => {
|
|||||||
array: ['a'],
|
array: ['a'],
|
||||||
};
|
};
|
||||||
|
|
||||||
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(() => {
|
expect(() => {
|
||||||
context.title = 'Updating the title';
|
context.title = 'Updating the title';
|
||||||
@ -411,7 +468,7 @@ describe('getPluginExtensions()', () => {
|
|||||||
test('should report interaction when onClick is triggered', async () => {
|
test('should report interaction when onClick is triggered', async () => {
|
||||||
const reportInteractionMock = jest.mocked(reportInteraction);
|
const reportInteractionMock = jest.mocked(reportInteraction);
|
||||||
|
|
||||||
const registry = await createPluginExtensionRegistry([
|
const registries = await createRegistries([
|
||||||
{
|
{
|
||||||
pluginId,
|
pluginId,
|
||||||
extensionConfigs: [
|
extensionConfigs: [
|
||||||
@ -421,9 +478,10 @@ describe('getPluginExtensions()', () => {
|
|||||||
onClick: jest.fn(),
|
onClick: jest.fn(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
addedComponentConfigs: [],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 });
|
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint1 });
|
||||||
const [extension] = extensions;
|
const [extension] = extensions;
|
||||||
|
|
||||||
assertPluginExtensionLink(extension);
|
assertPluginExtensionLink(extension);
|
||||||
@ -440,17 +498,65 @@ describe('getPluginExtensions()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should be possible to register and get component type extensions', async () => {
|
test('should be possible to register and get component type extensions', async () => {
|
||||||
const extension = component1;
|
const registries = await createRegistries([
|
||||||
const registry = await createPluginExtensionRegistry([{ pluginId, extensionConfigs: [extension] }]);
|
{
|
||||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extension.extensionPointId });
|
pluginId,
|
||||||
|
extensionConfigs: [],
|
||||||
|
addedComponentConfigs: [
|
||||||
|
{
|
||||||
|
...component1,
|
||||||
|
targets: component1.extensionPointId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const { extensions } = getPluginExtensions({ ...registries, extensionPointId: component1.extensionPointId });
|
||||||
|
|
||||||
expect(extensions).toHaveLength(1);
|
expect(extensions).toHaveLength(1);
|
||||||
expect(extensions[0]).toEqual(
|
expect(extensions[0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
pluginId,
|
pluginId,
|
||||||
type: PluginExtensionTypes.component,
|
type: PluginExtensionTypes.component,
|
||||||
title: extension.title,
|
title: component1.title,
|
||||||
description: extension.description,
|
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 { GetPluginExtensions, reportInteraction } from '@grafana/runtime';
|
||||||
|
|
||||||
import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry';
|
import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry';
|
||||||
import type { PluginExtensionRegistry } from './types';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import type { AddedComponentsRegistryState, PluginExtensionRegistry } from './types';
|
||||||
import {
|
import {
|
||||||
isPluginExtensionLinkConfig,
|
isPluginExtensionLinkConfig,
|
||||||
getReadOnlyProxy,
|
getReadOnlyProxy,
|
||||||
logWarning,
|
logWarning,
|
||||||
generateExtensionId,
|
generateExtensionId,
|
||||||
getEventHelpers,
|
getEventHelpers,
|
||||||
isPluginExtensionComponentConfig,
|
|
||||||
wrapWithPluginContext,
|
wrapWithPluginContext,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import {
|
import { assertIsNotPromise, assertLinkPathIsValid, assertStringProps, isPromise } from './validators';
|
||||||
assertIsReactComponent,
|
|
||||||
assertIsNotPromise,
|
|
||||||
assertLinkPathIsValid,
|
|
||||||
assertStringProps,
|
|
||||||
isPromise,
|
|
||||||
} from './validators';
|
|
||||||
|
|
||||||
type GetExtensions = ({
|
type GetExtensions = ({
|
||||||
context,
|
context,
|
||||||
extensionPointId,
|
extensionPointId,
|
||||||
limitPerPlugin,
|
limitPerPlugin,
|
||||||
registry,
|
registry,
|
||||||
|
addedComponentsRegistry,
|
||||||
}: {
|
}: {
|
||||||
context?: object | Record<string | symbol, unknown>;
|
context?: object | Record<string | symbol, unknown>;
|
||||||
extensionPointId: string;
|
extensionPointId: string;
|
||||||
limitPerPlugin?: number;
|
limitPerPlugin?: number;
|
||||||
registry: PluginExtensionRegistry;
|
registry: PluginExtensionRegistry;
|
||||||
|
addedComponentsRegistry: AddedComponentsRegistryState;
|
||||||
}) => { extensions: PluginExtension[] };
|
}) => { extensions: PluginExtension[] };
|
||||||
|
|
||||||
export function createPluginExtensionsGetter(extensionRegistry: ReactivePluginExtensionsRegistry): GetPluginExtensions {
|
export function createPluginExtensionsGetter(
|
||||||
|
extensionRegistry: ReactivePluginExtensionsRegistry,
|
||||||
|
addedComponentRegistry: AddedComponentsRegistry
|
||||||
|
): GetPluginExtensions {
|
||||||
let registry: PluginExtensionRegistry = { id: '', extensions: {} };
|
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
|
// Create a subscription to keep an copy of the registry state for use in the non-async
|
||||||
// plugin extensions getter.
|
// plugin extensions getter.
|
||||||
@ -50,11 +50,22 @@ export function createPluginExtensionsGetter(extensionRegistry: ReactivePluginEx
|
|||||||
registry = r;
|
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
|
// 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 frozenContext = context ? getReadOnlyProxy(context) : {};
|
||||||
const registryItems = registry.extensions[extensionPointId] ?? [];
|
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.
|
// 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);
|
extensions.push(extension);
|
||||||
extensionsByPlugin[pluginId] += 1;
|
extensionsByPlugin[pluginId] += 1;
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
logWarning(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// COMPONENT
|
if (extensionPointId in addedComponentsRegistry) {
|
||||||
if (isPluginExtensionComponentConfig(extensionConfig)) {
|
try {
|
||||||
assertIsReactComponent(extensionConfig.component);
|
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 = {
|
const extension: PluginExtensionComponent = {
|
||||||
id: generateExtensionId(registryItem.pluginId, extensionConfig),
|
id: generateExtensionId(addedComponent.pluginId, {
|
||||||
|
...addedComponent,
|
||||||
|
extensionPointId,
|
||||||
type: PluginExtensionTypes.component,
|
type: PluginExtensionTypes.component,
|
||||||
pluginId: registryItem.pluginId,
|
}),
|
||||||
|
type: PluginExtensionTypes.component,
|
||||||
title: extensionConfig.title,
|
pluginId: addedComponent.pluginId,
|
||||||
description: extensionConfig.description,
|
title: addedComponent.title,
|
||||||
component: wrapWithPluginContext(pluginId, extensionConfig.component),
|
description: addedComponent.description,
|
||||||
|
component: wrapWithPluginContext(addedComponent.pluginId, addedComponent.component),
|
||||||
};
|
};
|
||||||
|
|
||||||
extensions.push(extension);
|
extensions.push(extension);
|
||||||
extensionsByPlugin[pluginId] += 1;
|
extensionsByPlugin[addedComponent.pluginId] += 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
@ -40,6 +40,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = await reactiveRegistry.getRegistry();
|
const registry = await reactiveRegistry.getRegistry();
|
||||||
@ -66,6 +67,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry1 = await reactiveRegistry.getRegistry();
|
const registry1 = await reactiveRegistry.getRegistry();
|
||||||
@ -86,6 +88,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry2 = await reactiveRegistry.getRegistry();
|
const registry2 = await reactiveRegistry.getRegistry();
|
||||||
@ -120,6 +123,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = await reactiveRegistry.getRegistry();
|
const registry = await reactiveRegistry.getRegistry();
|
||||||
@ -173,6 +177,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry1 = await reactiveRegistry.getRegistry();
|
const registry1 = await reactiveRegistry.getRegistry();
|
||||||
@ -207,6 +212,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry2 = await reactiveRegistry.getRegistry();
|
const registry2 = await reactiveRegistry.getRegistry();
|
||||||
@ -258,6 +264,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry1 = await reactiveRegistry.getRegistry();
|
const registry1 = await reactiveRegistry.getRegistry();
|
||||||
@ -292,6 +299,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry2 = await reactiveRegistry.getRegistry();
|
const registry2 = await reactiveRegistry.getRegistry();
|
||||||
@ -344,6 +352,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register extensions to a different extension point
|
// Register extensions to a different extension point
|
||||||
@ -360,6 +369,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry2 = await reactiveRegistry.getRegistry();
|
const registry2 = await reactiveRegistry.getRegistry();
|
||||||
@ -410,6 +420,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register extensions to a different extension point
|
// Register extensions to a different extension point
|
||||||
@ -426,6 +437,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry2 = await reactiveRegistry.getRegistry();
|
const registry2 = await reactiveRegistry.getRegistry();
|
||||||
@ -482,6 +494,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||||
@ -500,6 +513,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(subscribeCallback).toHaveBeenCalledTimes(3);
|
expect(subscribeCallback).toHaveBeenCalledTimes(3);
|
||||||
@ -553,6 +567,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
observable.subscribe(subscribeCallback);
|
observable.subscribe(subscribeCallback);
|
||||||
@ -597,6 +612,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(consoleWarn).toHaveBeenCalled();
|
expect(consoleWarn).toHaveBeenCalled();
|
||||||
@ -657,6 +673,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(consoleWarn).toHaveBeenCalled();
|
expect(consoleWarn).toHaveBeenCalled();
|
||||||
@ -687,6 +704,7 @@ describe('createPluginExtensionsRegistry', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(consoleWarn).toHaveBeenCalled();
|
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(Object.keys(registry)).toHaveLength(1);
|
||||||
expect(registry[id]).toMatchObject({
|
expect(registry[id]).toMatchObject({
|
||||||
pluginId,
|
pluginId,
|
||||||
config: {
|
|
||||||
id,
|
id,
|
||||||
title: 'not important',
|
title: 'not important',
|
||||||
description: 'not important',
|
description: 'not important',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -82,9 +80,9 @@ describe('ExposedComponentsRegistry', () => {
|
|||||||
const registry = await reactiveRegistry.getState();
|
const registry = await reactiveRegistry.getState();
|
||||||
|
|
||||||
expect(Object.keys(registry)).toHaveLength(3);
|
expect(Object.keys(registry)).toHaveLength(3);
|
||||||
expect(registry[id1]).toMatchObject({ config: { id: id1 }, pluginId });
|
expect(registry[id1]).toMatchObject({ id: id1, pluginId });
|
||||||
expect(registry[id2]).toMatchObject({ config: { id: id2 }, pluginId });
|
expect(registry[id2]).toMatchObject({ id: id2, pluginId });
|
||||||
expect(registry[id3]).toMatchObject({ config: { id: id3 }, pluginId });
|
expect(registry[id3]).toMatchObject({ id: id3, pluginId });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be possible to register multiple exposed components from multiple plugins', async () => {
|
it('should be possible to register multiple exposed components from multiple plugins', async () => {
|
||||||
@ -135,10 +133,10 @@ describe('ExposedComponentsRegistry', () => {
|
|||||||
const registry = await reactiveRegistry.getState();
|
const registry = await reactiveRegistry.getState();
|
||||||
|
|
||||||
expect(Object.keys(registry)).toHaveLength(4);
|
expect(Object.keys(registry)).toHaveLength(4);
|
||||||
expect(registry[id1]).toMatchObject({ config: { id: id1 }, pluginId: pluginId1 });
|
expect(registry[id1]).toMatchObject({ id: id1, pluginId: pluginId1 });
|
||||||
expect(registry[id2]).toMatchObject({ config: { id: id2 }, pluginId: pluginId1 });
|
expect(registry[id2]).toMatchObject({ id: id2, pluginId: pluginId1 });
|
||||||
expect(registry[id3]).toMatchObject({ config: { id: id3 }, pluginId: pluginId2 });
|
expect(registry[id3]).toMatchObject({ id: id3, pluginId: pluginId2 });
|
||||||
expect(registry[id4]).toMatchObject({ config: { id: id4 }, pluginId: pluginId2 });
|
expect(registry[id4]).toMatchObject({ id: id4, pluginId: pluginId2 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should notify subscribers when the registry changes', async () => {
|
it('should notify subscribers when the registry changes', async () => {
|
||||||
@ -208,11 +206,9 @@ describe('ExposedComponentsRegistry', () => {
|
|||||||
|
|
||||||
expect(mock['grafana-basic-app/hello-world/v1']).toMatchObject({
|
expect(mock['grafana-basic-app/hello-world/v1']).toMatchObject({
|
||||||
pluginId: 'grafana-basic-app',
|
pluginId: 'grafana-basic-app',
|
||||||
config: {
|
|
||||||
id: 'grafana-basic-app/hello-world/v1',
|
id: 'grafana-basic-app/hello-world/v1',
|
||||||
title: 'not important',
|
title: 'not important',
|
||||||
description: 'not important',
|
description: 'not important',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -234,9 +230,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||||||
expect(Object.keys(currentState1)).toHaveLength(1);
|
expect(Object.keys(currentState1)).toHaveLength(1);
|
||||||
expect(currentState1['grafana-basic-app1/hello-world/v1']).toMatchObject({
|
expect(currentState1['grafana-basic-app1/hello-world/v1']).toMatchObject({
|
||||||
pluginId: 'grafana-basic-app1',
|
pluginId: 'grafana-basic-app1',
|
||||||
config: {
|
|
||||||
id: 'grafana-basic-app1/hello-world/v1',
|
id: 'grafana-basic-app1/hello-world/v1',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
|
@ -1,20 +1,28 @@
|
|||||||
import { PluginExposedComponentConfig } from '@grafana/data';
|
import { PluginExposedComponentConfig } from '@grafana/data';
|
||||||
|
|
||||||
import { logWarning } from '../utils';
|
import { logWarning } from '../utils';
|
||||||
|
import { extensionPointEndsWithVersion } from '../validators';
|
||||||
|
|
||||||
import { Registry, RegistryType, PluginExtensionConfigs } from './Registry';
|
import { Registry, RegistryType, PluginExtensionConfigs } from './Registry';
|
||||||
|
|
||||||
export class ExposedComponentsRegistry extends Registry<PluginExposedComponentConfig> {
|
export type ExposedComponentRegistryItem<Props = {}> = {
|
||||||
constructor(initialState: RegistryType<PluginExposedComponentConfig> = {}) {
|
pluginId: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
component: React.ComponentType<Props>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ExposedComponentsRegistry extends Registry<ExposedComponentRegistryItem, PluginExposedComponentConfig> {
|
||||||
|
constructor(initialState: RegistryType<ExposedComponentRegistryItem> = {}) {
|
||||||
super({
|
super({
|
||||||
initialState,
|
initialState,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mapToRegistry(
|
mapToRegistry(
|
||||||
registry: RegistryType<PluginExposedComponentConfig>,
|
registry: RegistryType<ExposedComponentRegistryItem>,
|
||||||
{ pluginId, configs }: PluginExtensionConfigs<PluginExposedComponentConfig>
|
{ pluginId, configs }: PluginExtensionConfigs<PluginExposedComponentConfig>
|
||||||
): RegistryType<PluginExposedComponentConfig> {
|
): RegistryType<ExposedComponentRegistryItem> {
|
||||||
if (!configs) {
|
if (!configs) {
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
@ -29,7 +37,7 @@ export class ExposedComponentsRegistry extends Registry<PluginExposedComponentCo
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!id.match(/.*\/v\d+$/)) {
|
if (!extensionPointEndsWithVersion(id)) {
|
||||||
logWarning(
|
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'.`
|
`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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
registry[id] = { config, pluginId };
|
registry[id] = { ...config, pluginId };
|
||||||
}
|
}
|
||||||
|
|
||||||
return registry;
|
return registry;
|
||||||
|
@ -7,28 +7,23 @@ export type PluginExtensionConfigs<T> = {
|
|||||||
configs: T[];
|
configs: T[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RegistryItem<T> = {
|
export type RegistryType<T> = Record<string | symbol, T>;
|
||||||
pluginId: string;
|
|
||||||
config: T;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RegistryType<T> = Record<string | symbol, RegistryItem<T>>;
|
|
||||||
|
|
||||||
type ConstructorOptions<T> = {
|
type ConstructorOptions<T> = {
|
||||||
initialState: RegistryType<T>;
|
initialState: RegistryType<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is the base-class used by the separate specific registries.
|
// This is the base-class used by the separate specific registries.
|
||||||
export abstract class Registry<T> {
|
export abstract class Registry<TRegistryValue, TMapType> {
|
||||||
private resultSubject: Subject<PluginExtensionConfigs<T>>;
|
private resultSubject: Subject<PluginExtensionConfigs<TMapType>>;
|
||||||
private registrySubject: ReplaySubject<RegistryType<T>>;
|
private registrySubject: ReplaySubject<RegistryType<TRegistryValue>>;
|
||||||
|
|
||||||
constructor(options: ConstructorOptions<T>) {
|
constructor(options: ConstructorOptions<TRegistryValue>) {
|
||||||
const { initialState } = options;
|
const { initialState } = options;
|
||||||
this.resultSubject = new Subject<PluginExtensionConfigs<T>>();
|
this.resultSubject = new Subject<PluginExtensionConfigs<TMapType>>();
|
||||||
// This is the subject that we expose.
|
// 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.)
|
// (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
|
this.resultSubject
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -41,17 +36,20 @@ export abstract class Registry<T> {
|
|||||||
.subscribe(this.registrySubject);
|
.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);
|
this.resultSubject.next(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
asObservable(): Observable<RegistryType<T>> {
|
asObservable(): Observable<RegistryType<TRegistryValue>> {
|
||||||
return this.registrySubject.asObservable();
|
return this.registrySubject.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
getState(): Promise<RegistryType<T>> {
|
getState(): Promise<RegistryType<TRegistryValue>> {
|
||||||
return firstValueFrom(this.asObservable());
|
return firstValueFrom(this.asObservable());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import type { PluginExtensionConfig } from '@grafana/data';
|
import type { PluginExtensionConfig } from '@grafana/data';
|
||||||
|
|
||||||
|
import { AddedComponentRegistryItem } from './registry/AddedComponentsRegistry';
|
||||||
|
import { RegistryType } from './registry/Registry';
|
||||||
|
|
||||||
// The information that is stored in the registry
|
// The information that is stored in the registry
|
||||||
export type PluginExtensionRegistryItem = {
|
export type PluginExtensionRegistryItem = {
|
||||||
// Any additional meta information that we would like to store about the extension in the registry
|
// 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;
|
id: string;
|
||||||
extensions: Record<string, PluginExtensionRegistryItem[]>;
|
extensions: Record<string, PluginExtensionRegistryItem[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AddedComponentsRegistryState = RegistryType<Array<AddedComponentRegistryItem<{}>>>;
|
||||||
|
@ -26,7 +26,7 @@ export function createUsePluginComponent(registry: ExposedComponentsRegistry) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
component: wrapWithPluginContext(registryItem.pluginId, registryItem.config.component),
|
component: wrapWithPluginContext(registryItem.pluginId, registryItem.component),
|
||||||
};
|
};
|
||||||
}, [id, registry]);
|
}, [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 { PluginExtensionTypes } from '@grafana/data';
|
||||||
|
|
||||||
import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry';
|
import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry';
|
||||||
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
import { createUsePluginExtensions } from './usePluginExtensions';
|
import { createUsePluginExtensions } from './usePluginExtensions';
|
||||||
|
|
||||||
describe('usePluginExtensions()', () => {
|
describe('usePluginExtensions()', () => {
|
||||||
let reactiveRegistry: ReactivePluginExtensionsRegistry;
|
let reactiveRegistry: ReactivePluginExtensionsRegistry;
|
||||||
|
let addedComponentsRegistry: AddedComponentsRegistry;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
reactiveRegistry = new ReactivePluginExtensionsRegistry();
|
reactiveRegistry = new ReactivePluginExtensionsRegistry();
|
||||||
|
addedComponentsRegistry = new AddedComponentsRegistry();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an empty array if there are no extensions registered for the extension point', () => {
|
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(() =>
|
const { result } = renderHook(() =>
|
||||||
usePluginExtensions({
|
usePluginExtensions({
|
||||||
extensionPointId: 'foo/bar',
|
extensionPointId: 'foo/bar',
|
||||||
@ -24,7 +27,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
expect(result.current.extensions).toEqual([]);
|
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 extensionPointId = 'plugins/foo/bar';
|
||||||
const pluginId = 'my-app-plugin';
|
const pluginId = 'my-app-plugin';
|
||||||
|
|
||||||
@ -47,9 +50,10 @@ describe('usePluginExtensions()', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||||
const { result } = renderHook(() => usePluginExtensions({ extensionPointId }));
|
const { result } = renderHook(() => usePluginExtensions({ extensionPointId }));
|
||||||
|
|
||||||
expect(result.current.extensions.length).toBe(2);
|
expect(result.current.extensions.length).toBe(2);
|
||||||
@ -57,10 +61,63 @@ describe('usePluginExtensions()', () => {
|
|||||||
expect(result.current.extensions[1].title).toBe('2');
|
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', () => {
|
it('should dynamically update the extensions registered for a certain extension point', () => {
|
||||||
const extensionPointId = 'plugins/foo/bar';
|
const extensionPointId = 'plugins/foo/bar';
|
||||||
const pluginId = 'my-app-plugin';
|
const pluginId = 'my-app-plugin';
|
||||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||||
let { result, rerender } = renderHook(() => usePluginExtensions({ extensionPointId }));
|
let { result, rerender } = renderHook(() => usePluginExtensions({ extensionPointId }));
|
||||||
|
|
||||||
// No extensions yet
|
// No extensions yet
|
||||||
@ -87,6 +144,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -101,7 +159,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
it('should only render the hook once', () => {
|
it('should only render the hook once', () => {
|
||||||
const spy = jest.spyOn(reactiveRegistry, 'asObservable');
|
const spy = jest.spyOn(reactiveRegistry, 'asObservable');
|
||||||
const extensionPointId = 'plugins/foo/bar';
|
const extensionPointId = 'plugins/foo/bar';
|
||||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||||
|
|
||||||
renderHook(() => usePluginExtensions({ extensionPointId }));
|
renderHook(() => usePluginExtensions({ extensionPointId }));
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
@ -110,7 +168,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
it('should return the same extensions object if the context object is the same', () => {
|
it('should return the same extensions object if the context object is the same', () => {
|
||||||
const extensionPointId = 'plugins/foo/bar';
|
const extensionPointId = 'plugins/foo/bar';
|
||||||
const pluginId = 'my-app-plugin';
|
const pluginId = 'my-app-plugin';
|
||||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||||
|
|
||||||
// Add extensions to the registry
|
// Add extensions to the registry
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -133,6 +191,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -146,7 +205,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
it('should return a new extensions object if the context object is different', () => {
|
it('should return a new extensions object if the context object is different', () => {
|
||||||
const extensionPointId = 'plugins/foo/bar';
|
const extensionPointId = 'plugins/foo/bar';
|
||||||
const pluginId = 'my-app-plugin';
|
const pluginId = 'my-app-plugin';
|
||||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||||
|
|
||||||
// Add extensions to the registry
|
// Add extensions to the registry
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -169,6 +228,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -182,7 +242,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
const extensionPointId = 'plugins/foo/bar';
|
const extensionPointId = 'plugins/foo/bar';
|
||||||
const pluginId = 'my-app-plugin';
|
const pluginId = 'my-app-plugin';
|
||||||
const context = {};
|
const context = {};
|
||||||
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry);
|
const usePluginExtensions = createUsePluginExtensions(reactiveRegistry, addedComponentsRegistry);
|
||||||
|
|
||||||
// Add the first extension
|
// Add the first extension
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -198,6 +258,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -219,6 +280,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exposedComponentConfigs: [],
|
exposedComponentConfigs: [],
|
||||||
|
addedComponentConfigs: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,9 +5,14 @@ import { GetPluginExtensionsOptions, UsePluginExtensionsResult } from '@grafana/
|
|||||||
|
|
||||||
import { getPluginExtensions } from './getPluginExtensions';
|
import { getPluginExtensions } from './getPluginExtensions';
|
||||||
import { ReactivePluginExtensionsRegistry } from './reactivePluginExtensionRegistry';
|
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 observableRegistry = extensionsRegistry.asObservable();
|
||||||
|
const observableAddedComponentRegistry = addedComponentsRegistry.asObservable();
|
||||||
const cache: {
|
const cache: {
|
||||||
id: string;
|
id: string;
|
||||||
extensions: Record<string, { context: GetPluginExtensionsOptions['context']; extensions: PluginExtension[] }>;
|
extensions: Record<string, { context: GetPluginExtensionsOptions['context']; extensions: PluginExtension[] }>;
|
||||||
@ -18,8 +23,9 @@ export function createUsePluginExtensions(extensionsRegistry: ReactivePluginExte
|
|||||||
|
|
||||||
return function usePluginExtensions(options: GetPluginExtensionsOptions): UsePluginExtensionsResult<PluginExtension> {
|
return function usePluginExtensions(options: GetPluginExtensionsOptions): UsePluginExtensionsResult<PluginExtension> {
|
||||||
const registry = useObservable(observableRegistry);
|
const registry = useObservable(observableRegistry);
|
||||||
|
const addedComponentsRegistry = useObservable(observableAddedComponentRegistry);
|
||||||
|
|
||||||
if (!registry) {
|
if (!registry || !addedComponentsRegistry) {
|
||||||
return { extensions: [], isLoading: false };
|
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] = {
|
cache.extensions[key] = {
|
||||||
context: options.context,
|
context: options.context,
|
||||||
|
@ -5,7 +5,6 @@ import { useAsync } from 'react-use';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type PluginExtensionLinkConfig,
|
type PluginExtensionLinkConfig,
|
||||||
type PluginExtensionComponentConfig,
|
|
||||||
type PluginExtensionConfig,
|
type PluginExtensionConfig,
|
||||||
type PluginExtensionEventHelpers,
|
type PluginExtensionEventHelpers,
|
||||||
PluginExtensionTypes,
|
PluginExtensionTypes,
|
||||||
@ -31,12 +30,6 @@ export function isPluginExtensionLinkConfig(
|
|||||||
return typeof extension === 'object' && 'type' in extension && extension['type'] === PluginExtensionTypes.link;
|
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 = '') {
|
export function handleErrorsInFn(fn: Function, errorMessagePrefix = '') {
|
||||||
return (...args: unknown[]) => {
|
return (...args: unknown[]) => {
|
||||||
try {
|
try {
|
||||||
|
@ -6,7 +6,7 @@ import type {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { isPluginExtensionLink } from '@grafana/runtime';
|
import { isPluginExtensionLink } from '@grafana/runtime';
|
||||||
|
|
||||||
import { isPluginExtensionComponentConfig, isPluginExtensionLinkConfig, logWarning } from './utils';
|
import { isPluginExtensionLinkConfig, logWarning } from './utils';
|
||||||
|
|
||||||
export function assertPluginExtensionLink(
|
export function assertPluginExtensionLink(
|
||||||
extension: PluginExtension | undefined,
|
extension: PluginExtension | undefined,
|
||||||
@ -41,7 +41,7 @@ export function assertIsReactComponent(component: React.ComponentType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function assertExtensionPointIdIsValid(pluginId: string, extension: PluginExtensionConfig) {
|
export function assertExtensionPointIdIsValid(pluginId: string, extension: PluginExtensionConfig) {
|
||||||
if (!isExtensionPointIdValid(pluginId, extension)) {
|
if (!isExtensionPointIdValid(pluginId, extension.extensionPointId)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid extension "${extension.title}". The extensionPointId should start with either "grafana/", "plugins/" or "capabilities/${pluginId}" (currently: "${extension.extensionPointId}"). Skipping the extension.`
|
`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}/`));
|
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(
|
return Boolean(
|
||||||
extension.extensionPointId?.startsWith('grafana/') ||
|
extensionPointId.startsWith('grafana/') ||
|
||||||
extension.extensionPointId?.startsWith('plugins/') ||
|
extensionPointId?.startsWith('plugins/') ||
|
||||||
extension.extensionPointId?.startsWith(`capabilities/${pluginId}/`)
|
extensionPointId?.startsWith(pluginId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extensionPointEndsWithVersion(extensionPointId: string) {
|
||||||
|
return extensionPointId.match(/.*\/v\d+$/);
|
||||||
|
}
|
||||||
|
|
||||||
export function isConfigureFnValid(extension: PluginExtensionLinkConfig) {
|
export function isConfigureFnValid(extension: PluginExtensionLinkConfig) {
|
||||||
return extension.configure ? typeof extension.configure === 'function' : true;
|
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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import type { PluginExposedComponentConfig, PluginExtensionConfig } from '@grafana/data';
|
import type { PluginExposedComponentConfig, PluginExtensionConfig } from '@grafana/data';
|
||||||
|
import { PluginAddedComponentConfig } from '@grafana/data/src/types/pluginExtensions';
|
||||||
import type { AppPluginConfig } from '@grafana/runtime';
|
import type { AppPluginConfig } from '@grafana/runtime';
|
||||||
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
|
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
|
||||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||||
|
|
||||||
import { ReactivePluginExtensionsRegistry } from './extensions/reactivePluginExtensionRegistry';
|
import { ReactivePluginExtensionsRegistry } from './extensions/reactivePluginExtensionRegistry';
|
||||||
|
import { AddedComponentsRegistry } from './extensions/registry/AddedComponentsRegistry';
|
||||||
import { ExposedComponentsRegistry } from './extensions/registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './extensions/registry/ExposedComponentsRegistry';
|
||||||
import * as pluginLoader from './plugin_loader';
|
import * as pluginLoader from './plugin_loader';
|
||||||
|
|
||||||
@ -12,12 +14,18 @@ export type PluginPreloadResult = {
|
|||||||
error?: unknown;
|
error?: unknown;
|
||||||
extensionConfigs: PluginExtensionConfig[];
|
extensionConfigs: PluginExtensionConfig[];
|
||||||
exposedComponentConfigs: PluginExposedComponentConfig[];
|
exposedComponentConfigs: PluginExposedComponentConfig[];
|
||||||
|
addedComponentConfigs: PluginAddedComponentConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type PluginExtensionRegistries = {
|
||||||
|
extensionsRegistry: ReactivePluginExtensionsRegistry;
|
||||||
|
addedComponentsRegistry: AddedComponentsRegistry;
|
||||||
|
exposedComponentsRegistry: ExposedComponentsRegistry;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function preloadPlugins(
|
export async function preloadPlugins(
|
||||||
apps: AppPluginConfig[] = [],
|
apps: AppPluginConfig[] = [],
|
||||||
registry: ReactivePluginExtensionsRegistry,
|
registries: PluginExtensionRegistries,
|
||||||
exposedComponentsRegistry: ExposedComponentsRegistry,
|
|
||||||
eventName = 'frontend_plugins_preload'
|
eventName = 'frontend_plugins_preload'
|
||||||
) {
|
) {
|
||||||
startMeasure(eventName);
|
startMeasure(eventName);
|
||||||
@ -30,12 +38,15 @@ export async function preloadPlugins(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.register(preloadedPlugin);
|
registries.extensionsRegistry.register(preloadedPlugin);
|
||||||
|
registries.exposedComponentsRegistry.register({
|
||||||
exposedComponentsRegistry.register({
|
|
||||||
pluginId: preloadedPlugin.pluginId,
|
pluginId: preloadedPlugin.pluginId,
|
||||||
configs: preloadedPlugin.exposedComponentConfigs,
|
configs: preloadedPlugin.exposedComponentConfigs,
|
||||||
});
|
});
|
||||||
|
registries.addedComponentsRegistry.register({
|
||||||
|
pluginId: preloadedPlugin.pluginId,
|
||||||
|
configs: preloadedPlugin.addedComponentConfigs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
stopMeasure(eventName);
|
stopMeasure(eventName);
|
||||||
@ -51,16 +62,16 @@ async function preload(config: AppPluginConfig): Promise<PluginPreloadResult> {
|
|||||||
isAngular: config.angular.detected,
|
isAngular: config.angular.detected,
|
||||||
pluginId,
|
pluginId,
|
||||||
});
|
});
|
||||||
const { extensionConfigs = [], exposedComponentConfigs = [] } = plugin;
|
const { extensionConfigs = [], exposedComponentConfigs = [], addedComponentConfigs = [] } = plugin;
|
||||||
|
|
||||||
// Fetching meta-information for the preloaded app plugin and caching it for later.
|
// 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.)
|
// (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);
|
getPluginSettings(pluginId);
|
||||||
|
|
||||||
return { pluginId, extensionConfigs, exposedComponentConfigs };
|
return { pluginId, extensionConfigs, exposedComponentConfigs, addedComponentConfigs };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Plugins] Failed to preload plugin: ${path} (version: ${version})`, error);
|
console.error(`[Plugins] Failed to preload plugin: ${path} (version: ${version})`, error);
|
||||||
return { pluginId, extensionConfigs: [], error, exposedComponentConfigs: [] };
|
return { pluginId, extensionConfigs: [], error, exposedComponentConfigs: [], addedComponentConfigs: [] };
|
||||||
} finally {
|
} finally {
|
||||||
stopMeasure(`frontend_plugin_preload_${pluginId}`);
|
stopMeasure(`frontend_plugin_preload_${pluginId}`);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user