Docs: Plugins doc reorganization, part 1 (#69864)

* Initial commit

* Prettier fixes

* Doc-validator fixes part 1

* Doc-validator fixes part 2

* More doc-validator fixes

* More doc-validator fixes

* Test

* link test

* Linnk test

* Link test

* More fixes

* More fixes

* Doc-validator fixes

* Doc-validator fixes

* fix broken link

* Fix

* Testing

* Doc fixes

* Link fixes

* Fix links

* Update docs/sources/developers/plugins/create-a-grafana-plugin/_index.md

Co-authored-by: David Harris <david.harris@grafana.com>

* Testing

* Testing

* Testing

* Testing

* Doc-validator fixes

* Doc-validator fixes

* Doc-validator fixes

* Fix broken links for plugins reorganization project

* Prettier fixes

* Prettier fixes

* Incorporate reviewer feedback

* Link fixes

* Link fixes

* Link fixes

* Link fix

* Deleted space

* Codeowners fix

* Change grafana.com links to absolute URLs for Hugo

---------

Co-authored-by: David Harris <david.harris@grafana.com>
This commit is contained in:
Joseph Perez
2023-07-05 11:25:11 -07:00
committed by GitHub
parent bdf60d69de
commit f9df1f3051
70 changed files with 563 additions and 412 deletions

View File

@ -0,0 +1,34 @@
---
title: Extend a Grafana plugin
menuTitle: Extend a plugin
keywords:
- grafana
- plugins
- plugin
- development
- extension
- documentation
description: An index of how-to topics for extending or enhancing Grafana plugins.
weight: 200
---
# Extend a Grafana plugin
This section contains how-to topics for extending or enhancing Grafana plugins:
- [Enable annotations]({{< relref "./add-support-for-annotations.md" >}})
- [Add anonymous usage reporting]({{< relref "./add-anonymous-usage-reporting.md" >}})
- [Add authentication for a data source plugin]({{< relref "./add-authentication-for-data-source-plugins.md" >}})
- [Add distributed tracing for backend plugins]({{< relref "./add-distributed-tracing-for-backend-plugins.md" >}})
- [Add features to Explore queries]({{< relref "./add-support-for-explore-queries.md" >}})
- [Add query editor help]({{< relref "./add-query-editor-help.md" >}})
- [Add support for variables]({{< relref "./add-support-for-variables.md" >}})
- [Build a custom panel option editor]({{< relref "./custom-panel-option-editors.md" >}})
- [Use extensions to add links to app plugins]({{< relref "./extend-the-grafana-ui-with-links.md" >}})
- [Work with cross-plugin links]({{< relref "./cross-plugin-linking.md" >}})
Additional resources:
- [Automate development with CI](https://grafana.github.io/plugin-tools/docs/ci)
- [Create nested plugins](https://grafana.github.io/plugin-tools/docs/nested-plugins)
- [Extend configurations](https://grafana.github.io/plugin-tools/docs/advanced-configuration)

View File

@ -0,0 +1,176 @@
---
title: Add anonymous usage reporting
aliases:
- ../../../plugins/add-anonymous-usage-reporting/
keywords:
- grafana
- plugins
- plugin
- anonymous usage
- reporting
description: How to add anonymous usage tracking to your Grafana plugin.
weight: 200
---
# Add anonymous usage reporting
Add anonymous usage tracking to your plugin to send [reporting events]({{< relref "../../../../setup-grafana/configure-grafana#reporting_enabled" >}}) that describe how your plugin is being used to a tracking system configured by your Grafana server administrator.
## Event reporting
In this section, we show an example of tracking usage data from a query editor and receiving a report back from the analytics service.
### Sample query editor
Let's say you have a `QueryEditor` that looks similar to the example below. It has a `CodeEditor` field where you can write your query and a query type selector so you can select the kind of query result that you expect to return:
```ts
import React, { ReactElement } from 'react';
import { InlineFieldRow, InlineField, Select, CodeEditor } from '@grafana/ui';
import type { EditorProps } from './types';
export function QueryEditor(props: EditorProps): ReactElement {
const { datasource, query, onChange, onRunQuery } = props;
const queryType = { value: query.value ?? 'timeseries' };
const queryTypes = [
{
label: 'Timeseries',
value: 'timeseries',
},
{
label: 'Table',
value: 'table',
},
];
const onChangeQueryType = (type: string) => {
onChange({
...query,
queryType: type,
});
runQuery();
};
const onChangeRawQuery = (rawQuery: string) => {
onChange({
...query,
rawQuery: type,
});
runQuery();
};
return (
<>
<div>
<CodeEditor
height="200px"
showLineNumbers={true}
language="sql"
onBlur={onChangeRawQuery}
value={query.rawQuery}
/>
</div>
<InlineFieldRow>
<InlineField label="Query type" grow>
<Select options={queryTypes} onChange={onChangeQueryType} value={queryType} />
</InlineField>
</InlineFieldRow>
</>
);
}
```
### Track usage with `usePluginInteractionReporter`
Let's say that you want to track how the usage looks between time series and table queries.
What you want to do is to add the `usePluginInteractionReporter` to fetch a report function that takes two arguments:
- Required: An event name that begins with `grafana_plugin_`. It is used to identify the interaction being made.
- Optional: Attached contextual data. In our example, that is the query type.
```ts
import React, { ReactElement } from 'react';
import { InlineFieldRow, InlineField, Select, CodeEditor } from '@grafana/ui';
import { usePluginInteractionReporter } from '@grafana/runtime';
import type { EditorProps } from './types';
export function QueryEditor(props: EditorProps): ReactElement {
const { datasource, query, onChange, onRunQuery } = props;
const report = usePluginInteractionReporter();
const queryType = { value: query.value ?? 'timeseries' };
const queryTypes = [
{
label: 'Timeseries',
value: 'timeseries',
},
{
label: 'Table',
value: 'table',
},
];
const onChangeQueryType = (type: string) => {
onChange({
...query,
queryType: type,
});
runQuery();
};
const onChangeRawQuery = (rawQuery: string) => {
onChange({
...query,
rawQuery: type,
});
report('grafana_plugin_executed_query', {
query_type: queryType.value,
});
runQuery();
};
return (
<>
<div>
<CodeEditor
height="200px"
showLineNumbers={true}
language="sql"
onBlur={onChangeRawQuery}
value={query.rawQuery}
/>
</div>
<InlineFieldRow>
<InlineField label="Query type" grow>
<Select options={queryTypes} onChange={onChangeQueryType} value={queryType} />
</InlineField>
</InlineFieldRow>
</>
);
}
```
### Data returned from the analytics service
When you use `usePluginInteractionReporter`, the report function that is handed back to you automatically attaches contextual data about the plugin you are tracking to the events.
In our example, the following information is sent to the analytics service configured by the Grafana server administrator:
```ts
{
type: 'interaction',
payload: {
interactionName: 'grafana_plugin_executed_query',
grafana_version: '9.2.1',
plugin_type: 'datasource',
plugin_version: '1.0.0',
plugin_id: 'grafana-example-datasource',
plugin_name: 'Example',
datasource_uid: 'qeSI8VV7z', // will only be added for datasources
query_type: 'timeseries'
}
}
```

View File

@ -0,0 +1,470 @@
---
title: Add authentication for data source plugins
aliases:
- ../../../plugins/add-authentication-for-data-source-plugins/
description: How to add authentication for data source plugins.
keywords:
- grafana
- plugins
- plugin
- authentication
- data source
- datasource
aliases:
- ../../plugins/developing/auth-for-datasources/
- /docs/grafana/next/developers/plugins/authentication/
weight: 300
---
# Add authentication for data source plugins
Grafana plugins can perform authenticated requests against a third-party API by using the _data source proxy_ or through a custom a _backend plugin_.
## Choose an authentication method
Configure your data source plugin to authenticate against a third-party API in one of either of two ways:
- Use the [_data source proxy_](#authenticate-using-the-data-source-proxy) method, or
- Build a [_backend plugin_](#authenticate-using-a-backend-plugin).
| Case | Use |
| ----------------------------------------------------------------------------------------------- | ------------------------------- |
| Do you need to authenticate your plugin using Basic Auth or API keys? | Use the data source proxy. |
| Does your API support OAuth 2.0 using client credentials? | Use the data source proxy. |
| Does your API use a custom authentication method that isn't supported by the data source proxy? | Use a backend plugin. |
| Does your API communicate over a protocol other than HTTP? | Build and use a backend plugin. |
| Does your plugin require alerting support? | Build and use a backend plugin. |
## Encrypt data source configuration
Data source plugins have two ways of storing custom configuration: `jsonData` and `secureJsonData`.
Users with the Viewer role can access data source configuration such as the contents of `jsonData` in cleartext. If you've enabled anonymous access, anyone who can access Grafana in their browser can see the contents of `jsonData`.
Users of [Grafana Enterprise](/products/enterprise/grafana/) can restrict access to data sources to specific users and teams. For more information, refer to [Data source permissions](/docs/grafana/latest/enterprise/datasource_permissions).
> **Important:** Do not use `jsonData` with sensitive data such as password, tokens, and API keys. If you need to store sensitive information, use `secureJsonData` instead.
> **Note:** You can see the settings that the current user has access to by entering `window.grafanaBootData` in the developer console of your browser.
### Store configuration in `secureJsonData`
If you need to store sensitive information, use `secureJsonData` instead of `jsonData`. Whenever the user saves the data source configuration, the secrets in `secureJsonData` are sent to the Grafana server and encrypted before they're stored.
Once you have encrypted the secure configuration, it can no longer be accessed from the browser. The only way to access secrets after they've been saved is by using the [_data source proxy_](#authenticate-using-the-data-source-proxy).
### Add secret configuration to your data source plugin
To demonstrate how you can add secrets to a data source plugin, let's add support for configuring an API key.
1. Create a new interface in `types.ts` to hold the API key:
```ts
export interface MySecureJsonData {
apiKey?: string;
}
```
1. Add type information to your `secureJsonData` object by updating the props for your `ConfigEditor` to accept the interface as a second type parameter. Access the value of the secret from the `options` prop inside your `ConfigEditor`:
```ts
interface Props extends DataSourcePluginOptionsEditorProps<MyDataSourceOptions, MySecureJsonData> {}
```
```ts
const { secureJsonData, secureJsonFields } = options;
const { apiKey } = secureJsonData;
```
> **Note:** You can do this until the user saves the configuration; when the user saves the configuration, Grafana clears the value. After that, you can use `secureJsonFields` to determine whether the property has been configured.
1. To securely update the secret in your plugin's configuration editor, update the `secureJsonData` object using the `onOptionsChange` prop:
```ts
const onAPIKeyChange = (event: ChangeEvent<HTMLInputElement>) => {
onOptionsChange({
...options,
secureJsonData: {
apiKey: event.target.value,
},
});
};
```
1. Define a component that can accept user input:
```ts
<Input
type="password"
placeholder={secureJsonFields?.apiKey ? 'configured' : ''}
value={secureJsonData.apiKey ?? ''}
onChange={onAPIKeyChange}
/>
```
1. Optional: If you want the user to be able to reset the API key, then you need to set the property to `false` in the `secureJsonFields` object:
```ts
const onResetAPIKey = () => {
onOptionsChange({
...options,
secureJsonFields: {
...options.secureJsonFields,
apiKey: false,
},
secureJsonData: {
...options.secureJsonData,
apiKey: '',
},
});
};
```
Now that users can configure secrets, the next step is to see how we can add them to our requests.
## Authenticate using the data source proxy
Once the user has saved the configuration for a data source, the secret data source configuration will no longer be available in the browser. Encrypted secrets can only be accessed on the server. So how do you add them to your request?
The Grafana server comes with a proxy that lets you define templates for your requests: _proxy routes_. Grafana sends the proxy route to the server, decrypts the secrets along with other configuration, and adds them to the request before sending it.
> **Note:** Be sure not to confuse the data source proxy with the [auth proxy]({{< relref "../../../../setup-grafana/configure-security/configure-authentication/auth-proxy/index.md" >}}). The data source proxy is used to authenticate a data source, while the auth proxy is used to log into Grafana itself.
### Add a proxy route to your plugin
To forward requests through the Grafana proxy, you need to configure one or more _proxy routes_. A proxy route is a template for any outgoing request that is handled by the proxy. You can configure proxy routes in the [plugin.json]({{< relref "../../metadata.md" >}}) file.
1. Add the route to `plugin.json`:
```json
"routes": [
{
"path": "example",
"url": "https://api.example.com"
}
]
```
> **Note:** You need to restart the Grafana server every time you make a change to your `plugin.json` file.
1. In the `DataSource`, extract the proxy URL from `instanceSettings` to a class property called `url`:
```ts
export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
url?: string;
constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
super(instanceSettings);
this.url = instanceSettings.url;
}
// ...
}
```
1. In the `query` method, make a request using `BackendSrv`. The first section of the URL path needs to match the `path` of your proxy route. The data source proxy replaces `this.url + routePath` with the `url` of the route. Based on our example, the URL for the request would be `https://api.example.com/v1/users`:
```ts
import { getBackendSrv } from '@grafana/runtime';
```
```ts
const routePath = '/example';
getBackendSrv().datasourceRequest({
url: this.url + routePath + '/v1/users',
method: 'GET',
});
```
### Add a dynamic proxy route to your plugin
Grafana sends the proxy route to the server, where the data source proxy decrypts any sensitive data and interpolates the template variables with the decrypted data before making the request.
To add user-defined configuration to your routes:
- Use `.JsonData` for configuration stored in `jsonData`. For example, where `projectId` is the name of a property in the `jsonData` object:
```json
"routes": [
{
"path": "example",
"url": "https://api.example.com/projects/{{ .JsonData.projectId }}"
}
]
```
- Use `.SecureJsonData` for sensitive data stored in `secureJsonData`. For example, where `password` is the name of a property in the `secureJsonData` object:
```json
"routes": [
{
"path": "example",
"url": "https://{{ .JsonData.username }}:{{ .SecureJsonData.password }}@api.example.com"
}
]
```
In addition to adding the URL to the proxy route, you can also add headers, URL parameters, and a request body.
#### Add HTTP headers to a proxy route
Here's an example of adding `name` and `content` as HTTP headers:
```json
"routes": [
{
"path": "example",
"url": "https://api.example.com",
"headers": [
{
"name": "Authorization",
"content": "Bearer {{ .SecureJsonData.apiToken }}"
}
]
}
]
```
#### Add URL parameters to a proxy route
Here's an example of adding `name` and `content` as URL parameters:
```json
"routes": [
{
"path": "example",
"url": "http://api.example.com",
"urlParams": [
{
"name": "apiKey",
"content": "{{ .SecureJsonData.apiKey }}"
}
]
}
]
```
#### Add a request body to a proxy route
Here's an example of adding `username` and `password` to the request body:
```json
"routes": [
{
"path": "example",
"url": "http://api.example.com",
"body": {
"username": "{{ .JsonData.username }}",
"password": "{{ .SecureJsonData.password }}"
}
}
]
```
### Add an OAuth 2.0 proxy route to your plugin
Since your request to each route is made server-side with OAuth 2.0 authentication, only machine-to-machine requests are supported. In order words, if you need to use a different grant than client credentials, you need to implement it yourself.
To authenticate using OAuth 2.0, add a `tokenAuth` object to the proxy route definition. If necessary, Grafana performs a request to the URL defined in `tokenAuth` to retrieve a token before making the request to the URL in your proxy route. Grafana automatically renews the token when it expires.
Any parameters defined in `tokenAuth.params` are encoded as `application/x-www-form-urlencoded` and sent to the token URL.
```json
{
"routes": [
{
"path": "api",
"url": "https://api.example.com/v1",
"tokenAuth": {
"url": "https://api.example.com/v1/oauth/token",
"params": {
"grant_type": "client_credentials",
"client_id": "{{ .SecureJsonData.clientId }}",
"client_secret": "{{ .SecureJsonData.clientSecret }}"
}
}
}
]
}
```
## Authenticate using a backend plugin
While the data source proxy supports the most common authentication methods for HTTP APIs, using proxy routes has a few limitations:
- Proxy routes only support HTTP or HTTPS.
- Proxy routes don't support custom token authentication.
If any of these limitations apply to your plugin, you need to add a [backend plugin]({{< relref "../../introduction-to-plugin-development/backend" >}}). Because backend plugins run on the server, they can access decrypted secrets, which makes it easier to implement custom authentication methods.
The decrypted secrets are available from the `DecryptedSecureJSONData` field in the instance settings.
```go
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
instanceSettings := req.PluginContext.DataSourceInstanceSettings
if apiKey, exists := settings.DecryptedSecureJSONData["apiKey"]; exists {
// Use the decrypted API key.
}
// ...
}
```
## Forward OAuth identity for the logged-in user
If your data source uses the same OAuth provider as Grafana itself, for example using [Generic OAuth Authentication]({{< relref "../../../../setup-grafana/configure-security/configure-authentication/generic-oauth" >}}), then your data source plugin can reuse the access token for the logged-in Grafana user.
To allow Grafana to pass the access token to the plugin, update the data source configuration and set the `jsonData.oauthPassThru` property to `true`. The [DataSourceHttpSettings](https://developers.grafana.com/ui/latest/index.html?path=/story/data-source-datasourcehttpsettings--basic) settings provide a toggle, the **Forward OAuth Identity** option, for this. You can also build an appropriate toggle to set `jsonData.oauthPassThru` in your data source configuration page UI.
When configured, Grafana can forward authorization HTTP headers such as `Authorization` or `X-ID-Token` to a backend data source. This information is available across the `QueryData`, `CallResource` and `CheckHealth` requests.
To get Grafana to forward the headers, create a HTTP client using the [Grafana plugin SDK for Go](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/httpclient) and set the `ForwardHTTPHeaders` option to `true` (by default, it's set to `false`). This package exposes request information which can be subsequently forwarded downstream and/or used directly within the plugin.
```go
func NewDatasource(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
opts, err := settings.HTTPClientOptions()
if err != nil {
return nil, fmt.Errorf("http client options: %w", err)
}
// Important: Reuse the same client for each query to avoid using all available connections on a host.
opts.ForwardHTTPHeaders = true
cl, err := httpclient.New(opts)
if err != nil {
return nil, fmt.Errorf("httpclient new: %w", err)
}
return &Datasource{
httpClient: cl,
}, nil
}
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
// Necessary to keep the Context, since the injected middleware is configured there
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://some-url", nil)
if err != nil {
return nil, fmt.Errorf("new request with context: %w", err)
}
// Authorization header will be automatically injected if oauthPassThru is configured
resp, err := ds.httpClient.Do(req)
// ...
}
```
You can see a full working plugin example here: [datasource-http-backend](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/datasource-http-backend).
### Extract a header from an HTTP request
If you need to access the HTTP header information directly, you can also extract that information from the request:
```go
func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
token := strings.Fields(req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName))
var (
tokenType = token[0]
accessToken = token[1]
)
idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName) // present if user's token includes an ID token
// ...
return &backend.CheckHealthResult{Status: backend.HealthStatusOk}, nil
}
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
token := strings.Fields(req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName))
var (
tokenType = token[0]
accessToken = token[1]
)
idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName)
for _, q := range req.Queries {
// ...
}
}
func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
token := req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName)
idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName)
// ...
}
```
## Work with cookies
### Forward cookies for the logged-in user
Your data source plugin can forward cookies for the logged-in Grafana user to the data source. Use the [DataSourceHttpSettings](https://developers.grafana.com/ui/latest/index.html?path=/story/data-source-datasourcehttpsettings--basic) component on the data source's configuration page. It provides the **Allowed cookies** option, where you can specify the cookie names.
When configured, as with [authorization headers](#forward-oauth-identity-for-the-logged-in-user), these cookies are automatically injected if you use the SDK HTTP client.
### Extract cookies for the logged-in user
You can also extract the cookies in the `QueryData`, `CallResource` and `CheckHealth` requests if required.
**`QueryData`**
```go
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
cookies:= req.GetHTTPHeader(backend.CookiesHeaderName)
// ...
}
```
**`CallResource`**
```go
func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
cookies:= req.GetHTTPHeader(backend.CookiesHeaderName)
// ...
}
```
**`CheckHealth`**
```go
func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
cookies:= req.GetHTTPHeader(backend.CookiesHeaderName)
// ...
}
```
## Forward user header for the logged-in user
When `send_user_header` is enabled, Grafana passes the user header to the plugin using the `X-Grafana-User` header. You can forward this header as well as [authorization headers](#forward-oauth-identity-for-the-logged-in-user) or [configured cookies](#forward-cookies-for-the-logged-in-user).
**`QueryData`**
```go
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
u := req.GetHTTPHeader("X-Grafana-User")
// ...
}
```
**`CallResource`**
```go
func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
u := req.GetHTTPHeader("X-Grafana-User")
// ...
}
```
**`CheckHealth`**
```go
func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
u := req.GetHTTPHeader("X-Grafana-User")
// ...
}
```

View File

@ -0,0 +1,122 @@
---
title: Add distributed tracing for backend plugins
aliases:
- ../../../plugins/add-distributed-tracing-for-backend-plugins/
keywords:
- grafana
- plugins
- plugin
- distributed tracing
- tracing
- backend
- back-end
description: How to add distributed tracing for backend plugins.
weight: 350
---
# Add distributed tracing for backend plugins
> **Note:** This feature requires at least Grafana 9.5.0, and your plugin needs to be built at least with grafana-plugins-sdk-go v0.157.0. If you run a plugin with tracing features on an older version of Grafana, tracing is disabled.
Distributed tracing allows backend plugin developers to create custom spans in their plugins, and send them to the same endpoint and with the same propagation format as the main Grafana instance. The tracing context is also propagated from the Grafana instance to the plugin, so the plugin's spans will be correlated to the correct trace.
## Plugin configuration
Plugin tracing must be enabled manually on a per-plugin basis, by specifying `tracing = true` in the plugin's config section:
```ini
[plugin.myorg-myplugin-datasource]
tracing = true
```
## OpenTelemetry configuration in Grafana
Grafana supports [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. If Grafana is configured to use a deprecated tracing system (Jaeger or OpenTracing), then tracing is disabled in the plugin provided by the SDK and configured when calling `datasource.Manage | app.Manage`.
OpenTelemetry must be enabled and configured for the Grafana instance. Please refer to the [Grafana configuration documentation](
{{< relref "../../../../setup-grafana/configure-grafana#tracingopentelemetry" >}}) for more information.
Refer to the [OpenTelemetry Go SDK](https://pkg.go.dev/go.opentelemetry.io/otel) for in-depth documentation about all the features provided by OpenTelemetry.
> **Note:** If tracing is disabled in Grafana, `backend.DefaultTracer()` returns a no-op tracer.
## Implement tracing in your plugin
> **Note:** Make sure you are using at least grafana-plugin-sdk-go v0.157.0. You can update with `go get -u github.com/grafana/grafana-plugin-sdk-go`.
### Configure a global tracer
When OpenTelemetry tracing is enabled on the main Grafana instance and tracing is enabled for a plugin, the OpenTelemetry endpoint address and propagation format is passed to the plugin during startup. These parameters are used to configure a global tracer.
1. Use `datasource.Manage` or `app.Manage` to run your plugin to automatically configure the global tracer. Specify any custom attributes for the default tracer using `CustomAttributes`:
```go
func main() {
if err := datasource.Manage("MY_PLUGIN_ID", plugin.NewDatasource, datasource.ManageOpts{
TracingOpts: tracing.Opts{
// Optional custom attributes attached to the tracer's resource.
// The tracer will already have some SDK and runtime ones pre-populated.
CustomAttributes: []attribute.KeyValue{
attribute.String("my_plugin.my_attribute", "custom value"),
},
},
}); err != nil {
log.DefaultLogger.Error(err.Error())
os.Exit(1)
}
}
```
1. Once you have configured tracing, use the global tracer like this:
```go
tracing.DefaultTracer()
```
This returns an [OpenTelemetry `trace.Tracer`](https://pkg.go.dev/go.opentelemetry.io/otel/trace#Tracer) for creating spans.
**Example:**
```go
func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, query backend.DataQuery) (backend.DataResponse, error) {
ctx, span := tracing.DefaultTracer().Start(
ctx,
"query processing",
trace.WithAttributes(
attribute.String("query.ref_id", query.RefID),
attribute.String("query.type", query.QueryType),
attribute.Int64("query.max_data_points", query.MaxDataPoints),
attribute.Int64("query.interval_ms", query.Interval.Milliseconds()),
attribute.Int64("query.time_range.from", query.TimeRange.From.Unix()),
attribute.Int64("query.time_range.to", query.TimeRange.To.Unix()),
),
)
defer span.End()
log.DefaultLogger.Debug("query", "traceID", trace.SpanContextFromContext(ctx).TraceID())
// ...
}
```
### Tracing gRPC calls
When tracing is enabled, a new span is created automatically for each gRPC call (`QueryData`, `CheckHealth`, etc.), both on Grafana's side and on the plugin's side. The plugin SDK also injects the trace context into the `context.Context` that is passed to those methods.
You can retrieve the [trace.SpanContext](https://pkg.go.dev/go.opentelemetry.io/otel/trace#SpanContext) with `tracing.SpanContextFromContext` by passing the original `context.Context` to it:
```go
func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, query backend.DataQuery) (backend.DataResponse, error) {
spanCtx := trace.SpanContextFromContext(ctx)
traceID := spanCtx.TraceID()
// ...
}
```
### Tracing HTTP requests
When tracing is enabled, a `TracingMiddleware` is also added to the default middleware stack to all HTTP clients created using the `httpclient.New` or `httpclient.NewProvider`, unless you specify custom middleware. This middleware creates spans for each outgoing HTTP request and provides some useful attributes and events related to the request's lifecycle.
## Plugin example
Refer to the [datasource-http-backend plugin example](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/datasource-http-backend) for a complete example of a plugin with full distributed tracing support.

View File

@ -0,0 +1,84 @@
---
title: Add query editor help
aliases:
- ../../../plugins/add-query-editor-help/
keywords:
- grafana
- plugins
- plugin
- queries
- query editor
- query editor help
description: How to add a help component to query editors in Grafana.
weight: 500
---
# Add query editor help
Query editors support the addition of a help component to display examples of potential queries. When the user clicks on one of the examples, the query editor is automatically updated. This helps the user to make faster queries.
1. In the `src` directory of your plugin, create a file `QueryEditorHelp.tsx` with the following content:
```ts
import React from 'react';
import { QueryEditorHelpProps } from '@grafana/data';
export default (props: QueryEditorHelpProps) => {
return <h2>My cheat sheet</h2>;
};
```
1. Configure the plugin to use `QueryEditorHelp`:
```ts
import QueryEditorHelp from './QueryEditorHelp';
```
```ts
export const plugin = new DataSourcePlugin<DataSource, MyQuery, MyDataSourceOptions>(DataSource)
.setConfigEditor(ConfigEditor)
.setQueryEditor(QueryEditor)
.setQueryEditorHelp(QueryEditorHelp);
```
1. Create a few examples of potential queries:
```ts
import React from 'react';
import { QueryEditorHelpProps, DataQuery } from '@grafana/data';
const examples = [
{
title: 'Addition',
expression: '1 + 2',
label: 'Add two integers',
},
{
title: 'Subtraction',
expression: '2 - 1',
label: 'Subtract an integer from another',
},
];
export default (props: QueryEditorHelpProps) => {
return (
<div>
<h2>Cheat Sheet</h2>
{examples.map((item, index) => (
<div className="cheat-sheet-item" key={index}>
<div className="cheat-sheet-item__title">{item.title}</div>
{item.expression ? (
<div
className="cheat-sheet-item__example"
onClick={(e) => props.onClickExample({ refId: 'A', queryText: item.expression } as DataQuery)}
>
<code>{item.expression}</code>
</div>
) : null}
<div className="cheat-sheet-item__label">{item.label}</div>
</div>
))}
</div>
);
};
```

View File

@ -0,0 +1,40 @@
---
title: Enable annotations
menuTitle: Enable annotations
aliases:
- ../../../plugins/add-support-for-annotations/
description: Add support for annotations in your plugin.
keywords:
- grafana
- plugins
- plugin
- annotations
weight: 100
---
# Enable annotations
You can add support to your plugin for annotations that will insert information into Grafana alerts. This guide explains how to add support for [annotations]({{< relref "../../../../dashboards/build-dashboards/annotate-visualizations#querying-other-data-sources" >}}) to a data source plugin.
## Support annotations in your data source plugin
To enable annotations, simply add two lines of code to your plugin. Grafana uses your default query editor for editing annotation queries.
1. Add `"annotations": true` to the [plugin.json]({{< relref "../../metadata.md" >}}) file to let Grafana know that your plugin supports annotations.
**In `plugin.json`:**
```json
{
"annotations": true
}
```
2. In `datasource.ts`, override the `annotations` property from `DataSourceApi` (or `DataSourceWithBackend` for backend data sources). For the default behavior, set `annotations` to an empty object.
**In `datasource.ts`:**
```ts
annotations: {
}
```

View File

@ -0,0 +1,81 @@
---
title: Add features to Explore queries
aliases:
- ../../../plugins/add-support-for-explore-queries/
description: Add features to Explore queries.
keywords:
- grafana
- plugins
- plugin
- queries
- explore queries
- explore
weight: 400
---
# Add features to Explore queries
[Explore]({{< relref "../../../../explore" >}}) allows users can make ad-hoc queries without the use of a dashboard. This is useful when they want to troubleshoot or learn more about the data.
Your data source supports Explore by default and uses the existing query editor for the data source. This guide explains how to extend functionality for Explore queries in a data source plugin.
## Add an Explore-specific query editor
To extend Explore functionality for your data source, define an Explore-specific query editor.
1. Create a file `ExploreQueryEditor.tsx` in the `src` directory of your plugin, with content similar to this:
```ts
import React from 'react';
import { QueryEditorProps } from '@grafana/data';
import { QueryField } from '@grafana/ui';
import { DataSource } from './DataSource';
import { MyQuery, MyDataSourceOptions } from './types';
type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>;
export default (props: Props) => {
return <h2>My Explore-specific query editor</h2>;
};
```
1. Modify your base query editor in `QueryEditor.tsx` to render the Explore-specific query editor. For example:
```ts
// [...]
import { CoreApp } from '@grafana/data';
import ExploreQueryEditor from './ExploreQueryEditor';
type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>;
export default (props: Props) => {
const { app } = props;
switch (app) {
case CoreApp.Explore:
return <ExploreQueryEditor {...props} />;
default:
return <div>My base query editor</div>;
}
};
```
## Select a preferred visualization type
By default, Explore should select an appropriate and useful visualization for your data. It can figure out whether the returned data is time series data or logs or something else, and creates the right type of visualization.
However, if you want a custom visualization, you can add a hint to your returned data frame by setting the `meta' attribute to `preferredVisualisationType`.
Construct a data frame with specific metadata like this:
```
const firstResult = new MutableDataFrame({
fields: [...],
meta: {
preferredVisualisationType: 'logs',
},
});
```
For possible options, refer to [PreferredVisualisationType](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/data.ts#L25).

View File

@ -0,0 +1,208 @@
---
title: Add support for variables
aliases:
- ../../../plugins/add-support-for-variables/
description: Add support for variables.
keywords:
- grafana
- plugins
- plugin
- queries
- variables
weight: 600
---
# Add support for variables
Variables are placeholders for values, and you can use them to create templated queries, and dashboard or panel links. For more information on variables, refer to [Templates and variables]({{< relref "../../../../dashboards/variables" >}}).
In this guide, you'll see how you can turn a query string like this:
```sql
SELECT * FROM services WHERE id = "$service"
```
into
```sql
SELECT * FROM services WHERE id = "auth-api"
```
Grafana provides a couple of helper functions to interpolate variables in a string template. Let's see how you can use them in your plugin.
## Interpolate variables in panel plugins
For panels, the `replaceVariables` function is available in the `PanelProps`.
Add `replaceVariables` to the argument list, and pass a user-defined template string to it:
```ts
export function SimplePanel({ options, data, width, height, replaceVariables }: Props) {
const query = replaceVariables('Now displaying $service');
return <div>{query}</div>;
}
```
## Interpolate variables in data source plugins
For data sources, you need to use the `getTemplateSrv`, which returns an instance of `TemplateSrv`.
1. Import `getTemplateSrv` from the `runtime` package:
```ts
import { getTemplateSrv } from '@grafana/runtime';
```
1. In your `query` method, call the `replace` method with a user-defined template string:
```ts
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
const query = getTemplateSrv().replace('SELECT * FROM services WHERE id = "$service"', options.scopedVars);
const data = makeDbQuery(query);
return { data };
}
```
## Format multi-value variables
When a user selects multiple values for a variable, the value of the interpolated variable depends on the [variable format]({{< relref "../../../../dashboards/variables/variable-syntax#advanced-variable-format-options" >}}).
A data source plugin can define the default format option when no format is specified by adding a third argument to the interpolation function.
Let's change the SQL query to use CSV format by default:
```ts
getTemplateSrv().replace('SELECT * FROM services WHERE id IN ($service)', options.scopedVars, 'csv');
```
Now, when users write `$service`, the query looks like this:
```sql
SELECT * FROM services WHERE id IN (admin,auth,billing)
```
For more information on the available variable formats, refer to [Advanced variable format options]({{< relref "../../../../dashboards/variables/variable-syntax/index.md#advanced-variable-format-options" >}}).
## Set a variable from your plugin
Not only can you read the value of a variable, you can also update the variable from your plugin. Use `locationService.partial(query, replace)`.
The following example shows how to update a variable called `service`.
- `query` contains the query parameters you want to update. The query parameters that control variables are prefixed with `var-`.
- `replace: true` tells Grafana to update the current URL state rather than creating a new history entry.
```ts
import { locationService } from '@grafana/runtime';
```
```ts
locationService.partial({ 'var-service': 'billing' }, true);
```
> **Note:** Grafana queries your data source whenever you update a variable. Excessive updates to variables can slow down Grafana and lead to a poor user experience.
## Add support for query variables to your data source
A [query variable]({{< relref "../../../../dashboards/variables/add-template-variables#add-a-query-variable" >}}) is a type of variable that allows you to query a data source for the values. By adding support for query variables to your data source plugin, users can create dynamic dashboards based on data from your data source.
Let's start by defining a query model for the variable query:
```ts
export interface MyVariableQuery {
namespace: string;
rawQuery: string;
}
```
For a data source to support query variables, override the `metricFindQuery` in your `DataSourceApi` class. The `metricFindQuery` function returns an array of `MetricFindValue` which has a single property, `text`:
```ts
async metricFindQuery(query: MyVariableQuery, options?: any) {
// Retrieve DataQueryResponse based on query.
const response = await this.fetchMetricNames(query.namespace, query.rawQuery);
// Convert query results to a MetricFindValue[]
const values = response.data.map(frame => ({ text: frame.name }));
return values;
}
```
> **Note:** By default, Grafana provides a basic query model and editor for simple text queries. If that's all you need, then leave the query type as `string`:
```ts
async metricFindQuery(query: string, options?: any)
```
Let's create a custom query editor to allow the user to edit the query model.
1. Create a `VariableQueryEditor` component:
```ts
import React, { useState } from 'react';
import { MyVariableQuery } from './types';
interface VariableQueryProps {
query: MyVariableQuery;
onChange: (query: MyVariableQuery, definition: string) => void;
}
export const VariableQueryEditor = ({ onChange, query }: VariableQueryProps) => {
const [state, setState] = useState(query);
const saveQuery = () => {
onChange(state, `${state.query} (${state.namespace})`);
};
const handleChange = (event: React.FormEvent<HTMLInputElement>) =>
setState({
...state,
[event.currentTarget.name]: event.currentTarget.value,
});
return (
<>
<div className="gf-form">
<span className="gf-form-label width-10">Namespace</span>
<input
name="namespace"
className="gf-form-input"
onBlur={saveQuery}
onChange={handleChange}
value={state.namespace}
/>
</div>
<div className="gf-form">
<span className="gf-form-label width-10">Query</span>
<input
name="rawQuery"
className="gf-form-input"
onBlur={saveQuery}
onChange={handleChange}
value={state.rawQuery}
/>
</div>
</>
);
};
```
Grafana saves the query model whenever one of the text fields loses focus (`onBlur`) and then previews the values returned by `metricFindQuery`.
The second argument to `onChange` allows you to set a text representation of the query that will appear next to the name of the variable in the variables list.
1. Configure your plugin to use the query editor:
```ts
import { VariableQueryEditor } from './VariableQueryEditor';
export const plugin = new DataSourcePlugin<DataSource, MyQuery, MyDataSourceOptions>(DataSource)
.setQueryEditor(QueryEditor)
.setVariableQueryEditor(VariableQueryEditor);
```
That's it! You can now try out the plugin by adding a [query variable]({{< relref "../../../../dashboards/variables/add-template-variables#add-a-query-variable" >}}) to your dashboard.

View File

@ -0,0 +1,85 @@
---
title: Work with cross-plugin links
aliases:
- ../../../plugins/cross-plugin-linking/
keywords:
- grafana
- plugins
- plugin
- links
- cross-plugin links
- extensions
- extensions api
description: Learn how to add plugin links to a Grafana app plugin.
weight: 800
---
# Work with cross-plugin links
With the Plugins extension API, app plugins can register extension points of their own to display other plugins links. This is called _cross-plugin linking_, and you can use it to create more immersive user experiences with installed plugins.
## Available extension points within plugins
An extension point is a location in another plugin's UI where your plugin can insert links. All extension point IDs within plugins should follow the naming convention `plugins/<plugin-id>/<extension-point-id>`.
## How to create an extension point within a plugin
Use the `getPluginExtensions` method in `@grafana/runtime` to create an extension point within your plugin. An extension point is a way to specify where in the plugin UI other plugins links are rendered.
{{% admonition type="note" %}}
Creating an extension point in a plugin creates 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.
{{% /admonition %}}
The `getPluginExtensions` method takes an object consisting of the `extensionPointId`, which must begin `plugin/<pluginId>`, and any contextual information that you want to provide. The `getPluginExtensions` method returns a list of `extensionLinks` that your program can loop over:
```typescript
import { getPluginExtensions } from '@grafana/runtime';
import { isPluginExtensionLink } from '@grafana/data';
import { LinkButton } from '@grafana/ui';
function AppExtensionPointExample() {
const { extensions } = getPluginExtensions({
extensionPointId: 'plugin/another-app-plugin/menu',
context: {
pluginId: 'another-app-plugin',
},
});
if (extensions.length === 0) {
return null;
}
return (
<div>
{extensions.map((extension) => {
if (isPluginExtensionLink(extension)) {
return (
<LinkButton href={extension.path} title={extension.description} key={extension.key}>
{extension.title}
</LinkButton>
);
}
return null;
})}
</div>
);
}
```
The preceding example shows a component that renders `<LinkButton />` components for all link extensions that other plugins registered for the `plugin/another-app-plugin/menu` extension point ID. The context is passed as the second parameter to `getPluginExtensions`, which uses `Object.freeze` to make the context immutable before passing it to other plugins.
## Insert links into another plugin
Create links for other plugins in the same way you [extend the Grafana application UI]({{< relref "./extend-the-grafana-ui-with-links" >}}) with a link. Don't specify a `grafana/...` extension point. Instead, specify the plugin extension point `plugin/<pluginId>/<extensionPointId>`.
Given the preceding example, use a plugin link such as the following:
```typescript
new AppPlugin().configureExtensionLink({
title: 'Go to basic app',
description: 'Will navigate the user to the basic app',
extensionPointId: 'plugin/another-app-plugin/menu',
path: '/a/myorg-basic-app/one',
});
```

View File

@ -0,0 +1,132 @@
---
title: Build a custom panel option editor
aliases:
- ../../../plugins/custom-panel-option-editors/
description: How to build a custom panel option editor.
keywords:
- grafana
- plugins
- plugin
- custom panel option editor
- customizing panel options
- panel options
weight: 700
---
# Build a custom panel option editor
The Grafana plugin platform comes with a range of editors that allow your users to customize a panel. The standard editors cover the most common types of options, such as text input and boolean switches. If you don't find the editor you're looking for, you can build your own.
## Panel option editor basics
The simplest editor is a React component that accepts two props:
- **`value`**: the current value of the option
- **`onChange`**: updates the option's value
The editor in the example below lets the user toggle a boolean value by clicking a button:
**SimpleEditor.tsx**
```ts
import React from 'react';
import { Button } from '@grafana/ui';
import { StandardEditorProps } from '@grafana/data';
export const SimpleEditor = ({ value, onChange }: StandardEditorProps<boolean>) => {
return <Button onClick={() => onChange(!value)}>{value ? 'Disable' : 'Enable'}</Button>;
};
```
To use a custom panel option editor, use the `addCustomEditor` on the `OptionsUIBuilder` object in your `module.ts` file and set the `editor` property to the name of your custom editor component.
**module.ts**
```ts
export const plugin = new PanelPlugin<SimpleOptions>(SimplePanel).setPanelOptions((builder) => {
return builder.addCustomEditor({
id: 'label',
path: 'label',
name: 'Label',
editor: SimpleEditor,
});
});
```
## Add settings to your panel option editor
You can use your custom editor to customize multiple possible settings. To add settings to your editor, set the second template variable of `StandardEditorProps` to an interface that contains the settings you want to configure. Access the editor settings through the `item` prop.
Here's an example of an editor that populates a drop-down with a range of numbers. The `Settings` interface defines the range of the `from` and `to` properties.
**SimpleEditor.tsx**
```ts
interface Settings {
from: number;
to: number;
}
type Props = StandardEditorProps<number, Settings>;
export const SimpleEditor = ({ item, value, onChange }: Props) => {
const options: Array<SelectableValue<number>> = [];
// Default values
const from = item.settings?.from ?? 1;
const to = item.settings?.to ?? 10;
for (let i = from; i <= to; i++) {
options.push({
label: i.toString(),
value: i,
});
}
return <Select options={options} value={value} onChange={(selectableValue) => onChange(selectableValue.value)} />;
};
```
You can now configure the editor for each option by configuring the `settings` property to call `addCustomEditor`:
```ts
export const plugin = new PanelPlugin<SimpleOptions>(SimplePanel).setPanelOptions((builder) => {
return builder.addCustomEditor({
id: 'index',
path: 'index',
name: 'Index',
editor: SimpleEditor,
settings: {
from: 1,
to: 10,
},
});
});
```
## Use query results in your panel option editor
Option editors can access the results from the last query. This lets you update your editor dynamically based on the data returned by the data source.
The editor context is available through the `context` prop. The data frames returned by the data source are available under `context.data`.
**SimpleEditor.tsx**
```ts
export const SimpleEditor = ({ item, value, onChange, context }: StandardEditorProps<string>) => {
const options: SelectableValue<string>[] = [];
if (context.data) {
const frames = context.data;
for (let i = 0; i < frames.length; i++) {
options.push({
label: frames[i].name,
value: frames[i].name,
});
}
}
return <Select options={options} value={value} onChange={(selectableValue) => onChange(selectableValue.value)} />;
};
```

View File

@ -0,0 +1,142 @@
---
title: Use extensions to add links to app plugins
aliases:
- ../../../plugins/extend-the-grafana-ui-with-links/
keywords:
- grafana
- plugins
- plugin
- links
- extensions
- app plugins
description: Learn how to add links to the Grafana user interface from an app plugin
weight: 760
---
# Use extensions to add links to app plugins
You can use the Plugin extensions API with your Grafana app plugins to add links to the Grafana UI. This feature lets you send users to your plugin's pages from other spots in the Grafana application.
## Before you begin
Be sure your plugin meets the following requirements before proceeding:
- It must be an app plugin.
- It must be preloaded (by setting the [preload property]({{< relref "../../metadata.md" >}}) to `true` in the `plugin.json`
- It must be installed and enabled.
## Available extension points within Grafana
An _extension point_ is a location within the Grafana UI where a plugin can insert links. The IDs of all extension points within Grafana start with `grafana/`. For example, you can use the following extension point ID:
- `grafana/dashboard/panel/menu`: extension point for all panel dropdown menus in dashboards
## Add a link extension within a Grafana dashboard panel menu
To add a link extension within a Grafana dashboard panel menu, complete the following steps:
1. Define the link extension in your plugin's `module.ts` file.
1. Define a new instance of the `AppPlugin` class by using the `configureExtensionLink` method. This method requires:
- an object that describes your link extension, including a `title` property for the link text
- an `extensionPointId` method that tells Grafana where the link should appear
- a `path` for the user to go to your plugin
```typescript
new AppPlugin().configureExtensionLink({
title: 'Go to basic app',
description: 'Will send the user to the basic app',
extensionPointId: 'grafana/dashboard/panel/menu',
path: '/a/myorg-basic-app/one', // Must start with "/a/<PLUGIN_ID>/"
});
```
Your link will now appear in dashboard panel menus. When the user clicks the link, they will be sent to the path you defined earlier.
{{% admonition type="note" %}} Each plugin is limited to a maximum of two links per extension point.{{%
/admonition %}}
## Add a link extension using context within Grafana
The above example works for simple cases. However, you may want to act on information from the app's panel from which the user is navigating.
To do this, use the `configure` property on the object that is passed to `configureExtensionLink()`. This property takes a function and returns an object that consists of a `title` property for the link text and a `path` to send the user to your plugin.
Alternatively, if you need to hide the link for certain scenarios, define the function to return _undefined_:
```typescript
new AppPlugin().configureExtensionLink({
title: 'Go to basic app',
description: 'Will send the user to the basic app',
extensionPointId: 'grafana/dashboard/panel/menu',
path: '/a/myorg-basic-app/one',
configure: (context: PanelContext) => {
switch (context?.pluginId) {
case 'timeseries':
return {
title: 'Go to page one',
description: 'hello',
path: '/a/myorg-basic-app/one',
};
case 'piechart':
return {
title: 'Go to page two',
path: '/a/myorg-basic-app/two',
};
// Returning undefined tells Grafana to hide the link
default:
return undefined;
}
},
});
```
The above example demonstrates how to return a different `path` based on which plugin the dashboard panel is using. If the clicked-upon panel is neither a time series nor a pie chart panel, then the `configure()` function returns _undefined_. When this happens, Grafana doesn't render the link.
{{% admonition type="note" %}} The context passed to the `configure()` function is bound by the `extensionPointId` into which you insert the link. Different extension points contain different contexts.{{%
/admonition %}}
## Add an event handler to a link
Link extensions give you the means to direct users to a plugin page via href links within the Grafana UI. You can also use them to trigger `onClick` events to perform dynamic actions when clicked.
To add an event handler to a link in a panel menu, complete the following steps:
1. Define the link extension in the plugin's `module.ts` file.
1. Create a new instance of the `AppPlugin` class, again using the `configureExtensionLink` method. This time, add an `onClick` property which takes a function. This function receives the click event and an object consisting of the `context` and an `openModal` function.
In the following example, we open a dialog.
```typescript
new AppPlugin().configureExtensionLink({
title: 'Go to basic app',
description: 'Will send the user to the basic app',
extensionPointId: 'grafana/dashboard/panel/menu',
path: '/a/myorg-basic-app/one',
onClick: (event, { context, openModal }) => {
event.preventDefault();
openModal({
title: 'My plugin dialog',
body: ({ onDismiss }) => <SampleModal onDismiss={onDismiss} pluginId={context?.pluginId} />,
});
},
});
type Props = {
onDismiss: () => void;
pluginId?: string;
};
const SampleModal = ({ onDismiss, pluginId }: Props) => {
return (
<VerticalGroup spacing="sm">
<p>This dialog was opened via the plugin extensions API.</p>
<p>The panel is using a {pluginId} plugin to display data.</p>
</VerticalGroup>
);
};
```
As you can see, the plugin extensions API enables you to insert links into the UI of Grafana applications that send users to plugin features or trigger actions based on where the user clicked. The plugins extension API can also be used for [cross-plugin linking]({{< relref "./cross-plugin-linking" >}}).