Create an extension point
In this guide you will learn how to provide an extension point so that app plugins can add their extensions to your plugin.
What is an extension point?
An extension point is a place in the UI which you make extendable by other plugins using extensions. These extensions can either be links or React components that can implement virtually anything.
In the extension point you can control how you want to handle the displaying of extensions to users. You can also share contextual information with the extensions using a context
object or component props. Refer to the examples below to learn more.
You can also share contextual information with the extensions using a context
object or component props (see the following examples).
Requirements
- an extension point ID (string)
- In case it's an extension point in core Grafana, it must start with
grafana/
- In case it's inside a plugin, it must start with
plugin/<PLUGIN_ID>/
- It must be unique
- In case it's an extension point in core Grafana, it must start with
- an app plugin - apart from core Grafana, currently only app plugins can create extension points and register extensions.
When designing the UI make sure the extension point supports a scenario where multiple extensions can be added without breaking the UI. Also consider if there is any information from the current view that should be shared with the extensions added to the extension point. It could be information from the current view that could let the extending plugin pre-fill values or other data in the extension's functionality.
Create an extension point
When you create an extension point in a plugin, you create a public interface for other plugins to interact with. Changes to the extension point ID or its context could break any plugin that attempts to register a link inside your plugin.
You can easily create an extension point using the following functions (they live in @grafana/runtime
) to fetch extensions for a certain extension point ID:
The usePluginExtensions()
hook
In case the context
object is created dynamically, make sure to wrap it into a useMemo()
to prevent unnecesssary rerenders. More info
The usePluginExtensions
React hooks return the list of extensions that are registered for a certain extension point ID. The hook dynamically updates its return value when the list of extensions changes. This behavior usually happens when extensions are registered during runtime due to dynamic plugin loading.
Syntax
usePluginExtensions(options);
usePluginLinkExtensions(options); // Only returns extensions that have type `type="link"`
usePluginComponentExtensions(options); // Only returns extensions that have type `type="component"`
Parameters
options.extensionPointId
- string
The unique identifier of your extension point. It must begin with plugins/<PLUGIN_ID>
for plugins and grafana/
for core Grafana extension points. For example: plugins/myorg-super-app
.
options?.context
- object (Optional)
An object containing information related to your extension point that you would like to share with the extensions. For example: { baseUrl: '/foo/bar' }
. This parameter is not available for component extensions, instead you should pass contextual information using the component props.
The provided context object is made immutable before being shared with the extensions.
options?.limitPerPlugin
- number (Optional)
Use this parameter to set the maximum value for how many extensions should be returned from the same plugin. It can be useful in cases when there is limited space on the UI to display extensions.
Return value
The hooks return an object in the following format:
// usePluginExtensions()
{
isLoading: boolean;
extensions: Array<PluginExtensionLink | PluginExtensionComponent>
}
// usePluginLinkExtensions()
{
isLoading: boolean;
extensions: PluginExtensionLink[]
}
// usePluginComponentExtensions()
{
isLoading: boolean;
extensions: PluginExtensionComponent[]
}
(For more information check the type definitions of PluginExtensionLink
and PluginExtensionComponent
.)
Example - rendering link extensions (static context)
The following example shows how to render a link component as link-type extensions that other plugins registered for the plugins/another-app-plugin/menu
extension point ID.
import { usePluginLinkExtensions } from '@grafana/runtime';
// We define the `context` outside of the React component for performance reasons.
// (Declaring it inside the component would result in a new object on every render,
// which would unnecessarily trigger the `usePluginLinkExtensions()` hook.)
const context = {
referenceId: '12345',
timeZone: 'UTC',
};
function AppMenuExtensionPoint() {
// This only returns type="link" extensions
const { extensions } = usePluginLinkExtensions({
extensionPointId: 'plugins/another-app-plugin/menu',
context,
});
if (extensions.length === 0) {
return null;
}
return (
<div>
{extensions.map((extension) => {
return (
<a href={extension.path} onClick={extension.onClick} title={extension.description} key={extension.key}>
{extension.title}
</a>
);
})}
</div>
);
}
Example - rendering link extensions (dynamic context)
The following example shows how to create the context object dynamically. Although this is a common practice, you should be aware that the usePluginLinkExtensions()
hook will re-render in the following scenarios:
- If the
context
object changes (so the extensions can react to the context changes) - If the extension-registry changes
Be sure to only change the context
object if its content changes; otherwise, you could create unnecessary re-renders. The following example shows how to approach these scenarios in a safe way:
import { useMemo } from 'react';
import { usePluginLinkExtensions } from '@grafana/runtime';
function AppMenuExtensionPoint({ referenceId }) {
// Instead of defining the object here (which would result in a new object on every render),
// we use `useMemo()` to only update the context object when its "dynamic" dependencies change.
const context = useMemo(
() => ({
referenceId,
timeZone: 'UTC',
}),
[referenceId]
);
const { extensions } = usePluginLinkExtensions({
extensionPointId: 'plugins/another-app-plugin/menu',
context,
});
if (extensions.length === 0) {
return null;
}
return (
<div>
{extensions.map((extension) => {
return (
<a href={extension.path} onClick={extension.onClick} title={extension.description} key={extension.key}>
{extension.title}
</a>
);
})}
</div>
);
}
Example - rendering component extensions
Component type extensions are simple React components, which gives you much more freedom in what you can make them do. You can pass contextual information to the extension components using props.
import { usePluginComponentExtensions } from '@grafana/runtime';
export const Toolbar = () => {
// This only returns type="component" extensions
// Heads up! We don't specify a context object below, we pass in the contextual information as a prop to the component later.
const { extensions } = usePluginComponentExtensions({ extensionPointId: '<extension-point-id>' });
return (
<div>
<div className="title">Title</div>
<div className="extensions">
{/* Loop through the available extensions */}
{extensions.map((extension) => {
const Component = extension.component as React.ComponentType<{
version: string;
}>;
// Render extension component and pass contextual information (version)
return (
<div key={extension.id}>
<Component version="1.0.0" />
</div>
);
})}
</div>
</div>
);
};
The getPluginExtensions()
method - deprecated
The getPluginExtensions
method takes an object consisting of the extensionPointId
, which must begin plugins/<PLUGIN_ID>
, and any contextual information that you want to provide. The getPluginLinkExtensions
method returns a list of extension links that your program can then loop over.
This function only returns the state of the extensions registry (the extensions registered by plugins) at a given time. If there are extensions registered by plugins after that point in time, you won't receive them.
As a best practice, use the reactive usePluginExtensions()
hook instead wherever possible.
Syntax
getPluginExtensions(options);
getPluginLinkExtensions(options); // Only returns extensions that have type `type="link"`
getPluginComponentExtensions(options); // Only returns extensions that have type `type="component"`
Parameters
options.extensionPointId
- string
The unique identifier of your extension point. It must begin with plugins/<PLUGIN_ID>
. For example: plugins/myorg-super-app
.
options?.context
- object (Optional)
As an arbitrary object, it contains information related to your extension point which you would like to share with the extensions. For example: { baseUrl: '/foo/bar' }
.
This parameter is not available for component extensions; for those, you can pass contextual information using the component props.
The provided context object always gets frozen (turned immutable) before being shared with the extensions.
options?.limitPerPlugin
- number (Optional)
Use this method to specify the maximum amount of extensions that should be returned from the same plugin. It can be useful in cases when there is limited space on the UI to display extensions.
Return value
getPluginExtensions()
- returns a mixed list ofPluginExtensionLink
andPluginExtensionComponent
getPluginLinkExtensions()
- returns a list ofPluginExtensionLink
getPluginComponentExtensions()
- returns a list ofPluginExtensionComponent
Example - rendering link extensions
In the following example, a <LinkButton />
-component is rendered for all link extensions that other plugins registered for the plugins/another-app-plugin/menu
extension point ID.
import { getPluginLinkExtensions } from '@grafana/runtime';
import { LinkButton } from '@grafana/ui';
function AppMenuExtensionPoint() {
// This only returns type="link" extensions
const { extensions } = getPluginLinkExtensions({
extensionPointId: 'plugins/another-app-plugin/menu',
context: {
referenceId: '12345',
timeZone: 'UTC',
},
});
if (extensions.length === 0) {
return null;
}
return (
<div>
{extensions.map((extension) => {
return (
<LinkButton
href={extension.path}
onClick={extension.onClick}
title={extension.description}
key={extension.key}
>
{extension.title}
</LinkButton>
);
})}
</div>
);
}
Example - rendering component extensions
Available in Grafana >=10.1.0
(Component type extensions are only available in Grafana 10.1.0 and above.)
Component type extensions are simple React components, which gives much more freedom on what they are able to do. In case you would like to make some part of your plugin extendable by other plugins, you can create a component-type extension point using getPluginComponentExtensions()
. Any contextual information can be shared with the extension components using the context={}
prop (see the example below).
import { getPluginComponentExtensions } from '@grafana/runtime';
export const Toolbar = () => {
// This only returns type="component" extensions
// Heads up! We don't specify a context object below, we pass in the contextual information as a prop to the component later.
const { extensions } = getPluginComponentExtensions({ extensionPointId: '<extension-point-id>' });
const version = '1.0.0'; // Let's share this with the extensions
return (
<div>
<div className="title">Title</div>
<div className="extensions">
{/* Loop through the available extensions */}
{extensions.map((extension) => {
const Component = extension.component as React.ComponentType<{
version: string;
}>;
// Render extension component and pass contextual information (version)
return (
<div key={extension.id}>
<Component version="1.0.0" />
</div>
);
})}
</div>
</div>
);
};