Plugins: Support for link extensions (#61663)

* added extensions to plugin.json and exposing it via frontend settings.

* added extensions to the plugin.json schema.

* changing the extensions in frontend settings to a map instead of an array.

* wip

* feat(pluginregistry): begin wiring up registry

* feat(pluginextensions): prevent duplicate links and clean up

* added test case for link extensions.

* added tests and implemented the getPluginLink function.

* wip

* feat(pluginextensions): expose plugin extension registry

* fix(pluginextensions): appease the typescript gods post rename

* renamed file and will throw error if trying to call setExtensionsRegistry if trying to call it twice.

* added reafactorings.

* fixed failing test.

* minor refactorings to make sure we only include extensions if the app is enabled.

* fixed some nits.

* Update public/app/features/plugins/extensions/registry.test.ts

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>

* Update packages/grafana-runtime/src/services/pluginExtensions/registry.ts

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>

* Update packages/grafana-runtime/src/services/pluginExtensions/registry.ts

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>

* Update public/app/features/plugins/extensions/registry.test.ts

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>

* Moved types for extensions from data to runtime.

* added a small example on how you could consume link extensions.

* renamed after feedback from levi.

* updated the plugindef.cue.

* using the generated plugin def.

* added tests for apps and extensions.

* fixed linting issues.

* wip

* wip

* wip

* wip

* test(extensions): fix up failing tests

* feat(extensions): freeze registry extension arrays, include type in registry items

* added restrictions in the pugindef cue schema.

* wip

* added required fields.

* added key to uniquely identify each item.

* test(pluginextensions): align tests with implementation

* chore(schema): refresh reference.md

---------

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
This commit is contained in:
Marcus Andersson
2023-02-07 17:20:05 +01:00
committed by GitHub
parent 8a94688114
commit 1cfd3f81fb
24 changed files with 812 additions and 98 deletions

View File

@ -14,7 +14,6 @@ import {
MapLayerOptions,
OAuthSettings,
PanelPluginMeta,
PreloadPlugin,
systemDateFormats,
SystemDateFormatSettings,
NewThemeOptions,
@ -25,11 +24,32 @@ export interface AzureSettings {
managedIdentityEnabled: boolean;
}
export enum PluginExtensionTypes {
link = 'link',
}
export type PluginsExtensionLinkConfig = {
target: string;
type: PluginExtensionTypes.link;
title: string;
description: string;
path: string;
};
export type AppPluginConfig = {
id: string;
path: string;
version: string;
preload: boolean;
extensions?: PluginsExtensionLinkConfig[];
};
export class GrafanaBootConfig implements GrafanaConfig {
isPublicDashboardView: boolean;
snapshotEnabled = true;
datasources: { [str: string]: DataSourceInstanceSettings } = {};
panels: { [key: string]: PanelPluginMeta } = {};
apps: Record<string, AppPluginConfig> = {};
auth: AuthSettings = {};
minRefreshInterval = '';
appUrl = '';
@ -77,7 +97,6 @@ export class GrafanaBootConfig implements GrafanaConfig {
/** @deprecated Use `theme2` instead. */
theme: GrafanaTheme;
theme2: GrafanaTheme2;
pluginsToPreload: PreloadPlugin[] = [];
featureToggles: FeatureToggles = {};
licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false;

View File

@ -8,3 +8,10 @@ export * from './legacyAngularInjector';
export * from './live';
export * from './LocationService';
export * from './appEvents';
export { setPluginsExtensionRegistry } from './pluginExtensions/registry';
export type { PluginsExtensionRegistry, PluginsExtensionLink, PluginsExtension } from './pluginExtensions/registry';
export {
type GetPluginExtensionsOptions,
type PluginExtensionsResult,
getPluginExtensions,
} from './pluginExtensions/extensions';

View File

@ -0,0 +1,51 @@
import { getPluginExtensions, PluginExtensionsMissingError } from './extensions';
import { setPluginsExtensionRegistry } from './registry';
describe('getPluginExtensions', () => {
describe('when getting a registered extension link', () => {
const pluginId = 'grafana-basic-app';
const linkId = 'declare-incident';
beforeAll(() => {
setPluginsExtensionRegistry({
[`plugins/${pluginId}/${linkId}`]: [
{
type: 'link',
title: 'Declare incident',
description: 'Declaring an incident in the app',
href: `/a/${pluginId}/declare-incident`,
key: 1,
},
],
});
});
it('should return a collection of extensions to the plugin', () => {
const { extensions, error } = getPluginExtensions({
target: `plugins/${pluginId}/${linkId}`,
});
expect(extensions[0].href).toBe(`/a/${pluginId}/declare-incident`);
expect(error).toBeUndefined();
});
it('should return a description for the requested link', () => {
const { extensions, error } = getPluginExtensions({
target: `plugins/${pluginId}/${linkId}`,
});
expect(extensions[0].href).toBe(`/a/${pluginId}/declare-incident`);
expect(extensions[0].description).toBe('Declaring an incident in the app');
expect(error).toBeUndefined();
});
it('should return an empty array when no links can be found', () => {
const { extensions, error } = getPluginExtensions({
target: `an-unknown-app/${linkId}`,
});
expect(extensions.length).toBe(0);
expect(error).toBeInstanceOf(PluginExtensionsMissingError);
});
});
});

View File

@ -0,0 +1,34 @@
import { getPluginsExtensionRegistry, PluginsExtension } from './registry';
export type GetPluginExtensionsOptions = {
target: string;
};
export type PluginExtensionsResult = {
extensions: PluginsExtension[];
error?: Error;
};
export class PluginExtensionsMissingError extends Error {
readonly target: string;
constructor(target: string) {
super(`Could not find extensions for '${target}'`);
this.target = target;
this.name = PluginExtensionsMissingError.name;
}
}
export function getPluginExtensions({ target }: GetPluginExtensionsOptions): PluginExtensionsResult {
const registry = getPluginsExtensionRegistry();
const extensions = registry[target];
if (!Array.isArray(extensions)) {
return {
extensions: [],
error: new PluginExtensionsMissingError(target),
};
}
return { extensions };
}

View File

@ -0,0 +1,27 @@
export type PluginsExtensionLink = {
type: 'link';
title: string;
description: string;
href: string;
key: number;
};
export type PluginsExtension = PluginsExtensionLink;
export type PluginsExtensionRegistry = Record<string, PluginsExtension[]>;
let registry: PluginsExtensionRegistry | undefined;
export function setPluginsExtensionRegistry(instance: PluginsExtensionRegistry): void {
if (registry) {
throw new Error('setPluginsExtensionRegistry function should only be called once, when Grafana is starting.');
}
registry = instance;
}
export function getPluginsExtensionRegistry(): PluginsExtensionRegistry {
if (!registry) {
throw new Error('getPluginsExtensionRegistry can only be used after the Grafana instance has started.');
}
return registry;
}