Plugins: Allow apps to expose components. Update the extensions API. (#87236)
* feat: introduce exposable components and update the public APIs Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * tests: fix the tests for `usePluginComponent()` I broke them when I wrapped the component with the PluginContextProvider which fetches the plugin metadata. * fix: typo --------- Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
This commit is contained in:
co-authored by
Marcus Andersson
parent
27a791db12
commit
ebe42e1ada
@@ -102,23 +102,79 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
||||
return this._extensionConfigs;
|
||||
}
|
||||
|
||||
configureExtensionLink<Context extends object>(extension: Omit<PluginExtensionLinkConfig<Context>, 'type'>) {
|
||||
this._extensionConfigs.push({
|
||||
...extension,
|
||||
type: PluginExtensionTypes.link,
|
||||
} as PluginExtensionLinkConfig);
|
||||
addLink<Context extends object>(
|
||||
extensionConfig: { targets: string | string[] } & Omit<
|
||||
PluginExtensionLinkConfig<Context>,
|
||||
'type' | 'extensionPointId'
|
||||
>
|
||||
) {
|
||||
const { targets, ...extension } = extensionConfig;
|
||||
const targetsArray = Array.isArray(targets) ? targets : [targets];
|
||||
|
||||
targetsArray.forEach((target) => {
|
||||
this._extensionConfigs.push({
|
||||
...extension,
|
||||
extensionPointId: target,
|
||||
type: PluginExtensionTypes.link,
|
||||
} as PluginExtensionLinkConfig);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
configureExtensionComponent<Props = {}>(extension: Omit<PluginExtensionComponentConfig<Props>, 'type'>) {
|
||||
addComponent<Props = {}>(
|
||||
extensionConfig: { targets: string | string[] } & Omit<
|
||||
PluginExtensionComponentConfig<Props>,
|
||||
'type' | 'extensionPointId'
|
||||
>
|
||||
) {
|
||||
const { targets, ...extension } = extensionConfig;
|
||||
const targetsArray = Array.isArray(targets) ? targets : [targets];
|
||||
|
||||
targetsArray.forEach((target) => {
|
||||
this._extensionConfigs.push({
|
||||
...extension,
|
||||
extensionPointId: target,
|
||||
type: PluginExtensionTypes.component,
|
||||
} as PluginExtensionComponentConfig);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
exposeComponent<Props = {}>(
|
||||
componentConfig: { id: string } & Omit<PluginExtensionComponentConfig<Props>, 'type' | 'extensionPointId'>
|
||||
) {
|
||||
const { id, ...extension } = componentConfig;
|
||||
|
||||
this._extensionConfigs.push({
|
||||
...extension,
|
||||
extensionPointId: `capabilities/${id}`,
|
||||
type: PluginExtensionTypes.component,
|
||||
} as PluginExtensionComponentConfig);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @deprecated Use .addLink() instead */
|
||||
configureExtensionLink<Context extends object>(extension: Omit<PluginExtensionLinkConfig<Context>, 'type'>) {
|
||||
this.addLink({
|
||||
targets: [extension.extensionPointId],
|
||||
...extension,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @deprecated Use .addComponent() instead */
|
||||
configureExtensionComponent<Props = {}>(extension: Omit<PluginExtensionComponentConfig<Props>, 'type'>) {
|
||||
this.addComponent({
|
||||
targets: [extension.extensionPointId],
|
||||
...extension,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,8 +54,10 @@ export type PluginExtensionLinkConfig<Context extends object = object> = {
|
||||
// (It is called with the original event object)
|
||||
onClick?: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
||||
|
||||
// The unique identifier of the Extension Point
|
||||
// (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
|
||||
/**
|
||||
* The unique identifier of the Extension Point
|
||||
* (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
|
||||
*/
|
||||
extensionPointId: string;
|
||||
|
||||
// (Optional) A function that can be used to configure the extension dynamically based on the extension point's context
|
||||
@@ -86,8 +88,10 @@ export type PluginExtensionComponentConfig<Props = {}> = {
|
||||
// (This component receives contextual information as props when it is rendered. You can just return `null` from the component to hide it.)
|
||||
component: React.ComponentType<Props>;
|
||||
|
||||
// The unique identifier of the Extension Point
|
||||
// (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
|
||||
/**
|
||||
* The unique identifier of the Extension Point
|
||||
* (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
|
||||
*/
|
||||
extensionPointId: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -19,11 +19,17 @@ export {
|
||||
type GetPluginExtensionsResult,
|
||||
type UsePluginExtensions,
|
||||
type UsePluginExtensionsResult,
|
||||
type UsePluginComponentResult,
|
||||
} from './pluginExtensions/getPluginExtensions';
|
||||
export {
|
||||
setPluginExtensionsHook,
|
||||
usePluginExtensions,
|
||||
usePluginLinkExtensions,
|
||||
usePluginComponentExtensions,
|
||||
usePluginComponents,
|
||||
usePluginLinks,
|
||||
} from './pluginExtensions/usePluginExtensions';
|
||||
|
||||
export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent';
|
||||
|
||||
export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils';
|
||||
|
||||
@@ -25,6 +25,11 @@ export type UsePluginExtensionsResult<T = PluginExtension> = {
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export type UsePluginComponentResult<Props = {}> = {
|
||||
component: React.ComponentType<Props> | undefined | null;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
let singleton: GetPluginExtensions | undefined;
|
||||
|
||||
export function setPluginExtensionGetter(instance: GetPluginExtensions): void {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { UsePluginComponentResult } from './getPluginExtensions';
|
||||
|
||||
export type UsePluginComponent<Props extends object = {}> = (id: string) => UsePluginComponentResult<Props>;
|
||||
|
||||
let singleton: UsePluginComponent | undefined;
|
||||
|
||||
export function setPluginComponentHook(hook: UsePluginComponent): void {
|
||||
// We allow overriding the registry in tests
|
||||
if (singleton && process.env.NODE_ENV !== 'test') {
|
||||
throw new Error('setPluginComponentHook() function should only be called once, when Grafana is starting.');
|
||||
}
|
||||
singleton = hook;
|
||||
}
|
||||
|
||||
export function usePluginComponent<Props extends object = {}>(id: string): UsePluginComponentResult<Props> {
|
||||
if (!singleton) {
|
||||
throw new Error('setPluginComponentHook(options) can only be used after the Grafana instance has started.');
|
||||
}
|
||||
return singleton(id) as UsePluginComponentResult<Props>;
|
||||
}
|
||||
@@ -15,6 +15,9 @@ export function setPluginExtensionsHook(hook: UsePluginExtensions): void {
|
||||
singleton = hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use either usePluginLinks() or usePluginComponents() instead.
|
||||
*/
|
||||
export function usePluginExtensions(options: GetPluginExtensionsOptions): UsePluginExtensionsResult {
|
||||
if (!singleton) {
|
||||
throw new Error('usePluginExtensions(options) can only be used after the Grafana instance has started.');
|
||||
@@ -22,6 +25,37 @@ export function usePluginExtensions(options: GetPluginExtensionsOptions): UsePlu
|
||||
return singleton(options);
|
||||
}
|
||||
|
||||
export function usePluginLinks(options: GetPluginExtensionsOptions): {
|
||||
links: PluginExtensionLink[];
|
||||
isLoading: boolean;
|
||||
} {
|
||||
const { extensions, isLoading } = usePluginExtensions(options);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
links: extensions.filter(isPluginExtensionLink),
|
||||
isLoading,
|
||||
};
|
||||
}, [extensions, isLoading]);
|
||||
}
|
||||
|
||||
export function usePluginComponents<Props = {}>(
|
||||
options: GetPluginExtensionsOptions
|
||||
): { components: Array<PluginExtensionComponent<Props>>; isLoading: boolean } {
|
||||
const { extensions, isLoading } = usePluginExtensions(options);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
components: extensions.filter(isPluginExtensionComponent) as Array<PluginExtensionComponent<Props>>,
|
||||
isLoading,
|
||||
}),
|
||||
[extensions, isLoading]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use usePluginLinks() instead.
|
||||
*/
|
||||
export function usePluginLinkExtensions(
|
||||
options: GetPluginExtensionsOptions
|
||||
): UsePluginExtensionsResult<PluginExtensionLink> {
|
||||
@@ -35,6 +69,9 @@ export function usePluginLinkExtensions(
|
||||
}, [extensions, isLoading]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use usePluginComponents() instead.
|
||||
*/
|
||||
export function usePluginComponentExtensions<Props = {}>(
|
||||
options: GetPluginExtensionsOptions
|
||||
): { extensions: Array<PluginExtensionComponent<Props>>; isLoading: boolean } {
|
||||
|
||||
Reference in New Issue
Block a user