PluginExtensions: Added support for sharing functions (#98888)
* feat: add generic plugin extension functions * updated betterer. * Fixed type issues after sync with main. * Remved extensions from datasource and panel. * Added validation for extension function registry. * Added tests and validation logic for function extensions registry. * removed prop already existing on base. * fixed lint error. --------- Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
This commit is contained in:
co-authored by
Marcus Andersson
parent
fbf96916aa
commit
8a8e47fcea
@@ -491,6 +491,9 @@ exports[`better eslint`] = {
|
|||||||
"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"]
|
||||||
],
|
],
|
||||||
|
"packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
|
],
|
||||||
"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"]
|
||||||
],
|
],
|
||||||
@@ -5602,6 +5605,9 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
[0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||||
],
|
],
|
||||||
|
"public/app/features/plugins/extensions/usePluginFunctions.tsx:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
|
],
|
||||||
"public/app/features/plugins/extensions/usePluginLinks.tsx:5381": [
|
"public/app/features/plugins/extensions/usePluginLinks.tsx:5381": [
|
||||||
[0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
|
[0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -549,6 +549,7 @@ export {
|
|||||||
type PluginExtensionLink,
|
type PluginExtensionLink,
|
||||||
type PluginExtensionComponent,
|
type PluginExtensionComponent,
|
||||||
type PluginExtensionConfig,
|
type PluginExtensionConfig,
|
||||||
|
type PluginExtensionFunction,
|
||||||
type PluginExtensionLinkConfig,
|
type PluginExtensionLinkConfig,
|
||||||
type PluginExtensionComponentConfig,
|
type PluginExtensionComponentConfig,
|
||||||
type PluginExtensionEventHelpers,
|
type PluginExtensionEventHelpers,
|
||||||
@@ -559,6 +560,7 @@ export {
|
|||||||
type PluginExtensionExposedComponentConfig,
|
type PluginExtensionExposedComponentConfig,
|
||||||
type PluginExtensionAddedComponentConfig,
|
type PluginExtensionAddedComponentConfig,
|
||||||
type PluginExtensionAddedLinkConfig,
|
type PluginExtensionAddedLinkConfig,
|
||||||
|
type PluginExtensionAddedFunctionConfig,
|
||||||
} from './types/pluginExtensions';
|
} from './types/pluginExtensions';
|
||||||
export {
|
export {
|
||||||
type ScopeDashboardBindingSpec,
|
type ScopeDashboardBindingSpec,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
PluginExtensionExposedComponentConfig,
|
PluginExtensionExposedComponentConfig,
|
||||||
PluginExtensionAddedComponentConfig,
|
PluginExtensionAddedComponentConfig,
|
||||||
PluginExtensionAddedLinkConfig,
|
PluginExtensionAddedLinkConfig,
|
||||||
|
PluginExtensionAddedFunctionConfig,
|
||||||
} from './pluginExtensions';
|
} from './pluginExtensions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,6 +61,7 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
|||||||
private _exposedComponentConfigs: PluginExtensionExposedComponentConfig[] = [];
|
private _exposedComponentConfigs: PluginExtensionExposedComponentConfig[] = [];
|
||||||
private _addedComponentConfigs: PluginExtensionAddedComponentConfig[] = [];
|
private _addedComponentConfigs: PluginExtensionAddedComponentConfig[] = [];
|
||||||
private _addedLinkConfigs: PluginExtensionAddedLinkConfig[] = [];
|
private _addedLinkConfigs: PluginExtensionAddedLinkConfig[] = [];
|
||||||
|
private _addedFunctionConfigs: PluginExtensionAddedFunctionConfig[] = [];
|
||||||
|
|
||||||
// Content under: /a/${plugin-id}/*
|
// Content under: /a/${plugin-id}/*
|
||||||
root?: ComponentType<AppRootProps<T>>;
|
root?: ComponentType<AppRootProps<T>>;
|
||||||
@@ -113,6 +115,10 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
|||||||
return this._addedLinkConfigs;
|
return this._addedLinkConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get addedFunctionConfigs() {
|
||||||
|
return this._addedFunctionConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
addLink<Context extends object>(linkConfig: PluginExtensionAddedLinkConfig<Context>) {
|
addLink<Context extends object>(linkConfig: PluginExtensionAddedLinkConfig<Context>) {
|
||||||
this._addedLinkConfigs.push(linkConfig as PluginExtensionAddedLinkConfig);
|
this._addedLinkConfigs.push(linkConfig as PluginExtensionAddedLinkConfig);
|
||||||
|
|
||||||
@@ -125,6 +131,12 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addFunction<Signature>(addedFunctionConfig: PluginExtensionAddedFunctionConfig<Signature>) {
|
||||||
|
this._addedFunctionConfigs.push(addedFunctionConfig);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
exposeComponent<Props = {}>(componentConfig: PluginExtensionExposedComponentConfig<Props>) {
|
exposeComponent<Props = {}>(componentConfig: PluginExtensionExposedComponentConfig<Props>) {
|
||||||
this._exposedComponentConfigs.push(componentConfig as PluginExtensionExposedComponentConfig);
|
this._exposedComponentConfigs.push(componentConfig as PluginExtensionExposedComponentConfig);
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ export interface PluginExtensions {
|
|||||||
// The component extensions that the plugin registers
|
// The component extensions that the plugin registers
|
||||||
addedComponents: ExtensionInfo[];
|
addedComponents: ExtensionInfo[];
|
||||||
|
|
||||||
|
addedFunctions: ExtensionInfo[];
|
||||||
|
|
||||||
// The link extensions that the plugin registers
|
// The link extensions that the plugin registers
|
||||||
addedLinks: ExtensionInfo[];
|
addedLinks: ExtensionInfo[];
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { RawTimeRange, TimeZone } from './time';
|
|||||||
export enum PluginExtensionTypes {
|
export enum PluginExtensionTypes {
|
||||||
link = 'link',
|
link = 'link',
|
||||||
component = 'component',
|
component = 'component',
|
||||||
|
function = 'function',
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginExtensionBase = {
|
type PluginExtensionBase = {
|
||||||
@@ -36,7 +37,12 @@ export type PluginExtensionComponent<Props = {}> = PluginExtensionBase & {
|
|||||||
component: React.ComponentType<Props>;
|
component: React.ComponentType<Props>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginExtension = PluginExtensionLink | PluginExtensionComponent;
|
export type PluginExtensionFunction<Signature = () => void> = PluginExtensionBase & {
|
||||||
|
type: PluginExtensionTypes.function;
|
||||||
|
fn: Signature;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginExtension = PluginExtensionLink | PluginExtensionComponent | PluginExtensionFunction;
|
||||||
|
|
||||||
// Objects used for registering extensions (in app plugins)
|
// Objects used for registering extensions (in app plugins)
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -74,6 +80,17 @@ export type PluginExtensionAddedComponentConfig<Props = {}> = PluginExtensionCon
|
|||||||
*/
|
*/
|
||||||
component: React.ComponentType<Props>;
|
component: React.ComponentType<Props>;
|
||||||
};
|
};
|
||||||
|
export type PluginExtensionAddedFunctionConfig<Signature = unknown> = PluginExtensionConfigBase & {
|
||||||
|
/**
|
||||||
|
* The target extension points where the component will be added
|
||||||
|
*/
|
||||||
|
targets: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function to be executed
|
||||||
|
*/
|
||||||
|
fn: Signature;
|
||||||
|
};
|
||||||
|
|
||||||
export type PluginAddedLinksConfigureFunc<Context extends object> = (context: Readonly<Context> | undefined) =>
|
export type PluginAddedLinksConfigureFunc<Context extends object> = (context: Readonly<Context> | undefined) =>
|
||||||
| Partial<{
|
| Partial<{
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export {
|
|||||||
type UsePluginExtensions,
|
type UsePluginExtensions,
|
||||||
type UsePluginExtensionsResult,
|
type UsePluginExtensionsResult,
|
||||||
type UsePluginComponentResult,
|
type UsePluginComponentResult,
|
||||||
|
type UsePluginFunctionsOptions,
|
||||||
|
type UsePluginFunctionsResult,
|
||||||
} from './pluginExtensions/getPluginExtensions';
|
} from './pluginExtensions/getPluginExtensions';
|
||||||
export {
|
export {
|
||||||
setPluginExtensionsHook,
|
setPluginExtensionsHook,
|
||||||
@@ -33,6 +35,7 @@ export {
|
|||||||
export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent';
|
export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent';
|
||||||
export { setPluginComponentsHook, usePluginComponents } from './pluginExtensions/usePluginComponents';
|
export { setPluginComponentsHook, usePluginComponents } from './pluginExtensions/usePluginComponents';
|
||||||
export { setPluginLinksHook, usePluginLinks } from './pluginExtensions/usePluginLinks';
|
export { setPluginLinksHook, usePluginLinks } from './pluginExtensions/usePluginLinks';
|
||||||
|
export { setPluginFunctionsHook, usePluginFunctions } from './pluginExtensions/usePluginFunctions';
|
||||||
|
|
||||||
export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils';
|
export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils';
|
||||||
export { setCurrentUser } from './user';
|
export { setCurrentUser } from './user';
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { PluginExtension, PluginExtensionLink, PluginExtensionComponent } from '@grafana/data';
|
import type {
|
||||||
|
PluginExtension,
|
||||||
|
PluginExtensionLink,
|
||||||
|
PluginExtensionComponent,
|
||||||
|
PluginExtensionFunction,
|
||||||
|
} from '@grafana/data';
|
||||||
|
|
||||||
import { isPluginExtensionComponent, isPluginExtensionLink } from './utils';
|
import { isPluginExtensionComponent, isPluginExtensionLink } from './utils';
|
||||||
|
|
||||||
@@ -52,6 +57,16 @@ export type UsePluginLinksResult = {
|
|||||||
links: PluginExtensionLink[];
|
links: PluginExtensionLink[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UsePluginFunctionsOptions = {
|
||||||
|
extensionPointId: string;
|
||||||
|
limitPerPlugin?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UsePluginFunctionsResult<Signature> = {
|
||||||
|
isLoading: boolean;
|
||||||
|
functions: Array<PluginExtensionFunction<Signature>>;
|
||||||
|
};
|
||||||
|
|
||||||
let singleton: GetPluginExtensions | undefined;
|
let singleton: GetPluginExtensions | undefined;
|
||||||
|
|
||||||
export function setPluginExtensionGetter(instance: GetPluginExtensions): void {
|
export function setPluginExtensionGetter(instance: GetPluginExtensions): void {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { UsePluginFunctionsOptions, UsePluginFunctionsResult } from './getPluginExtensions';
|
||||||
|
|
||||||
|
export type UsePluginFunctions<T> = (options: UsePluginFunctionsOptions) => UsePluginFunctionsResult<T>;
|
||||||
|
|
||||||
|
let singleton: UsePluginFunctions<unknown> | undefined;
|
||||||
|
|
||||||
|
export function setPluginFunctionsHook(hook: UsePluginFunctions<unknown>): void {
|
||||||
|
// We allow overriding the registry in tests
|
||||||
|
if (singleton && process.env.NODE_ENV !== 'test') {
|
||||||
|
throw new Error('setUsePluginFunctionsHook() function should only be called once, when Grafana is starting.');
|
||||||
|
}
|
||||||
|
singleton = hook;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePluginFunctions<T>(options: UsePluginFunctionsOptions): UsePluginFunctionsResult<T> {
|
||||||
|
if (!singleton) {
|
||||||
|
throw new Error('usePluginFunctions(options) can only be used after the Grafana instance has started.');
|
||||||
|
}
|
||||||
|
return singleton(options) as UsePluginFunctionsResult<T>;
|
||||||
|
}
|
||||||
@@ -57,6 +57,7 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -96,8 +97,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -127,8 +130,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -200,8 +205,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Action: "plugins.app:access"},
|
{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Action: "plugins.app:access"},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -238,8 +245,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -269,8 +278,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -300,8 +311,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -340,8 +353,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -201,8 +202,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -249,8 +252,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -304,8 +309,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -398,8 +405,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
{Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
|
{Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ type ExtensionsV2 struct {
|
|||||||
AddedComponents []AddedComponent `json:"addedComponents"`
|
AddedComponents []AddedComponent `json:"addedComponents"`
|
||||||
ExposedComponents []ExposedComponent `json:"exposedComponents"`
|
ExposedComponents []ExposedComponent `json:"exposedComponents"`
|
||||||
ExtensionPoints []ExtensionPoint `json:"extensionPoints"`
|
ExtensionPoints []ExtensionPoint `json:"extensionPoints"`
|
||||||
|
AddedFunctions []AddedFunction `json:"addedFunctions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Extensions ExtensionsV2
|
type Extensions ExtensionsV2
|
||||||
@@ -76,6 +77,7 @@ func (e *Extensions) UnmarshalJSON(data []byte) error {
|
|||||||
e.AddedLinks = extensionsV2.AddedLinks
|
e.AddedLinks = extensionsV2.AddedLinks
|
||||||
e.ExposedComponents = extensionsV2.ExposedComponents
|
e.ExposedComponents = extensionsV2.ExposedComponents
|
||||||
e.ExtensionPoints = extensionsV2.ExtensionPoints
|
e.ExtensionPoints = extensionsV2.ExtensionPoints
|
||||||
|
e.AddedFunctions = extensionsV2.AddedFunctions
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -123,6 +125,11 @@ type AddedComponent struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AddedFunction struct {
|
||||||
|
Targets []string `json:"targets"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
type ExposedComponent struct {
|
type ExposedComponent struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@@ -267,6 +274,7 @@ type PluginMetaDTO struct {
|
|||||||
Angular AngularMeta `json:"angular"`
|
Angular AngularMeta `json:"angular"`
|
||||||
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
|
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
|
||||||
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
||||||
|
Extensions Extensions `json:"extensions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataSourceDTO struct {
|
type DataSourceDTO struct {
|
||||||
|
|||||||
@@ -167,6 +167,10 @@ func ReadPluginJSON(reader io.Reader) (JSONData, error) {
|
|||||||
plugin.Extensions.AddedComponents = []AddedComponent{}
|
plugin.Extensions.AddedComponents = []AddedComponent{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if plugin.Extensions.AddedFunctions == nil {
|
||||||
|
plugin.Extensions.AddedFunctions = []AddedFunction{}
|
||||||
|
}
|
||||||
|
|
||||||
if plugin.Extensions.ExposedComponents == nil {
|
if plugin.Extensions.ExposedComponents == nil {
|
||||||
plugin.Extensions.ExposedComponents = []ExposedComponent{}
|
plugin.Extensions.ExposedComponents = []ExposedComponent{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -108,8 +109,10 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Name: "Pie Chart (old)",
|
Name: "Pie Chart (old)",
|
||||||
|
|
||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -143,8 +146,10 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Type: TypeDataSource,
|
Type: TypeDataSource,
|
||||||
|
|
||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -188,6 +193,9 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
"id": "myorg-extensions-app/component-1/v1"
|
"id": "myorg-extensions-app/component-1/v1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"addedFunctions": [
|
||||||
|
{"targets": ["foo/bar"], "title":"some hook"}
|
||||||
|
],
|
||||||
"extensionPoints": [
|
"extensionPoints": [
|
||||||
{
|
{
|
||||||
"title": "Extension point 1",
|
"title": "Extension point 1",
|
||||||
@@ -209,6 +217,7 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
{Title: "Added link 1", Description: "Added link 1 description", Targets: []string{"grafana/dashboard/panel/menu"}},
|
{Title: "Added link 1", Description: "Added link 1 description", Targets: []string{"grafana/dashboard/panel/menu"}},
|
||||||
},
|
},
|
||||||
AddedComponents: []AddedComponent{
|
AddedComponents: []AddedComponent{
|
||||||
|
|
||||||
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
||||||
},
|
},
|
||||||
ExposedComponents: []ExposedComponent{
|
ExposedComponents: []ExposedComponent{
|
||||||
@@ -217,6 +226,9 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
ExtensionPoints: []ExtensionPoint{
|
ExtensionPoints: []ExtensionPoint{
|
||||||
{Id: "myorg-extensions-app/extensions-point-1/v1", Title: "Extension point 1", Description: "Extension points 1 description"},
|
{Id: "myorg-extensions-app/extensions-point-1/v1", Title: "Extension point 1", Description: "Extension points 1 description"},
|
||||||
},
|
},
|
||||||
|
AddedFunctions: []AddedFunction{
|
||||||
|
{Targets: []string{"foo/bar"}, Title: "some hook"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Dependencies: Dependencies{
|
Dependencies: Dependencies{
|
||||||
@@ -271,6 +283,7 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
AddedComponents: []AddedComponent{
|
AddedComponents: []AddedComponent{
|
||||||
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
||||||
},
|
},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -301,8 +314,10 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Type: TypeApp,
|
Type: TypeApp,
|
||||||
|
|
||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -332,8 +347,10 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Type: TypeApp,
|
Type: TypeApp,
|
||||||
|
|
||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -371,6 +388,7 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -200,8 +201,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -248,8 +251,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -309,8 +314,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -423,8 +430,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -504,8 +513,10 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -615,8 +626,10 @@ func TestLoader_Load_CustomSource(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -696,8 +709,10 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -801,8 +816,10 @@ func TestLoader_Load_RBACReady(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -883,8 +900,10 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
|
|||||||
ExposedComponents: []string{},
|
ExposedComponents: []string{},
|
||||||
}},
|
}},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -964,8 +983,10 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -1060,8 +1081,10 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
|
|||||||
{Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-datasource"},
|
{Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-datasource"},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -1272,8 +1295,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -1314,8 +1339,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -1463,8 +1490,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -1512,8 +1541,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
setChromeHeaderHeightHook,
|
setChromeHeaderHeightHook,
|
||||||
setPluginLinksHook,
|
setPluginLinksHook,
|
||||||
setCorrelationsService,
|
setCorrelationsService,
|
||||||
|
setPluginFunctionsHook,
|
||||||
} from '@grafana/runtime';
|
} from '@grafana/runtime';
|
||||||
import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView';
|
import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView';
|
||||||
import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer';
|
import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer';
|
||||||
@@ -89,6 +90,7 @@ import { pluginExtensionRegistries } from './features/plugins/extensions/registr
|
|||||||
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
||||||
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
||||||
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
|
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
|
||||||
|
import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions';
|
||||||
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
|
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
|
||||||
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
|
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
|
||||||
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
|
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
|
||||||
@@ -229,6 +231,7 @@ export class GrafanaApp {
|
|||||||
setPluginLinksHook(usePluginLinks);
|
setPluginLinksHook(usePluginLinks);
|
||||||
setPluginComponentHook(usePluginComponent);
|
setPluginComponentHook(usePluginComponent);
|
||||||
setPluginComponentsHook(usePluginComponents);
|
setPluginComponentsHook(usePluginComponents);
|
||||||
|
setPluginFunctionsHook(usePluginFunctions);
|
||||||
|
|
||||||
// initialize chrome service
|
// initialize chrome service
|
||||||
const queryParams = locationService.getSearchObject();
|
const queryParams = locationService.getSearchObject();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '',
|
grafanaVersion: '',
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export function pluginMetaToPluginConfig(pluginMeta: PluginMeta): AppPluginConfi
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ describe('getRuleOrigin', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '',
|
grafanaVersion: '',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Echo } from 'app/core/services/echo/Echo';
|
|||||||
|
|
||||||
import { ExtensionRegistriesProvider } from '../extensions/ExtensionRegistriesContext';
|
import { ExtensionRegistriesProvider } from '../extensions/ExtensionRegistriesContext';
|
||||||
import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from '../extensions/registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry';
|
||||||
import { getPluginSettings } from '../pluginSettings';
|
import { getPluginSettings } from '../pluginSettings';
|
||||||
@@ -93,6 +94,7 @@ function renderUnderRouter(page = '') {
|
|||||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||||
};
|
};
|
||||||
const pagePath = page ? `/${page}` : '';
|
const pagePath = page ? `/${page}` : '';
|
||||||
const route = {
|
const route = {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
useAddedLinksRegistry,
|
useAddedLinksRegistry,
|
||||||
useAddedComponentsRegistry,
|
useAddedComponentsRegistry,
|
||||||
useExposedComponentsRegistry,
|
useExposedComponentsRegistry,
|
||||||
|
useAddedFunctionsRegistry,
|
||||||
} from '../extensions/ExtensionRegistriesContext';
|
} from '../extensions/ExtensionRegistriesContext';
|
||||||
import { getPluginSettings } from '../pluginSettings';
|
import { getPluginSettings } from '../pluginSettings';
|
||||||
import { importAppPlugin } from '../plugin_loader';
|
import { importAppPlugin } from '../plugin_loader';
|
||||||
@@ -60,6 +61,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
|
|||||||
const addedLinksRegistry = useAddedLinksRegistry();
|
const addedLinksRegistry = useAddedLinksRegistry();
|
||||||
const addedComponentsRegistry = useAddedComponentsRegistry();
|
const addedComponentsRegistry = useAddedComponentsRegistry();
|
||||||
const exposedComponentsRegistry = useExposedComponentsRegistry();
|
const exposedComponentsRegistry = useExposedComponentsRegistry();
|
||||||
|
const addedFunctionsRegistry = useAddedFunctionsRegistry();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [state, dispatch] = useReducer(stateSlice.reducer, initialState);
|
const [state, dispatch] = useReducer(stateSlice.reducer, initialState);
|
||||||
const currentUrl = config.appSubUrl + location.pathname + location.search;
|
const currentUrl = config.appSubUrl + location.pathname + location.search;
|
||||||
@@ -104,6 +106,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
|
|||||||
addedLinksRegistry: addedLinksRegistry.readOnly(),
|
addedLinksRegistry: addedLinksRegistry.readOnly(),
|
||||||
addedComponentsRegistry: addedComponentsRegistry.readOnly(),
|
addedComponentsRegistry: addedComponentsRegistry.readOnly(),
|
||||||
exposedComponentsRegistry: exposedComponentsRegistry.readOnly(),
|
exposedComponentsRegistry: exposedComponentsRegistry.readOnly(),
|
||||||
|
addedFunctionsRegistry: addedFunctionsRegistry.readOnly(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<plugin.root
|
<plugin.root
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PropsWithChildren, createContext, useContext } from 'react';
|
import { PropsWithChildren, createContext, useContext } from 'react';
|
||||||
|
|
||||||
import { AddedComponentsRegistry } from 'app/features/plugins/extensions/registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from 'app/features/plugins/extensions/registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from 'app/features/plugins/extensions/registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from 'app/features/plugins/extensions/registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from 'app/features/plugins/extensions/registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from 'app/features/plugins/extensions/registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from 'app/features/plugins/extensions/registry/ExposedComponentsRegistry';
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ export interface ExtensionRegistriesContextType {
|
|||||||
// Using a different context for each registry to avoid unnecessary re-renders
|
// Using a different context for each registry to avoid unnecessary re-renders
|
||||||
export const AddedLinksRegistryContext = createContext<AddedLinksRegistry | undefined>(undefined);
|
export const AddedLinksRegistryContext = createContext<AddedLinksRegistry | undefined>(undefined);
|
||||||
export const AddedComponentsRegistryContext = createContext<AddedComponentsRegistry | undefined>(undefined);
|
export const AddedComponentsRegistryContext = createContext<AddedComponentsRegistry | undefined>(undefined);
|
||||||
|
export const AddedFunctionsRegistryContext = createContext<AddedFunctionsRegistry | undefined>(undefined);
|
||||||
export const ExposedComponentsRegistryContext = createContext<ExposedComponentsRegistry | undefined>(undefined);
|
export const ExposedComponentsRegistryContext = createContext<ExposedComponentsRegistry | undefined>(undefined);
|
||||||
|
|
||||||
export function useAddedLinksRegistry(): AddedLinksRegistry {
|
export function useAddedLinksRegistry(): AddedLinksRegistry {
|
||||||
@@ -31,6 +33,14 @@ export function useAddedComponentsRegistry(): AddedComponentsRegistry {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useAddedFunctionsRegistry(): AddedFunctionsRegistry {
|
||||||
|
const context = useContext(AddedFunctionsRegistryContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('No `AddedFunctionsRegistry` found.');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
export function useExposedComponentsRegistry(): ExposedComponentsRegistry {
|
export function useExposedComponentsRegistry(): ExposedComponentsRegistry {
|
||||||
const context = useContext(ExposedComponentsRegistryContext);
|
const context = useContext(ExposedComponentsRegistryContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -46,9 +56,11 @@ export const ExtensionRegistriesProvider = ({
|
|||||||
return (
|
return (
|
||||||
<AddedLinksRegistryContext.Provider value={registries.addedLinksRegistry}>
|
<AddedLinksRegistryContext.Provider value={registries.addedLinksRegistry}>
|
||||||
<AddedComponentsRegistryContext.Provider value={registries.addedComponentsRegistry}>
|
<AddedComponentsRegistryContext.Provider value={registries.addedComponentsRegistry}>
|
||||||
<ExposedComponentsRegistryContext.Provider value={registries.exposedComponentsRegistry}>
|
<AddedFunctionsRegistryContext.Provider value={registries.addedFunctionsRegistry}>
|
||||||
{children}
|
<ExposedComponentsRegistryContext.Provider value={registries.exposedComponentsRegistry}>
|
||||||
</ExposedComponentsRegistryContext.Provider>
|
{children}
|
||||||
|
</ExposedComponentsRegistryContext.Provider>
|
||||||
|
</AddedFunctionsRegistryContext.Provider>
|
||||||
</AddedComponentsRegistryContext.Provider>
|
</AddedComponentsRegistryContext.Provider>
|
||||||
</AddedLinksRegistryContext.Provider>
|
</AddedLinksRegistryContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const TITLE_MISSING = 'Title is missing.';
|
|||||||
|
|
||||||
export const DESCRIPTION_MISSING = 'Description is missing.';
|
export const DESCRIPTION_MISSING = 'Description is missing.';
|
||||||
|
|
||||||
|
export const INVALID_EXTENSION_FUNCTION = 'The "fn" argument is invalid, it should be a function.';
|
||||||
|
|
||||||
export const INVALID_CONFIGURE_FUNCTION = 'The "configure" function is invalid. It should be a function.';
|
export const INVALID_CONFIGURE_FUNCTION = 'The "configure" function is invalid. It should be a function.';
|
||||||
|
|
||||||
export const INVALID_PATH_OR_ON_CLICK = 'Either "path" or "onClick" is required.';
|
export const INVALID_PATH_OR_ON_CLICK = 'Either "path" or "onClick" is required.';
|
||||||
@@ -33,6 +35,9 @@ export const TITLE_NOT_MATCHING_META_INFO = 'The "title" doesn\'t match the titl
|
|||||||
export const ADDED_LINK_META_INFO_MISSING =
|
export const ADDED_LINK_META_INFO_MISSING =
|
||||||
'The extension was not recorded in the plugin.json. Added link extensions must be listed in the section "extensions.addedLinks[]". Currently, this is only required in development but will be enforced also in production builds in the future.';
|
'The extension was not recorded in the plugin.json. Added link extensions must be listed in the section "extensions.addedLinks[]". Currently, this is only required in development but will be enforced also in production builds in the future.';
|
||||||
|
|
||||||
|
export const ADDED_FUNCTION_META_INFO_MISSING =
|
||||||
|
'The extension was not recorded in the plugin.json. Added function extensions must be listed in the section "extensions.addedFunction[]". Currently, this is only required in development but will be enforced also in production builds in the future.';
|
||||||
|
|
||||||
export const DESCRIPTION_NOT_MATCHING_META_INFO =
|
export const DESCRIPTION_NOT_MATCHING_META_INFO =
|
||||||
'The "description" doesn\'t match the description recorded in plugin.json.';
|
'The "description" doesn\'t match the description recorded in plugin.json.';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type PluginExtension,
|
|
||||||
PluginExtensionTypes,
|
PluginExtensionTypes,
|
||||||
|
type PluginExtension,
|
||||||
type PluginExtensionLink,
|
type PluginExtensionLink,
|
||||||
type PluginExtensionComponent,
|
type PluginExtensionComponent,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,677 @@
|
|||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import { PluginLoadingStrategy } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { log } from '../logs/log';
|
||||||
|
import { resetLogMock } from '../logs/testUtils';
|
||||||
|
import { isGrafanaDevMode } from '../utils';
|
||||||
|
|
||||||
|
import { AddedFunctionsRegistry } from './AddedFunctionsRegistry';
|
||||||
|
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
|
||||||
|
|
||||||
|
jest.mock('../utils', () => ({
|
||||||
|
...jest.requireActual('../utils'),
|
||||||
|
|
||||||
|
// Manually set the dev mode to false
|
||||||
|
// (to make sure that by default we are testing a production scneario)
|
||||||
|
isGrafanaDevMode: jest.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../logs/log', () => {
|
||||||
|
const { createLogMock } = jest.requireActual('../logs/testUtils');
|
||||||
|
const original = jest.requireActual('../logs/log');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
log: createLogMock(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addedFunctionsRegistry', () => {
|
||||||
|
const originalApps = config.apps;
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const appPluginConfig = {
|
||||||
|
id: pluginId,
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedFunctions: [],
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetLogMock(log);
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
config.apps = {
|
||||||
|
[pluginId]: appPluginConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty registry when no extensions registered', async () => {
|
||||||
|
const addedFunctionsRegistry = new AddedFunctionsRegistry();
|
||||||
|
const observable = addedFunctionsRegistry.asObservable();
|
||||||
|
const registry = await firstValueFrom(observable);
|
||||||
|
expect(registry).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be possible to register function extensions in the registry', async () => {
|
||||||
|
const addedFunctionsRegistry = new AddedFunctionsRegistry();
|
||||||
|
|
||||||
|
addedFunctionsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: jest.fn(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = await addedFunctionsRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'plugins/myorg-basic-app/start': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
extensionPointId: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should be possible to asynchronously register function extensions for the same placement (different plugins)', async () => {
|
||||||
|
const pluginId1 = 'grafana-basic-app';
|
||||||
|
const pluginId2 = 'grafana-basic-app2';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
|
||||||
|
// Register extensions for the first plugin
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId1,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry1 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry1).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId1,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register extensions for the second plugin to a different placement
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId2,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry2 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry2).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId1,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: pluginId2,
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be possible to asynchronously register function extensions for a different placement (different plugin)', async () => {
|
||||||
|
const pluginId1 = 'grafana-basic-app';
|
||||||
|
const pluginId2 = 'grafana-basic-app2';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
|
||||||
|
// Register extensions for the first plugin
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId1,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry1 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry1).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId1,
|
||||||
|
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register extensions for the second plugin to a different placement
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId2,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry2 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry2).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId1,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'plugins/myorg-basic-app/start': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId2,
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
extensionPointId: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be possible to asynchronously register function extensions for the same placement (same plugin)', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
|
||||||
|
// Register extensions for the first extension point
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register extensions to a different extension point
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry2 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry2).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be possible to asynchronously register function extensions for a different placement (same plugin)', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
|
||||||
|
// Register extensions for the first extension point
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register extensions to a different extension point
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry2 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry2).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'plugins/myorg-basic-app/start': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
extensionPointId: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should notify subscribers when the registry changes', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
const observable = reactiveRegistry.asObservable();
|
||||||
|
const subscribeCallback = jest.fn();
|
||||||
|
|
||||||
|
observable.subscribe(subscribeCallback);
|
||||||
|
|
||||||
|
// Register extensions for the first plugin
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
// Register extensions for the first plugin
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: 'another-plugin',
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
|
const registry = subscribeCallback.mock.calls[2][0];
|
||||||
|
|
||||||
|
expect(registry).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: 'another-plugin',
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give the last version of the registry for new subscribers', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
const observable = reactiveRegistry.asObservable();
|
||||||
|
const subscribeCallback = jest.fn();
|
||||||
|
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
observable.subscribe(subscribeCallback);
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const registry = subscribeCallback.mock.calls[0][0];
|
||||||
|
|
||||||
|
expect(registry).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not register a function extension if it has an invalid fn function', () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
const observable = reactiveRegistry.asObservable();
|
||||||
|
const subscribeCallback = jest.fn();
|
||||||
|
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
//@ts-ignore
|
||||||
|
fn: '...',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(log.error).toHaveBeenCalled();
|
||||||
|
|
||||||
|
observable.subscribe(subscribeCallback);
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const registry = subscribeCallback.mock.calls[0][0];
|
||||||
|
expect(registry).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not register a function extension if it has invalid properties (empty title)', () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
const observable = reactiveRegistry.asObservable();
|
||||||
|
const subscribeCallback = jest.fn();
|
||||||
|
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(log.error).toHaveBeenCalled();
|
||||||
|
|
||||||
|
observable.subscribe(subscribeCallback);
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const registry = subscribeCallback.mock.calls[0][0];
|
||||||
|
expect(registry).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be possible to register a function on a read-only registry', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const readOnlyRegistry = registry.readOnly();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
readOnlyRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}).toThrow(MSG_CANNOT_REGISTER_READ_ONLY);
|
||||||
|
|
||||||
|
const currentState = await readOnlyRegistry.getState();
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass down fresh registrations to the read-only version of the registry', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const readOnlyRegistry = registry.readOnly();
|
||||||
|
const subscribeCallback = jest.fn();
|
||||||
|
let readOnlyState;
|
||||||
|
|
||||||
|
// Should have no extensions registered in the beginning
|
||||||
|
readOnlyState = await readOnlyRegistry.getState();
|
||||||
|
expect(Object.keys(readOnlyState)).toHaveLength(0);
|
||||||
|
|
||||||
|
readOnlyRegistry.asObservable().subscribe(subscribeCallback);
|
||||||
|
|
||||||
|
// Register an extension to the original (writable) registry
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// The read-only registry should have received the new extension
|
||||||
|
readOnlyState = await readOnlyRegistry.getState();
|
||||||
|
expect(Object.keys(readOnlyState)).toHaveLength(1);
|
||||||
|
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||||
|
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual(['plugins/myorg-basic-app/start']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not register a function added by a plugin in dev-mode if the meta-info is missing from the plugin.json', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const fnConfig = {
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedFunctions = [];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [fnConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(0);
|
||||||
|
expect(log.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a function added by core Grafana in dev-mode even if the meta-info is missing', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const fnConfig = {
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId: 'grafana',
|
||||||
|
configs: [fnConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(log.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a function added by a plugin in production mode even if the meta-info is missing', async () => {
|
||||||
|
// Production mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const fnConfig = {
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedFunctions = [];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [fnConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(log.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a function added by a plugin in dev-mode if the meta-info is present', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const fnConfig = {
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: ['grafana/dashboard/panel/menu'],
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedFunctions = [fnConfig];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [fnConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(log.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { isFunction } from 'lodash';
|
||||||
|
import { ReplaySubject } from 'rxjs';
|
||||||
|
|
||||||
|
import { PluginExtensionAddedFunctionConfig } from '@grafana/data';
|
||||||
|
|
||||||
|
import * as errors from '../errors';
|
||||||
|
import { isGrafanaDevMode } from '../utils';
|
||||||
|
import { isAddedFunctionMetaInfoMissing } from '../validators';
|
||||||
|
|
||||||
|
import { PluginExtensionConfigs, Registry, RegistryType } from './Registry';
|
||||||
|
|
||||||
|
const logPrefix = 'Could not register function extension. Reason:';
|
||||||
|
|
||||||
|
export type AddedFunctionsRegistryItem = {
|
||||||
|
pluginId: string;
|
||||||
|
title: string;
|
||||||
|
fn: unknown;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AddedFunctionsRegistry extends Registry<AddedFunctionsRegistryItem[], PluginExtensionAddedFunctionConfig> {
|
||||||
|
constructor(
|
||||||
|
options: {
|
||||||
|
registrySubject?: ReplaySubject<RegistryType<AddedFunctionsRegistryItem[]>>;
|
||||||
|
initialState?: RegistryType<AddedFunctionsRegistryItem[]>;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
mapToRegistry(
|
||||||
|
registry: RegistryType<AddedFunctionsRegistryItem[]>,
|
||||||
|
item: PluginExtensionConfigs<PluginExtensionAddedFunctionConfig>
|
||||||
|
): RegistryType<AddedFunctionsRegistryItem[]> {
|
||||||
|
const { pluginId, configs } = item;
|
||||||
|
for (const config of configs) {
|
||||||
|
const configLog = this.logger.child({
|
||||||
|
title: config.title,
|
||||||
|
pluginId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config.title) {
|
||||||
|
configLog.error(`${logPrefix} ${errors.TITLE_MISSING}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFunction(config.fn)) {
|
||||||
|
configLog.error(`${logPrefix} ${errors.INVALID_EXTENSION_FUNCTION}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedFunctionMetaInfoMissing(pluginId, config, configLog)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionPointIds = Array.isArray(config.targets) ? config.targets : [config.targets];
|
||||||
|
for (const extensionPointId of extensionPointIds) {
|
||||||
|
const pointIdLog = configLog.child({ extensionPointId });
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
pluginId,
|
||||||
|
fn: config.fn,
|
||||||
|
description: config.description,
|
||||||
|
title: config.title,
|
||||||
|
extensionPointId,
|
||||||
|
};
|
||||||
|
|
||||||
|
pointIdLog.debug('Added function extension successfully registered');
|
||||||
|
|
||||||
|
if (!(extensionPointId in registry)) {
|
||||||
|
registry[extensionPointId] = [result];
|
||||||
|
} else {
|
||||||
|
registry[extensionPointId].push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a read-only version of the registry.
|
||||||
|
readOnly() {
|
||||||
|
return new AddedFunctionsRegistry({
|
||||||
|
registrySubject: this.registrySubject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ describe('AddedLinksRegistry', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getCoreExtensionConfigurations } from '../getCoreExtensionConfigurations';
|
import { getCoreExtensionConfigurations } from '../getCoreExtensionConfigurations';
|
||||||
|
|
||||||
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './types';
|
import { PluginExtensionRegistries } from './types';
|
||||||
@@ -8,10 +9,12 @@ import { PluginExtensionRegistries } from './types';
|
|||||||
export const addedComponentsRegistry = new AddedComponentsRegistry();
|
export const addedComponentsRegistry = new AddedComponentsRegistry();
|
||||||
export const exposedComponentsRegistry = new ExposedComponentsRegistry();
|
export const exposedComponentsRegistry = new ExposedComponentsRegistry();
|
||||||
export const addedLinksRegistry = new AddedLinksRegistry();
|
export const addedLinksRegistry = new AddedLinksRegistry();
|
||||||
|
export const addedFunctionsRegistry = new AddedFunctionsRegistry();
|
||||||
export const pluginExtensionRegistries: PluginExtensionRegistries = {
|
export const pluginExtensionRegistries: PluginExtensionRegistries = {
|
||||||
addedComponentsRegistry,
|
addedComponentsRegistry,
|
||||||
exposedComponentsRegistry,
|
exposedComponentsRegistry,
|
||||||
addedLinksRegistry,
|
addedLinksRegistry,
|
||||||
|
addedFunctionsRegistry,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Registering core extensions
|
// Registering core extensions
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
||||||
|
|
||||||
export type PluginExtensionRegistries = {
|
export type PluginExtensionRegistries = {
|
||||||
addedComponentsRegistry: AddedComponentsRegistry;
|
addedComponentsRegistry: AddedComponentsRegistry;
|
||||||
exposedComponentsRegistry: ExposedComponentsRegistry;
|
exposedComponentsRegistry: ExposedComponentsRegistry;
|
||||||
|
addedFunctionsRegistry: AddedFunctionsRegistry;
|
||||||
addedLinksRegistry: AddedLinksRegistry;
|
addedLinksRegistry: AddedLinksRegistry;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
|||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { resetLogMock } from './logs/testUtils';
|
import { resetLogMock } from './logs/testUtils';
|
||||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
@@ -78,6 +79,7 @@ describe('usePluginComponent()', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
// This is necessary, so we can register exposed components to the registry during the tests
|
// This is necessary, so we can register exposed components to the registry during the tests
|
||||||
// (Otherwise the registry would reject it in the imitated production mode)
|
// (Otherwise the registry would reject it in the imitated production mode)
|
||||||
exposedComponents: [exposedComponentConfig],
|
exposedComponents: [exposedComponentConfig],
|
||||||
@@ -90,6 +92,7 @@ describe('usePluginComponent()', () => {
|
|||||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||||
};
|
};
|
||||||
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
@@ -122,6 +125,7 @@ describe('usePluginComponent()', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '8.0.0',
|
grafanaVersion: '8.0.0',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
|||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { resetLogMock } from './logs/testUtils';
|
import { resetLogMock } from './logs/testUtils';
|
||||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
@@ -60,6 +61,7 @@ describe('usePluginComponents()', () => {
|
|||||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mocked(wrapWithPluginContext).mockClear();
|
jest.mocked(wrapWithPluginContext).mockClear();
|
||||||
@@ -89,6 +91,7 @@ describe('usePluginComponents()', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '8.0.0',
|
grafanaVersion: '8.0.0',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
|
||||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
@@ -19,6 +20,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
|
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||||
};
|
};
|
||||||
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useObservable } from 'react-use';
|
||||||
|
|
||||||
|
import { usePluginContext, PluginExtensionFunction, PluginExtensionTypes } from '@grafana/data';
|
||||||
|
import { UsePluginFunctionsOptions, UsePluginFunctionsResult } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { useAddedFunctionsRegistry } from './ExtensionRegistriesContext';
|
||||||
|
import * as errors from './errors';
|
||||||
|
import { log } from './logs/log';
|
||||||
|
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||||
|
import { generateExtensionId, getExtensionPointPluginDependencies, isGrafanaDevMode } from './utils';
|
||||||
|
import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators';
|
||||||
|
|
||||||
|
// Returns an array of component extensions for the given extension point
|
||||||
|
export function usePluginFunctions<Signature>({
|
||||||
|
limitPerPlugin,
|
||||||
|
extensionPointId,
|
||||||
|
}: UsePluginFunctionsOptions): UsePluginFunctionsResult<Signature> {
|
||||||
|
const registry = useAddedFunctionsRegistry();
|
||||||
|
const registryState = useObservable(registry.asObservable());
|
||||||
|
const pluginContext = usePluginContext();
|
||||||
|
const deps = getExtensionPointPluginDependencies(extensionPointId);
|
||||||
|
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(deps);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
// For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana.
|
||||||
|
const enableRestrictions = isGrafanaDevMode() && pluginContext;
|
||||||
|
const results: Array<PluginExtensionFunction<Signature>> = [];
|
||||||
|
const extensionsByPlugin: Record<string, number> = {};
|
||||||
|
const pluginId = pluginContext?.meta.id ?? '';
|
||||||
|
const pointLog = log.child({
|
||||||
|
pluginId,
|
||||||
|
extensionPointId,
|
||||||
|
});
|
||||||
|
if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) {
|
||||||
|
pointLog.error(errors.INVALID_EXTENSION_POINT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) {
|
||||||
|
pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING);
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
functions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoadingAppPlugins) {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
functions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const registryItem of registryState?.[extensionPointId] ?? []) {
|
||||||
|
const { pluginId } = registryItem;
|
||||||
|
|
||||||
|
// Only limit if the `limitPerPlugin` is set
|
||||||
|
if (limitPerPlugin && extensionsByPlugin[pluginId] >= limitPerPlugin) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensionsByPlugin[pluginId] === undefined) {
|
||||||
|
extensionsByPlugin[pluginId] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
id: generateExtensionId(pluginId, extensionPointId, registryItem.title),
|
||||||
|
type: PluginExtensionTypes.function,
|
||||||
|
title: registryItem.title,
|
||||||
|
description: registryItem.description ?? '',
|
||||||
|
pluginId: pluginId,
|
||||||
|
fn: registryItem.fn as Signature,
|
||||||
|
});
|
||||||
|
extensionsByPlugin[pluginId] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
functions: results,
|
||||||
|
};
|
||||||
|
}, [extensionPointId, limitPerPlugin, pluginContext, registryState, isLoadingAppPlugins]);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
|||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { resetLogMock } from './logs/testUtils';
|
import { resetLogMock } from './logs/testUtils';
|
||||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
@@ -57,6 +58,7 @@ describe('usePluginLinks()', () => {
|
|||||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||||
};
|
};
|
||||||
resetLogMock(log);
|
resetLogMock(log);
|
||||||
|
|
||||||
@@ -85,6 +87,7 @@ describe('usePluginLinks()', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '8.0.0',
|
grafanaVersion: '8.0.0',
|
||||||
|
|||||||
@@ -475,6 +475,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
@@ -553,6 +554,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
@@ -584,6 +586,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'myorg-third-app': {
|
'myorg-third-app': {
|
||||||
@@ -623,6 +626,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
],
|
],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -679,6 +683,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
],
|
],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
...genereicAppPluginConfig.dependencies,
|
...genereicAppPluginConfig.dependencies,
|
||||||
@@ -705,6 +710,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
...genereicAppPluginConfig.dependencies,
|
...genereicAppPluginConfig.dependencies,
|
||||||
@@ -726,6 +732,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'myorg-sixth-app': {
|
'myorg-sixth-app': {
|
||||||
@@ -763,6 +770,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -791,6 +799,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'myorg-third-app': {
|
'myorg-third-app': {
|
||||||
@@ -825,6 +834,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
...genereicAppPluginConfig.dependencies,
|
...genereicAppPluginConfig.dependencies,
|
||||||
@@ -850,6 +860,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
...genereicAppPluginConfig.dependencies,
|
...genereicAppPluginConfig.dependencies,
|
||||||
@@ -871,6 +882,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -902,6 +914,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ describe('Plugin Extension Validators', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const extensionConfig = {
|
const extensionConfig = {
|
||||||
@@ -387,6 +388,7 @@ describe('Plugin Extension Validators', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const extensionConfig = {
|
const extensionConfig = {
|
||||||
@@ -503,6 +505,7 @@ describe('Plugin Extension Validators', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const exposedComponentConfig = {
|
const exposedComponentConfig = {
|
||||||
@@ -688,6 +691,7 @@ describe('Plugin Extension Validators', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '8.0.0',
|
grafanaVersion: '8.0.0',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
PluginContextType,
|
PluginContextType,
|
||||||
PluginExtensionAddedComponentConfig,
|
PluginExtensionAddedComponentConfig,
|
||||||
PluginExtensionExposedComponentConfig,
|
PluginExtensionExposedComponentConfig,
|
||||||
|
PluginExtensionAddedFunctionConfig,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { PluginAddedLinksConfigureFunc, PluginExtensionPoints } from '@grafana/data/src/types/pluginExtensions';
|
import { PluginAddedLinksConfigureFunc, PluginExtensionPoints } from '@grafana/data/src/types/pluginExtensions';
|
||||||
import { config, isPluginExtensionLink } from '@grafana/runtime';
|
import { config, isPluginExtensionLink } from '@grafana/runtime';
|
||||||
@@ -160,6 +161,38 @@ export const isAddedLinkMetaInfoMissing = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isAddedFunctionMetaInfoMissing = (
|
||||||
|
pluginId: string,
|
||||||
|
metaInfo: PluginExtensionAddedFunctionConfig,
|
||||||
|
log: ExtensionsLog
|
||||||
|
) => {
|
||||||
|
const logPrefix = 'Could not register function extension. Reason:';
|
||||||
|
const app = config.apps[pluginId];
|
||||||
|
const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.find(({ title }) => title === metaInfo.title) : null;
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pluginJsonMetaInfo) {
|
||||||
|
log.error(`${logPrefix} ${errors.ADDED_FUNCTION_META_INFO_MISSING}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets];
|
||||||
|
if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) {
|
||||||
|
log.error(`${logPrefix} ${errors.TARGET_NOT_MATCHING_META_INFO}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginJsonMetaInfo.description !== metaInfo.description) {
|
||||||
|
log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
export const isAddedComponentMetaInfoMissing = (
|
export const isAddedComponentMetaInfoMissing = (
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
metaInfo: PluginExtensionAddedComponentConfig,
|
metaInfo: PluginExtensionAddedComponentConfig,
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
|
|||||||
if (!plugin.panel && plugin.angularPanelCtrl) {
|
if (!plugin.panel && plugin.angularPanelCtrl) {
|
||||||
plugin.panel = getAngularPanelReactWrapper(plugin);
|
plugin.panel = getAngularPanelReactWrapper(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugin;
|
return plugin;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ import { DataQuery } from '@grafana/schema';
|
|||||||
import { GenericDataSourcePlugin } from '../datasources/types';
|
import { GenericDataSourcePlugin } from '../datasources/types';
|
||||||
|
|
||||||
import builtInPlugins from './built_in_plugins';
|
import builtInPlugins from './built_in_plugins';
|
||||||
import { addedComponentsRegistry, addedLinksRegistry, exposedComponentsRegistry } from './extensions/registry/setup';
|
import {
|
||||||
|
addedComponentsRegistry,
|
||||||
|
addedFunctionsRegistry,
|
||||||
|
addedLinksRegistry,
|
||||||
|
exposedComponentsRegistry,
|
||||||
|
} from './extensions/registry/setup';
|
||||||
import { getPluginFromCache, registerPluginInCache } from './loader/cache';
|
import { getPluginFromCache, registerPluginInCache } from './loader/cache';
|
||||||
// SystemJS has to be imported before the sharedDependenciesMap
|
// SystemJS has to be imported before the sharedDependenciesMap
|
||||||
import { SystemJS } from './loader/systemjs';
|
import { SystemJS } from './loader/systemjs';
|
||||||
@@ -153,7 +158,6 @@ export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<Gene
|
|||||||
dsPlugin.meta = meta;
|
dsPlugin.meta = meta;
|
||||||
return dsPlugin;
|
return dsPlugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pluginExports.Datasource) {
|
if (pluginExports.Datasource) {
|
||||||
const dsPlugin = new DataSourcePlugin<
|
const dsPlugin = new DataSourcePlugin<
|
||||||
DataSourceApi<DataQuery, DataSourceJsonData>,
|
DataSourceApi<DataQuery, DataSourceJsonData>,
|
||||||
@@ -205,6 +209,10 @@ export async function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
|
|||||||
pluginId,
|
pluginId,
|
||||||
configs: plugin.addedLinkConfigs || [],
|
configs: plugin.addedLinkConfigs || [],
|
||||||
});
|
});
|
||||||
|
addedFunctionsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: plugin.addedFunctionConfigs || [],
|
||||||
|
});
|
||||||
|
|
||||||
importedAppPlugins[pluginId] = plugin;
|
importedAppPlugins[pluginId] = plugin;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user