Create an extension point
An extension point is a part of your plugin UI or Grafana UI where other plugins can add links or React components via hooks. You can use them to extend the user experience based on a context exposed by the extension point.
Read more about extensions under key concepts.
Type | Description |
---|---|
Link | Links have a path or an onClick() property. When to use? Use links if you would like to give plugins a way to define custom user actions for a part of your UI. These actions can either just be cross-links to the plugin, or using onClick() methods they can implement a more interactive on-page experience with a modal. API reference - addLink() - registering a link from a plugin - usePluginLinks() - fetching links registered for an extension point |
Component | Components are React components that can be used to render a custom user experience. When to use? Use components if you would like to give more freedom for plugins to extend your UI, for example to extend a configuration form with custom parts. API reference - addComponent() - registering a component from a plugin - usePluginComponents() - fetching components registered for an extension point |
Links​
Best practices for rendering links​
-
Make sure your UI handles multiple links
Multiple plugins may add links to your extension point. Make sure your extension point can handle this and still provide good user experience. See how you can limit the number of extensions displayed by plugins. -
Share contextual information
Think about what contextual information could be useful for other plugins and add this to thecontext
object. For example, the panel menu extension point shares thepanelId
and thetimeRange
. Note that thecontext{}
object always gets frozen before being passed to the links, so it can't be mutated. -
Avoid unnecessary re-renders
-
Static context
// Define the `context` object outside of the component if it only has static values
const context { foo: 'bar' };
export const InstanceToolbar = () => {
const { links, isLoading } = usePluginLinks({ extensionPointId, context }); -
Dynamic context
export const InstanceToolbar = ({ instanceId }) => {
// Always use `useMemo()` when the `context` object has "dynamic" values
const context = useMemo(() => ({ instanceId }), [instanceId]);
const { links, isLoading } = usePluginLinks({ extensionPointId, context });
-
Creating an extension point for links​
import { usePluginLinks } from '@grafana/runtime';
export const InstanceToolbar = () => {
// The `extensionPointId` must be prefixed.
// - Core Grafana -> prefix with "grafana/"
// - Plugin -> prefix with "{your-plugin-id}/"
//
// This is also what plugins use when they call `addLink()`
const extensionPointId = 'myorg-foo-app/toolbar/v1';
const { links, isLoading } = usePluginLinks({ extensionPointId });
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
{/* Loop through the links added by plugins */}
{links.map(({ id, title, path, onClick }) => (
<a href={path} title={title} key={id} onClick={onClick}>
{title}
</a>
))}
</div>
);
};
Passing data to links​
import { usePluginLinks } from '@grafana/runtime';
export const InstanceToolbar = ({ instanceId }) => {
const extensionPointId = 'myorg-foo-app/toolbar/v1';
// Heads up! Always use `useMemo()` in case the `context` object has any "dynamic" properties
// to prevent unnecessary re-renders (Otherwise a new object would be created on every render, that could
// result in a new links{} object, that could trigger a new re-render, and so on.)
const context = useMemo(() => ({ instanceId }), [instanceId]);
const { links, isLoading } = usePluginLinks({ extensionPointId, context });
// ...
};
Limit the number of extensions by plugins​
You might have limited space on the UI and you would like the limit the number of extensions plugins can register to your extension point. By default there is no limit.
import { usePluginLinks } from '@grafana/runtime';
export const InstanceToolbar = () => {
// Only one link per plugin is allowed.
// (If a plugin registers more than one links, then the rest will be ignored
// and won't be returned by the hook.)
const { links, isLoading } = usePluginLinks({ extensionPointId, limitPerPlugin: 1 });
// ...
};
Limit which plugins can register links​
import { usePluginLinks } from '@grafana/runtime';
export const InstanceToolbar = () => {
const { links, isLoading } = usePluginLinks({ extensionPointId, limitPerPlugin: 1 });
// You can rely on the `link.pluginId` prop to filter based on the plugin
// that has registered the extension.
const allowedLinks = useMemo(() => {
const allowedPluginIds = ['myorg-a-app', 'myorg-b-app'];
return links.filter(({ pluginId }) => allowedPluginIds.includes(pluginId));
}, [links]);
// ...
};
Components​
Best practices for rendering components​
- Make sure your UI controls the behavior
Component extensions can render different layouts and can respond to various kind of user interactions. Make sure that your UI defines clear boundaries for rendering components defined by other plugins. - Share contextual information
Think about what contextual information could be useful for other plugins and pass this asprops
to the components.
Creating an extension point for components​
import { usePluginComponents } from '@grafana/runtime';
export const InstanceToolbar = () => {
// The `extensionPointId` must be prefixed.
// - Core Grafana -> prefix with "grafana/"
// - Plugin -> prefix with "{your-plugin-id}/"
//
// This is also what plugins use when they call `addComponent()`
const extensionPointId = 'myorg-foo-app/toolbar/v1';
const { components, isLoading } = usePluginComponents({ extensionPointId });
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
{/* Loop through the components added by plugins */}
{components.map(({ id, component: Component }) => (
<Component key={id} />
))}
</div>
);
};
Passing data to the components​
import { usePluginComponents } from '@grafana/runtime';
// Types for the props (passed as a generic to the hook in the following code block)
type ComponentProps = {
instanceId: string;
};
export const InstanceToolbar = ({ instanceId }) => {
const extensionPointId = 'myorg-foo-app/toolbar/v1';
const { components, isLoading } = usePluginComponents<ComponentProps>({ extensionPointId });
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
{/* Sharing contextual information using component props */}
{components.map(({ id, component: Component }) => (
<Component key={id} instanceId={instanceId} />
))}
</div>
);
};
Limit which plugins can register components​
import { usePluginComponents } from '@grafana/runtime';
export const InstanceToolbar = () => {
const extensionPointId = 'myorg-foo-app/toolbar/v1';
const { components, isLoading } = usePluginComponents<ComponentProps>({ extensionPointId });
// You can rely on the `component.pluginId` prop to filter based on the plugin
// that has registered the extension.
const allowedComponents = useMemo(() => {
const allowedPluginIds = ['myorg-a-app', 'myorg-b-app'];
return components.filter(({ pluginId }) => allowedPluginIds.includes(pluginId));
}, [components]);
// ...
};