Skip to main content

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.

TypeDescription
LinkLinks 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
ComponentComponents 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
  • 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 the context object. For example, the panel menu extension point shares the panelId and the timeRange. Note that the context{} 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 });
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>
);
};
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 });

// ...
};
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 as props 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]);

// ...
};