mirror of
https://github.com/grafana/grafana.git
synced 2025-07-28 05:53:46 +08:00
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:
@ -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)
|
@ -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'
|
||||
}
|
||||
}
|
||||
```
|
@ -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")
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
@ -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.
|
@ -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>
|
||||
);
|
||||
};
|
||||
```
|
@ -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: {
|
||||
}
|
||||
```
|
@ -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).
|
@ -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.
|
@ -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',
|
||||
});
|
||||
```
|
@ -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)} />;
|
||||
};
|
||||
```
|
@ -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" >}}).
|
Reference in New Issue
Block a user