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

@@ -0,0 +1,100 @@
import { AppPluginConfig, PluginExtensionTypes, PluginsExtensionLinkConfig } from '@grafana/runtime';
import { createPluginExtensionsRegistry } from './registry';
describe('Plugin registry', () => {
describe('createPluginExtensionsRegistry function', () => {
const registry = createPluginExtensionsRegistry({
'belugacdn-app': createConfig([
{
target: 'plugins/belugacdn-app/menu',
title: 'The title',
type: PluginExtensionTypes.link,
description: 'Incidents are occurring!',
path: '/incidents/declare',
},
]),
'strava-app': createConfig([
{
target: 'plugins/strava-app/menu',
title: 'The title',
type: PluginExtensionTypes.link,
description: 'Incidents are occurring!',
path: '/incidents/declare',
},
]),
'duplicate-links-app': createConfig([
{
target: 'plugins/duplicate-links-app/menu',
title: 'The title',
type: PluginExtensionTypes.link,
description: 'Incidents are occurring!',
path: '/incidents/declare',
},
{
target: 'plugins/duplicate-links-app/menu',
title: 'The title',
type: PluginExtensionTypes.link,
description: 'Incidents are occurring!',
path: '/incidents/declare2',
},
]),
'no-extensions-app': createConfig(undefined),
});
it('should configure a registry link', () => {
const [link] = registry['plugins/belugacdn-app/menu'];
expect(link).toEqual({
title: 'The title',
type: 'link',
description: 'Incidents are occurring!',
href: '/a/belugacdn-app/incidents/declare',
key: 539074708,
});
});
it('should configure all registry targets', () => {
const numberOfTargets = Object.keys(registry).length;
expect(numberOfTargets).toBe(3);
});
it('should configure registry targets from multiple plugins', () => {
const [pluginALink] = registry['plugins/belugacdn-app/menu'];
const [pluginBLink] = registry['plugins/strava-app/menu'];
expect(pluginALink).toEqual({
title: 'The title',
type: 'link',
description: 'Incidents are occurring!',
href: '/a/belugacdn-app/incidents/declare',
key: 539074708,
});
expect(pluginBLink).toEqual({
title: 'The title',
type: 'link',
description: 'Incidents are occurring!',
href: '/a/strava-app/incidents/declare',
key: -1637066384,
});
});
it('should configure multiple links for a single target', () => {
const links = registry['plugins/duplicate-links-app/menu'];
expect(links.length).toBe(2);
});
});
});
function createConfig(extensions?: PluginsExtensionLinkConfig[]): AppPluginConfig {
return {
id: 'myorg-basic-app',
preload: false,
path: '',
version: '',
extensions,
};
}

View File

@@ -0,0 +1,54 @@
import {
AppPluginConfig,
PluginExtensionTypes,
PluginsExtensionLinkConfig,
PluginsExtensionRegistry,
PluginsExtensionLink,
} from '@grafana/runtime';
export function createPluginExtensionsRegistry(apps: Record<string, AppPluginConfig> = {}): PluginsExtensionRegistry {
const registry: PluginsExtensionRegistry = {};
for (const [pluginId, config] of Object.entries(apps)) {
const extensions = config.extensions;
if (!Array.isArray(extensions)) {
continue;
}
for (const extension of extensions) {
const target = extension.target;
const item = createRegistryItem(pluginId, extension);
if (!Array.isArray(registry[target])) {
registry[target] = [item];
continue;
}
registry[target].push(item);
continue;
}
}
for (const key of Object.keys(registry)) {
Object.freeze(registry[key]);
}
return Object.freeze(registry);
}
function createRegistryItem(pluginId: string, extension: PluginsExtensionLinkConfig): PluginsExtensionLink {
const href = `/a/${pluginId}${extension.path}`;
return Object.freeze({
type: PluginExtensionTypes.link,
title: extension.title,
description: extension.description,
href: href,
key: hashKey(`${extension.title}${href}`),
});
}
function hashKey(key: string): number {
return Array.from(key).reduce((s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0, 0);
}

View File

@@ -1,12 +1,13 @@
import { PreloadPlugin } from '@grafana/data';
import { AppPluginConfig } from '@grafana/runtime';
import { importPluginModule } from './plugin_loader';
export async function preloadPlugins(pluginsToPreload: PreloadPlugin[] = []): Promise<void> {
export async function preloadPlugins(apps: Record<string, AppPluginConfig> = {}): Promise<void> {
const pluginsToPreload = Object.values(apps).filter((app) => app.preload);
await Promise.all(pluginsToPreload.map(preloadPlugin));
}
async function preloadPlugin(plugin: PreloadPlugin): Promise<void> {
async function preloadPlugin(plugin: AppPluginConfig): Promise<void> {
const { path, version } = plugin;
try {
await importPluginModule(path, version);