mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 04:22:13 +08:00
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:
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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 };
|
||||
}
|
@ -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;
|
||||
}
|
Reference in New Issue
Block a user