mirror of https://github.com/grafana/grafana
Actually remove the plugins docs now that all the links are redirected to https://grafana.com/developers/ (#75632)
Signed-off-by: Jack Baldry <jack.baldry@grafana.com>v8.3.x
parent
d7ac94907c
commit
16b670492a
@ -1,87 +0,0 @@ |
||||
+++ |
||||
title = "Build a plugin" |
||||
aliases = ["/docs/grafana/v8.3/plugins/developing/"] |
||||
+++ |
||||
|
||||
# Build a plugin |
||||
|
||||
For more information on the types of plugins you can build, refer to the [Plugin Overview]({{< relref "../../plugins/_index.md" >}}). |
||||
|
||||
## Get started |
||||
|
||||
The easiest way to start developing Grafana plugins is to use the [Grafana Toolkit](https://www.npmjs.com/package/@grafana/toolkit). |
||||
|
||||
Open the terminal, and run the following command in your [plugin directory]({{< relref "../../administration/configuration.md#plugins" >}}): |
||||
|
||||
```bash |
||||
npx @grafana/toolkit plugin:create my-grafana-plugin |
||||
``` |
||||
|
||||
> **Note:** If running NPM 7+ the `npx` commands mentioned in this article may hang. The workaround is to use `npx --legacy-peer-deps <command to run>`. |
||||
|
||||
If you want a more guided introduction to plugin development, check out our tutorials: |
||||
|
||||
- [Build a panel plugin](/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-a-panel-plugin/) |
||||
- [Build a data source plugin](/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-a-data-source-plugin/) |
||||
|
||||
## Go further |
||||
|
||||
Learn more about specific areas of plugin development. |
||||
|
||||
### Tutorials |
||||
|
||||
If you're looking to build your first plugin, check out these introductory tutorials: |
||||
|
||||
- [Build a panel plugin](/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-a-panel-plugin/) |
||||
- [Build a data source plugin](/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-a-data-source-plugin/) |
||||
- [Build a data source backend plugin](/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-a-data-source-plugin/) |
||||
|
||||
Ready to learn more? Check out our other tutorials: |
||||
|
||||
- [Build a panel plugin with D3.js](/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-a-panel-plugin-with-d3/) |
||||
|
||||
### Guides |
||||
|
||||
Improve an existing plugin with one of our guides: |
||||
|
||||
- [Add authentication for data source plugins]({{< relref "add-authentication-for-data-source-plugins" >}}) |
||||
- [Add support for annotations]({{< relref "add-support-for-annotations.md" >}}) |
||||
- [Add support for Explore queries]({{< relref "add-support-for-explore-queries.md" >}}) |
||||
- [Add support for variables]({{< relref "add-support-for-variables.md" >}}) |
||||
- [Add a query editor help component]({{< relref "add-query-editor-help.md" >}}) |
||||
- [Build a logs data source plugin]({{< relref "build-a-logs-data-source-plugin.md" >}}) |
||||
- [Build a streaming data source plugin]({{< relref "build-a-streaming-data-source-plugin.md" >}}) |
||||
- [Error handling]({{< relref "error-handling.md" >}}) |
||||
- [Working with data frames]({{< relref "working-with-data-frames.md" >}}) |
||||
|
||||
### Concepts |
||||
|
||||
Deepen your knowledge through a series of high-level overviews of plugin concepts: |
||||
|
||||
- [Data frames]({{< relref "data-frames.md" >}}) |
||||
|
||||
### UI library |
||||
|
||||
Explore the many UI components in our [Grafana UI library](https://developers.grafana.com/ui). |
||||
|
||||
### Examples |
||||
|
||||
For inspiration, check out our [plugin examples](https://github.com/grafana/grafana-plugin-examples). |
||||
|
||||
### API reference |
||||
|
||||
Learn more about Grafana options and packages. |
||||
|
||||
#### Metadata |
||||
|
||||
- [Plugin metadata]({{< relref "metadata.md" >}}) |
||||
|
||||
#### Typescript |
||||
|
||||
- Grafana Data |
||||
- Grafana Runtime |
||||
- Grafana UI |
||||
|
||||
#### Go |
||||
|
||||
- [Grafana Plugin SDK for Go]({{< relref "backend/grafana-plugin-sdk-for-go" >}}) |
@ -1,307 +0,0 @@ |
||||
+++ |
||||
title = "Add authentication for data source plugins" |
||||
aliases = ["/docs/grafana/v8.3/plugins/developing/auth-for-datasources/", "/docs/grafana/next/developers/plugins/authentication/"] |
||||
+++ |
||||
|
||||
# Add authentication for data source plugins |
||||
|
||||
This page explains how to configure your data source plugin to authenticate against a third-party API. |
||||
|
||||
There are two ways you can perform authenticated requests from your plugin—using the [_data source proxy_](#authenticate-using-the-data-source-proxy), or by building a [_backend plugin_](#authenticate-using-a-backend-plugin). The one you choose depends on how your plugin authenticates against the third-party API. |
||||
|
||||
- Use the data source proxy if you need to authenticate using Basic Auth or API keys |
||||
- Use the data source proxy if the API supports OAuth 2.0 using client credentials |
||||
- Use a backend plugin if the API uses a custom authentication method that isn't supported by the data source proxy, or if your API communicates over a different protocol than HTTP |
||||
|
||||
Regardless of which approach you use, you first need to encrypt any sensitive information that the plugin needs to store. |
||||
|
||||
## 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 that can access Grafana in their browser can see the contents of `jsonData`. **Only use `jsonData` to store non-sensitive configuration.** |
||||
|
||||
> **Note:** You can see the settings that the current user has access to by entering `window.grafanaBootData` in the developer console of your browser. |
||||
|
||||
> **Note:** Users of [Grafana Enterprise](https://grafana.com/products/enterprise/grafana/) can restrict access to data sources to specific users and teams. For more information, refer to [Data source permissions](https://grafana.com/docs/grafana/v8.3/enterprise/datasource_permissions). |
||||
|
||||
If you need to store sensitive information, such as passwords, tokens and API keys, use `secureJsonData` instead. 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 the secure configuration has been encrypted, 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. |
||||
|
||||
Create a new interface in `types.go` to hold the API key. |
||||
|
||||
```ts |
||||
export interface MySecureJsonData { |
||||
apiKey?: string; |
||||
} |
||||
``` |
||||
|
||||
Add type information to your `secureJsonData` object by updating the props for your `ConfigEditor` to accept the interface as a second type parameter. |
||||
|
||||
```ts |
||||
interface Props extends DataSourcePluginOptionsEditorProps<MyDataSourceOptions, MySecureJsonData> {} |
||||
``` |
||||
|
||||
You can access the value of the secret from the `options` prop inside your `ConfigEditor` until the user saves the configuration. When the user saves the configuration, Grafana clears the value. After that, you can use the `secureJsonFields` to determine whether the property has been configured. |
||||
|
||||
```ts |
||||
const { secureJsonData, secureJsonFields } = options; |
||||
const { apiKey } = secureJsonData; |
||||
``` |
||||
|
||||
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, |
||||
}, |
||||
}); |
||||
}; |
||||
``` |
||||
|
||||
Next, define a component that can accept user input. |
||||
|
||||
```ts |
||||
<Input |
||||
type="password" |
||||
placeholder={secureJsonFields?.apiKey ? 'configured' : ''} |
||||
value={secureJsonData.apiKey ?? ''} |
||||
onChange={onAPIKeyChange} |
||||
/> |
||||
``` |
||||
|
||||
Finally, 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, any 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 you request? |
||||
|
||||
The Grafana server comes with a proxy that lets you define templates for your requests. We call them _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 off. |
||||
|
||||
> **Note:** Be sure not to confuse the data source proxy with the [auth proxy]({{< relref "../../auth/auth-proxy.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](https://grafana.com/docs/grafana/v8.3/developers/plugins/metadata/) file. |
||||
|
||||
1. Add the route to plugin.json. Note that you need to restart the Grafana server every time you make a change to your plugin.json file. |
||||
|
||||
```json |
||||
"routes": [ |
||||
{ |
||||
"path": "example", |
||||
"url": "https://api.example.com" |
||||
} |
||||
] |
||||
``` |
||||
|
||||
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. The following request will be made to `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, add `{{ .JsonData.apiKey }}` to the route, where `apiKey` is the name of a property in the `jsonData` object. |
||||
|
||||
```json |
||||
"routes": [ |
||||
{ |
||||
"path": "example", |
||||
"url": "https://api.example.com/projects/{{ .JsonData.projectId }}" |
||||
} |
||||
] |
||||
``` |
||||
|
||||
You can also configure your route to use sensitive data by using `.SecureJsonData`. |
||||
|
||||
```json |
||||
"routes": [ |
||||
{ |
||||
"path": "example", |
||||
"url": "https://{{ .JsonData.username }}:{{ .SecureJsonData.password }}@api.example.com" |
||||
} |
||||
] |
||||
``` |
||||
|
||||
In addition to the URL, you can also add headers, URL parameters, and a request body, to a proxy route. |
||||
|
||||
#### Add HTTP headers to a proxy route |
||||
|
||||
```json |
||||
"routes": [ |
||||
{ |
||||
"path": "example", |
||||
"url": "https://api.example.com", |
||||
"headers": [ |
||||
{ |
||||
"name": "Authorization", |
||||
"content": "Bearer {{ .SecureJsonData.apiToken }}" |
||||
} |
||||
] |
||||
} |
||||
] |
||||
``` |
||||
|
||||
#### Add URL parameters to a proxy route |
||||
|
||||
```json |
||||
"routes": [ |
||||
{ |
||||
"path": "example", |
||||
"url": "http://api.example.com", |
||||
"urlParams": [ |
||||
{ |
||||
"name": "apiKey", |
||||
"content": "{{ .SecureJsonData.apiKey }}" |
||||
} |
||||
] |
||||
} |
||||
] |
||||
``` |
||||
|
||||
#### Add a request body to a proxy route |
||||
|
||||
```json |
||||
"routes": [ |
||||
{ |
||||
"path": "example", |
||||
"url": "http://api.example.com", |
||||
"body": { |
||||
"username": "{{ .JsonData.username }}", |
||||
"password": "{{ .SecureJsonData.password }}" |
||||
} |
||||
} |
||||
] |
||||
``` |
||||
|
||||
### Add a OAuth 2.0 proxy route to your plugin |
||||
|
||||
The data source proxy supports OAuth 2.0 authentication. |
||||
|
||||
Since the request to each route is made server-side, only machine-to-machine authentication is 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 "./backend/_index.md" >}}). Since 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 "../../auth/generic-oauth.md" >}}), 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) provides 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 will pass the user's token to the plugin in an Authorization header, available on the `QueryDataRequest` object on the `QueryData` request in your backend data source. |
||||
|
||||
```go |
||||
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
for _, q := range req.Queries { |
||||
token := strings.Fields(q.Headers.Get("Authorization")) |
||||
|
||||
var ( |
||||
tokenType = token[0] |
||||
accessToken = token[1] |
||||
) |
||||
|
||||
// ... |
||||
} |
||||
} |
||||
``` |
||||
|
||||
> **Note:** Due to a bug in Grafana, using this feature with PostgreSQL can cause a deadlock. For more information, refer to [Grafana causes deadlocks in PostgreSQL, while trying to refresh users token](https://github.com/grafana/grafana/issues/20515). |
@ -1,74 +0,0 @@ |
||||
+++ |
||||
title = "Add query editor help" |
||||
+++ |
||||
|
||||
# Add a query editor help component |
||||
|
||||
By adding a help component to your plugin, you can for example create "cheat sheets" with commonly used queries. When the user clicks on one of the examples, it automatically updates the query editor. It's a great way to increase productivity for your users. |
||||
|
||||
1. Create a file `QueryEditorHelp.tsx` in the `src` directory of your plugin, 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 the `QueryEditorHelp`. |
||||
|
||||
```ts |
||||
import QueryEditorHelp from './QueryEditorHelp'; |
||||
``` |
||||
|
||||
```ts |
||||
export const plugin = new DataSourcePlugin<DataSource, MyQuery, MyDataSourceOptions>(DataSource) |
||||
.setConfigEditor(ConfigEditor) |
||||
.setQueryEditor(QueryEditor) |
||||
.setExploreQueryField(ExploreQueryEditor) |
||||
.setQueryEditorHelp(QueryEditorHelp); |
||||
``` |
||||
|
||||
1. Create a few examples. |
||||
|
||||
```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> |
||||
); |
||||
}; |
||||
``` |
@ -1,34 +0,0 @@ |
||||
+++ |
||||
title = "Add support for annotations" |
||||
+++ |
||||
|
||||
# Add support for annotations |
||||
|
||||
This guide explains how to add support for [annotations]({{< relref "../../dashboards/annotations.md" >}}) to an existing data source plugin. |
||||
|
||||
This guide assumes that you're already familiar with how to [Build a data source plugin](/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-a-data-source-plugin/). |
||||
|
||||
> **Note:** Annotation support for React plugins was released in Grafana 7.2. To support earlier versions, refer to the [Add support for annotation for Grafana 7.1](https://grafana.com/docs/grafana/v7.1/developers/plugins/add-support-for-annotations/). |
||||
|
||||
## Add annotations support to your data source |
||||
|
||||
To enable annotation support for your data source, add the following two lines of code. 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. |
||||
|
||||
**plugin.json** |
||||
|
||||
```json |
||||
{ |
||||
"annotations": true |
||||
} |
||||
``` |
||||
|
||||
2. In `datasource.ts`, override the `annotations` property from `DataSourceApi`. For the default behavior, you can set `annotations` to an empty object. |
||||
|
||||
**datasource.ts** |
||||
|
||||
```ts |
||||
annotations: { |
||||
} |
||||
``` |
@ -1,103 +0,0 @@ |
||||
+++ |
||||
title = "Add support for Explore queries" |
||||
+++ |
||||
|
||||
# Add support for Explore queries |
||||
|
||||
This guide explains how to improve support for [Explore]({{< relref "../../explore/_index.md" >}}) in an existing data source plugin. |
||||
|
||||
This guide assumes that you're already familiar with how to [Build a data source plugin](/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-a-data-source-plugin/). |
||||
|
||||
With Explore, users can make ad-hoc queries without the use of a dashboard. This is useful when users want to troubleshoot or to learn more about the data. |
||||
|
||||
Your data source already supports Explore by default, and will use the existing query editor for the data source. If you want to offer extended Explore functionality for your data source however, you can define a Explore-specific query editor. |
||||
|
||||
## Add a query editor for Explore |
||||
|
||||
The query editor for Explore is similar to the query editor for the data source itself. In fact, you'll probably reuse the same components for both query editors. |
||||
|
||||
1. Create a file `ExploreQueryEditor.tsx` in the `src` directory of your plugin, with the following content: |
||||
|
||||
```ts |
||||
import React from 'react'; |
||||
|
||||
import { QueryEditorProps } from '@grafana/data'; |
||||
import { QueryField } from '@grafana/ui'; |
||||
import { DataSource } from './DataSource'; |
||||
import { MyQuery, MyDataSourceOptions } from './types'; |
||||
|
||||
export type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>; |
||||
|
||||
export default (props: Props) => { |
||||
return <h2>My query editor</h2>; |
||||
}; |
||||
``` |
||||
|
||||
1. Configure the plugin to use the `ExploreQueryEditor`. |
||||
|
||||
```ts |
||||
import ExploreQueryEditor from './ExploreQueryEditor'; |
||||
``` |
||||
|
||||
```ts |
||||
export const plugin = new DataSourcePlugin<DataSource, MyQuery, MyDataSourceOptions>(DataSource) |
||||
.setConfigEditor(ConfigEditor) |
||||
.setQueryEditor(QueryEditor) |
||||
.setExploreQueryField(ExploreQueryEditor); |
||||
``` |
||||
|
||||
1. Add a `QueryField` to `ExploreQueryEditor`. |
||||
|
||||
```ts |
||||
import { QueryField } from '@grafana/ui'; |
||||
``` |
||||
|
||||
```ts |
||||
export default (props: Props) => { |
||||
const { query } = props; |
||||
|
||||
const onQueryChange = (value: string, override?: boolean) => { |
||||
const { query, onChange, onRunQuery } = props; |
||||
|
||||
if (onChange) { |
||||
// Update the query whenever the query field changes. |
||||
onChange({ ...query, queryText: value }); |
||||
|
||||
// Run the query on Enter. |
||||
if (override && onRunQuery) { |
||||
onRunQuery(); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
return ( |
||||
<QueryField |
||||
portalOrigin="mock-origin" |
||||
onChange={onQueryChange} |
||||
onRunQuery={props.onRunQuery} |
||||
onBlur={props.onBlur} |
||||
query={query.queryText || ''} |
||||
placeholder="Enter a query" |
||||
/> |
||||
); |
||||
}; |
||||
``` |
||||
|
||||
## Selecting preferred visualisation |
||||
|
||||
Explore should by default select a reasonable visualization for your data so users do not have to tweak and play with the visualizations and just focus on querying. This usually works fairly well and Explore can figure out whether the returned data is time series data or logs or something else. |
||||
|
||||
If this does not work for you or you want to show some data in a specific visualization, add a hint to your returned data frame using the `preferredVisualisationType` meta attribute. |
||||
|
||||
You can construct a data frame with specific metadata: |
||||
|
||||
``` |
||||
const firstResult = new MutableDataFrame({ |
||||
fields: [...], |
||||
meta: { |
||||
preferredVisualisationType: 'logs', |
||||
}, |
||||
}); |
||||
``` |
||||
|
||||
For possible options, refer to [PreferredVisualisationType](https://grafana.com/docs/grafana/v8.3/packages_api/data/preferredvisualisationtype/). |
@ -1,207 +0,0 @@ |
||||
+++ |
||||
title = "Add support for variables in plugins" |
||||
+++ |
||||
|
||||
# Add support for variables in plugins |
||||
|
||||
Variables are placeholders for values, and can be used to create things like templated queries and dashboard or panel links. For more information on variables, refer to [Templates and variables]({{< relref "../../variables/_index.md" >}}). |
||||
|
||||
This guide explains how to leverage template variables in your panel plugins and data source plugins. |
||||
|
||||
We'll see how you can turn a 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 it a user-defined template string. |
||||
|
||||
```ts |
||||
export const SimplePanel: React.FC<Props> = ({ options, data, width, height, replaceVariables }) => { |
||||
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 variable, the value of the interpolated variable depends on the [variable format](https://grafana.com/docs/grafana/next/variables/advanced-variable-format-options/). |
||||
|
||||
A data source 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 "../../variables/advanced-variable-format-options.md" >}}). |
||||
|
||||
## 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 LocationSrv.update() |
||||
|
||||
The following example shows how to update a variable called `service`. |
||||
|
||||
- `query` contains the query parameters you want to update. Query parameters controlling variables are prefixed with `var-`. |
||||
- `partial: true` makes the update only affect the query parameters listed in `query`, and leaves the other query parameters unchanged. |
||||
- `replace: true` tells Grafana to update the current URL state, rather than creating a new history entry. |
||||
|
||||
```ts |
||||
import { getLocationSrv } from '@grafana/runtime'; |
||||
``` |
||||
|
||||
```ts |
||||
getLocationSrv().update({ |
||||
query: { |
||||
'var-service': 'billing', |
||||
}, |
||||
partial: true, |
||||
replace: 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 |
||||
|
||||
[Query variables]({{< relref "../../variables/variable-types/add-query-variable.md" >}}) 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, you must override the `metricFindQuery` in your `DataSourceApi` class. `metricFindQuery` 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 default query model and editor for simple text queries. If that's all you need, then you can 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: React.FC<VariableQueryProps> = ({ onChange, query }) => { |
||||
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 which will appear next to the name of the variable in the variables list. |
||||
|
||||
1. Finally, 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 "../../variables/variable-types/add-query-variable.md" >}}) to your dashboard. |
@ -1,68 +0,0 @@ |
||||
+++ |
||||
title = "Backend plugins" |
||||
keywords = ["grafana", "plugins", "backend", "plugin", "backend-plugins", "documentation"] |
||||
aliases = ["/docs/grafana/v8.3/plugins/developing/backend-plugins-guide/"] |
||||
+++ |
||||
|
||||
# Backend plugins |
||||
|
||||
Grafana added support for plugins in version 3.0 and this enabled the Grafana community to create panel plugins and data source plugins. It was wildly successful and has made Grafana much more useful as you can integrate it with anything and do any type of custom visualization that you want. |
||||
|
||||
However, one limitation with these plugins are that they execute on the client-side (in the browser) which makes it hard to support certain use cases/features, e.g. enable Grafana Alerting for data sources. Grafana v7.0 adds official support for backend plugins which removes this limitation. At the same time it gives plugin developers the possibility to extend Grafana in new and interesting ways, with code running in the backend (server side). |
||||
|
||||
We use the term _backend plugin_ to denote that a plugin has a backend component. Still, normally a backend plugin requires frontend components as well. This is for example true for backend data source plugins which normally need configuration and query editor components implemented for the frontend. |
||||
|
||||
Data source plugins can be extended with a backend component. In the future we plan to support additional types and possibly new kinds of plugins, such as [notifiers for Grafana Alerting]({{< relref "../../../alerting/old-alerting/notifications.md" >}}) and custom authentication to name a few. |
||||
|
||||
## Use cases for implementing a backend plugin |
||||
|
||||
The following examples gives you an idea of why you'd consider implementing a backend plugin: |
||||
|
||||
- Enable [Grafana Alerting]({{< relref "../../../alerting" >}}) for data sources. |
||||
- Connect to non-HTTP services that normally can't be connected to from a web browser, e.g. SQL database servers. |
||||
- Keep state between users, e.g. query caching for data sources. |
||||
- Use custom authentication methods and/or authorization checks that aren't supported in Grafana. |
||||
- Use a custom data source request proxy, see [Resources]({{< relref "#resources" >}}). |
||||
|
||||
## Grafana’s backend plugin system |
||||
|
||||
The Grafana backend plugin system is based on the [go-plugin library by HashiCorp](https://github.com/hashicorp/go-plugin). The Grafana server launches each backend plugin as a subprocess and communicates with it over [gRPC](https://grpc.io/). This approach has a number of benefits: |
||||
|
||||
- Plugins can’t crash your grafana process: a panic in a plugin doesn’t panic the server. |
||||
- Plugins are easy to develop: just write a Go application and run `go build` (or use any other language which supports gRPC). |
||||
- Plugins can be relatively secure: The plugin only has access to the interfaces and arguments that are given to it, not to the entire memory space of the process. |
||||
|
||||
Grafana's backend plugin system exposes a couple of different capabilities, or building blocks, that a backend plugin can implement: |
||||
|
||||
- Query data |
||||
- Resources |
||||
- Health checks |
||||
- Collect metrics |
||||
|
||||
### Query data |
||||
|
||||
The query data capability allows a backend plugin to handle data source queries that are submitted from a [dashboard]({{< relref "../../../dashboards/_index.md" >}}), [Explore]({{< relref "../../../explore/_index.md" >}}) or [Grafana Alerting]({{< relref "../../../alerting" >}}). The response contains [data frames]({{< relref "../data-frames.md" >}}), which are used to visualize metrics, logs, and traces. The query data capability is required to implement for a backend data source plugin. |
||||
|
||||
### Resources |
||||
|
||||
The resources capability allows a backend plugin to handle custom HTTP requests sent to the Grafana HTTP API and respond with custom HTTP responses. Here, the request and response formats can vary, e.g. JSON, plain text, HTML or static resources (files, images) etc. Compared to the query data capability where the response contains data frames, resources give the plugin developer a lot of flexibility for extending and open up Grafana for new and interesting use cases. |
||||
|
||||
Examples of use cases for implementing resources: |
||||
|
||||
- Implement a custom data source proxy in case certain authentication/authorization or other requirements are required/needed that are not supported in Grafana's [built-in data proxy]({{< relref "../../../http_api/data_source.md#data-source-proxy-calls" >}}). |
||||
- Return data or information in a format suitable to use within a data source query editor to provide auto-complete functionality. |
||||
- Return static resources, such as images or files. |
||||
- Send a command to a device, such as a micro controller or IOT device. |
||||
- Request information from a device, such as a micro controller or IOT device. |
||||
- Extend Grafana's HTTP API with custom resources, methods and actions. |
||||
- Use [chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) to return large data responses in chunks or to enable "basic" streaming capabilities. |
||||
|
||||
### Health checks |
||||
|
||||
The health checks capability allows a backend plugin to return the status of the plugin. For data source backend plugins the health check will automatically be called when you do _Save & Test_ in the UI when editing a data source. A plugin's health check endpoint is exposed in the Grafana HTTP API and allows external systems to continuously poll the plugin's health to make sure it's running and working as expected. |
||||
|
||||
### Collect metrics |
||||
|
||||
A backend plugin can collect and return runtime, process and custom metrics using the text-based Prometheus [exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/). If you’re using the [Grafana Plugin SDK for Go]({{< relref "grafana-plugin-sdk-for-go.md" >}}) to implement your backend plugin, then the [Prometheus instrumentation library for Go applications](https://github.com/prometheus/client_golang) is built-in, and gives you Go runtime metrics and process metrics out of the box. By using the [Prometheus instrumentation library](https://github.com/prometheus/client_golang) you can add custom metrics to instrument your backend plugin. |
||||
|
||||
A metrics endpoint (`/api/plugins/<plugin id>/metrics`) for a plugin is available in the Grafana HTTP API and allows a Prometheus instance to be configured to scrape the metrics. |
@ -1,19 +0,0 @@ |
||||
+++ |
||||
title = "Grafana Plugin SDK for Go" |
||||
keywords = ["grafana", "plugins", "backend", "plugin", "backend-plugins", "sdk", "documentation"] |
||||
+++ |
||||
|
||||
# Grafana plugin SDK for Go |
||||
|
||||
The Grafana plugin SDK for Go enables building Grafana backend plugins using [Go](https://golang.org/). The SDK provides a high-level framework with APIs, utilities and tooling that abstract away the details of the [plugin protocol]({{< relref "plugin-protocol.md" >}}) and RPC communication so plugin developers do not need to manage either. |
||||
|
||||
The [github.com/grafana/grafana-plugin-sdk-go](https://pkg.go.dev/mod/github.com/grafana/grafana-plugin-sdk-go?tab=overview) is a Go module that provides a set of [Go packages](https://pkg.go.dev/mod/github.com/grafana/grafana-plugin-sdk-go?tab=packages) that can be used to implement a backend plugin. |
||||
|
||||
## Versioning |
||||
|
||||
The SDK is still in development. The [plugin protocol]({{< relref "plugin-protocol.md" >}}) between Grafana and the plugin SDK is versioned separately and considered stable. However, there might be breaking changes introduced in the SDK. This means that plugins using an older version of the SDK should still work with Grafana, but might lose out on new features and capabilities introduced in the SDK. |
||||
|
||||
## See also |
||||
|
||||
- [Source code](https://github.com/grafana/grafana-plugin-sdk-go) |
||||
- [Go reference documentation](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go) |
@ -1,28 +0,0 @@ |
||||
+++ |
||||
title = "Plugin protocol" |
||||
keywords = ["grafana", "plugins", "backend", "plugin", "backend-plugins", "documentation"] |
||||
+++ |
||||
|
||||
# Plugin protocol |
||||
|
||||
There’s a physical wire protocol that Grafana server uses to communicate with backend plugins. This is the contract between Grafana and backend plugins, that must be agreed upon for Grafana and a backend plugin to be able to communicate with each other. The plugin protocol is built on [gRPC](https://grpc.io/) and is defined in [Protocol Buffers (a.k.a., protobuf)](https://developers.google.com/protocol-buffers). |
||||
|
||||
We advise for backend plugins to not be implemented directly against this protocol. Instead, prefer to use the [Grafana Plugin SDK for Go]({{< relref "grafana-plugin-sdk-for-go.md" >}}) that implements this protocol and provides higher level APIs. |
||||
|
||||
The plugin protocol is available in the [GitHub repository](https://github.com/grafana/grafana-plugin-sdk-go/blob/master/proto/backend.proto). The plugin protocol lives in the [Grafana Plugin SDK for Go]({{< relref "grafana-plugin-sdk-for-go.md" >}}) since Grafana itself uses parts of the SDK as a dependency. |
||||
|
||||
## Versioning |
||||
|
||||
Additions of services, messages and fields in the latest version of the plugin protocol are expected to happen, but should not introduce any breaking changes. If breaking changes to the plugin protocol is needed, a new major version of the plugin protocol will be created and released together with a new major Grafana release. Grafana will then support both the old and the new plugin protocol for some time to make sure existing backend plugins continue to work. |
||||
|
||||
Because Grafana maintains the plugin protocol, the plugin protocol attempts to follow Grafana's versioning, However, that doesn't automatically mean that a new major version of the plugin protocol is created when a new major release of Grafana is released. |
||||
|
||||
## Writing plugins without Go |
||||
|
||||
If you want to write a backend plugin in another language than Go, then it’s possible as long as the language supports [gRPC](https://grpc.io/). However, writing a plugin in Go is recommended and has several advantages that should be carefully taken into account before proceeding: |
||||
|
||||
- There's an official [SDK]({{< relref "grafana-plugin-sdk-for-go.md" >}}) available. |
||||
- Single binary as the compiled output. |
||||
- Building and compiling for multiple platforms is easy. |
||||
- A statically compiled binary (in most cases) doesn't require any additional dependencies installed on the target platform enabling it to run “everywhere”. |
||||
- Small footprint in regards to binary size and resource usage. |
@ -1,141 +0,0 @@ |
||||
+++ |
||||
title = "Build a logs data source plugin" |
||||
+++ |
||||
|
||||
# Build a logs data source plugin |
||||
|
||||
This guide explains how to build a logs data source plugin. |
||||
|
||||
Data sources in Grafana supports both metrics and log data. The steps to build a logs data source plugin are largely the same as for a metrics data source. This guide assumes that you're already familiar with how to [Build a data source plugin](/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-a-data-source-plugin/) for metrics. |
||||
|
||||
## Add logs support to your data source |
||||
|
||||
To add logs support to an existing data source, you need to: |
||||
|
||||
- Enable logs support |
||||
- Construct the log data |
||||
- (Optional) Add preferred visualisation type hint to the data frame |
||||
|
||||
### Enable logs support |
||||
|
||||
Tell Grafana that your data source plugin can return log data, by adding `"logs": true` to the [plugin.json]({{< relref "metadata.md" >}}) file. |
||||
|
||||
```json |
||||
{ |
||||
"logs": true |
||||
} |
||||
``` |
||||
|
||||
### Construct the log data |
||||
|
||||
Just like for metrics data, Grafana expects your plugin to return log data as a [data frame]({{< relref "data-frames.md" >}}). |
||||
|
||||
To return log data, return a data frame with at least one time field and one text field from the data source's `query` method. |
||||
|
||||
**Example:** |
||||
|
||||
```ts |
||||
const frame = new MutableDataFrame({ |
||||
refId: query.refId, |
||||
fields: [ |
||||
{ name: 'time', type: FieldType.time }, |
||||
{ name: 'content', type: FieldType.string }, |
||||
], |
||||
}); |
||||
|
||||
frame.add({ time: 1589189388597, content: 'user registered' }); |
||||
frame.add({ time: 1589189406480, content: 'user logged in' }); |
||||
``` |
||||
|
||||
That's all you need to start returning log data from your data source. Go ahead and try it out in [Explore]({{< relref "../../explore/_index.md" >}}) or by adding a [Logs panel]({{< relref "../../visualizations/logs-panel.md" >}}). |
||||
|
||||
Congratulations, you just wrote your first logs data source plugin! Next, let's look at a couple of features that can further improve the experience for the user. |
||||
|
||||
### (Optional) Add preferred visualisation type hint to the data frame |
||||
|
||||
To make sure Grafana recognizes data as logs and shows logs visualization automatically in Explore you have do set `meta.preferredVisualisationType` to `'logs'` in the returned data frame. See [Selecting preferred visualisation section]({{< relref "add-support-for-explore-queries.md#selecting-preferred-visualisation" >}}) |
||||
|
||||
**Example:** |
||||
|
||||
```ts |
||||
const frame = new MutableDataFrame({ |
||||
refId: query.refId, |
||||
meta: { |
||||
preferredVisualisationType: 'logs', |
||||
}, |
||||
fields: [ |
||||
{ name: 'time', type: FieldType.time }, |
||||
{ name: 'content', type: FieldType.string }, |
||||
], |
||||
}); |
||||
``` |
||||
|
||||
## Add labels to your logs |
||||
|
||||
To help filter log lines, many log systems let you query logs based on metadata, or _labels_. |
||||
|
||||
You can add labels to a stream of logs by setting the labels property on the Field. |
||||
|
||||
**Example**: |
||||
|
||||
```ts |
||||
const frame = new MutableDataFrame({ |
||||
refId: query.refId, |
||||
fields: [ |
||||
{ name: 'time', type: FieldType.time }, |
||||
{ name: 'content', type: FieldType.string, labels: { filename: 'file.txt' } }, |
||||
], |
||||
}); |
||||
|
||||
frame.add({ time: 1589189388597, content: 'user registered' }); |
||||
frame.add({ time: 1589189406480, content: 'user logged in' }); |
||||
``` |
||||
|
||||
## Extract detected fields from your logs |
||||
|
||||
You can add additional information about each log line by adding more data frame fields. |
||||
|
||||
If a data frame has more than one text field, then Grafana assumes the first field in the data frame to be the actual log line. Any subsequent text fields are treated as [detected fields]({{< relref "../../explore/_index.md#labels-and-detected-fields" >}}). |
||||
|
||||
While you can add any number of custom fields to your data frame, Grafana comes with a couple of dedicated fields: `levels` and `id`. Let's have a closer look at each one. |
||||
|
||||
### Levels |
||||
|
||||
To set the level for each log line, add a `level` field. |
||||
|
||||
**Example:** |
||||
|
||||
```ts |
||||
const frame = new MutableDataFrame({ |
||||
refId: query.refId, |
||||
fields: [ |
||||
{ name: 'time', type: FieldType.time }, |
||||
{ name: 'content', type: FieldType.string, labels: { filename: 'file.txt' } }, |
||||
{ name: 'level', type: FieldType.string }, |
||||
], |
||||
}); |
||||
|
||||
frame.add({ time: 1589189388597, content: 'user registered', level: 'info' }); |
||||
frame.add({ time: 1589189406480, content: 'unknown error', level: 'error' }); |
||||
``` |
||||
|
||||
### Unique log lines |
||||
|
||||
By default, Grafana offers basic support for deduplicating log lines. You can improve the support by adding an `id` field to explicitly assign identifiers to each log line. |
||||
|
||||
**Example:** |
||||
|
||||
```ts |
||||
const frame = new MutableDataFrame({ |
||||
refId: query.refId, |
||||
fields: [ |
||||
{ name: 'time', type: FieldType.time }, |
||||
{ name: 'content', type: FieldType.string, labels: { filename: 'file.txt' } }, |
||||
{ name: 'level', type: FieldType.string }, |
||||
{ name: 'id', type: FieldType.string }, |
||||
], |
||||
}); |
||||
|
||||
frame.add({ time: 1589189388597, content: 'user registered', level: 'info', id: 'd3b07384d113edec49eaa6238ad5ff00' }); |
||||
frame.add({ time: 1589189406480, content: 'unknown error', level: 'error', id: 'c157a79031e1c40f85931829bc5fc552' }); |
||||
``` |
@ -1,137 +0,0 @@ |
||||
+++ |
||||
title = "Build a streaming data source plugin" |
||||
+++ |
||||
|
||||
# Build a streaming data source plugin |
||||
|
||||
This guide explains how to build a streaming data source plugin. |
||||
|
||||
This guide assumes that you're already familiar with how to [Build a data source plugin](/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-a-data-source-plugin/). |
||||
|
||||
When monitoring critical applications, you want your dashboard to refresh as soon as your data does. In Grafana, you can set your dashboards to automatically refresh at a certain interval, no matter what data source you use. Unfortunately, this means that your queries are requesting all the data to be sent again, regardless of whether the data has actually changed. |
||||
|
||||
By enabling _streaming_ for your data source plugin, you can update your dashboard as soon as new data becomes available. |
||||
|
||||
For example, a streaming data source plugin can connect to a websocket, or subscribe to a message bus, and update the visualization whenever a new message is available. |
||||
|
||||
Let's see how you can add streaming to an existing data source! |
||||
|
||||
Grafana uses [RxJS](https://rxjs.dev/) to continuously send data from a data source to a panel visualization. There's a lot more to RxJS than what's covered in this guide. If you want to learn more, check out the [RxJS documentation](https://rxjs.dev/guide/overview). |
||||
|
||||
1. Enable streaming for your data source in the `plugin.json` file. |
||||
|
||||
```json |
||||
{ |
||||
"streaming": true |
||||
} |
||||
``` |
||||
|
||||
1. Change the signature of the `query` method to return an `Observable` from the `rxjs` package. Make sure you remove the `async` keyword. |
||||
|
||||
```ts |
||||
import { Observable } from 'rxjs'; |
||||
``` |
||||
|
||||
```ts |
||||
query(options: DataQueryRequest<MyQuery>): Observable<DataQueryResponse> { |
||||
// ... |
||||
} |
||||
``` |
||||
|
||||
1. Create an `Observable` for each query, and then combine them all using the `merge` function from the `rxjs` package. |
||||
|
||||
```ts |
||||
import { Observable, merge } from 'rxjs'; |
||||
``` |
||||
|
||||
```ts |
||||
const observables = options.targets.map((target) => { |
||||
return new Observable<DataQueryResponse>((subscriber) => { |
||||
// ... |
||||
}); |
||||
}); |
||||
|
||||
return merge(...observables); |
||||
``` |
||||
|
||||
1. In the `subscribe` function, create a `CircularDataFrame`. |
||||
|
||||
```ts |
||||
import { CircularDataFrame } from '@grafana/data'; |
||||
``` |
||||
|
||||
```ts |
||||
const frame = new CircularDataFrame({ |
||||
append: 'tail', |
||||
capacity: 1000, |
||||
}); |
||||
|
||||
frame.refId = query.refId; |
||||
frame.addField({ name: 'time', type: FieldType.time }); |
||||
frame.addField({ name: 'value', type: FieldType.number }); |
||||
``` |
||||
|
||||
Circular data frames have a limited capacity. When a circular data frame reaches its capacity, the oldest data point is removed. |
||||
|
||||
1. Use `subscriber.next()` to send the updated data frame whenever you receive new updates. |
||||
|
||||
```ts |
||||
import { LoadingState } from '@grafana/data'; |
||||
``` |
||||
|
||||
```ts |
||||
const intervalId = setInterval(() => { |
||||
frame.add({ time: Date.now(), value: Math.random() }); |
||||
|
||||
subscriber.next({ |
||||
data: [frame], |
||||
key: query.refId, |
||||
state: LoadingState.Streaming, |
||||
}); |
||||
}, 500); |
||||
|
||||
return () => { |
||||
clearInterval(intervalId); |
||||
}; |
||||
``` |
||||
|
||||
> **Note:** In practice, you'd call `subscriber.next` as soon as you receive new data from a websocket or a message bus. The example above simulates data being received every 500 milliseconds. |
||||
|
||||
Here's the final `query` method. |
||||
|
||||
```ts |
||||
query(options: DataQueryRequest<MyQuery>): Observable<DataQueryResponse> { |
||||
const streams = options.targets.map(target => { |
||||
const query = defaults(target, defaultQuery); |
||||
|
||||
return new Observable<DataQueryResponse>(subscriber => { |
||||
const frame = new CircularDataFrame({ |
||||
append: 'tail', |
||||
capacity: 1000, |
||||
}); |
||||
|
||||
frame.refId = query.refId; |
||||
frame.addField({ name: 'time', type: FieldType.time }); |
||||
frame.addField({ name: 'value', type: FieldType.number }); |
||||
|
||||
const intervalId = setInterval(() => { |
||||
frame.add({ time: Date.now(), value: Math.random() }); |
||||
|
||||
subscriber.next({ |
||||
data: [frame], |
||||
key: query.refId, |
||||
state: LoadingState.Streaming, |
||||
}); |
||||
}, 100); |
||||
|
||||
return () => { |
||||
clearInterval(intervalId); |
||||
}; |
||||
}); |
||||
}); |
||||
|
||||
return merge(...streams); |
||||
} |
||||
``` |
||||
|
||||
One limitation with this example is that the panel visualization is cleared every time you update the dashboard. If you have access to historical data, you can add, or _backfill_, it to the data frame before the first call to `subscriber.next()`. |
@ -1,118 +0,0 @@ |
||||
+++ |
||||
title = "Custom panel option editors" |
||||
+++ |
||||
|
||||
# Custom panel option editors |
||||
|
||||
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. In this guide, you'll learn how to build your own panel option editor. |
||||
|
||||
The simplest editor is a React component that accepts two props: `value` and `onChange`. `value` contains the current value of the option, and `onChange` updates it. |
||||
|
||||
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: React.FC<StandardEditorProps<boolean>> = ({ value, onChange }) => { |
||||
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. Configure the editor to use by setting the `editor` property to the `SimpleEditor` 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 |
||||
|
||||
If you're using your custom editor to configure multiple options, you might want to be able to customize it. Add settings to your editor by setting the second template variable of `StandardEditorProps` to an interface that contains the settings you want to be able to configure. |
||||
|
||||
You can 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 range is defined by the `from` and `to` properties in the `Settings` interface. |
||||
|
||||
**SimpleEditor.tsx** |
||||
|
||||
```ts |
||||
interface Settings { |
||||
from: number; |
||||
to: number; |
||||
} |
||||
|
||||
export const SimpleEditor: React.FC<StandardEditorProps<number, Settings>> = ({ item, value, onChange }) => { |
||||
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 in the call to `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. |
||||
|
||||
> **Note:** This feature was introduced in 7.0.3. Anyone using an older version of Grafana will see an error when using your plugin. |
||||
|
||||
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: React.FC<StandardEditorProps<string>> = ({ item, value, onChange, context }) => { |
||||
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)} />; |
||||
}; |
||||
``` |
||||
|
||||
Have you built a custom editor that you think would be useful to other plugin developers? Consider contributing it as a standard editor! |
@ -1,182 +0,0 @@ |
||||
+++ |
||||
title = "Data frames" |
||||
+++ |
||||
|
||||
# Data frames |
||||
|
||||
Grafana supports a variety of different data sources, each with its own data model. To make this possible, Grafana consolidates the query results from each of these data sources into one unified data structure called a _data frame_. |
||||
|
||||
The data frame structure is a concept that's borrowed from data analysis tools like the [R programming language](https://www.r-project.org), and [Pandas](https://pandas.pydata.org/). |
||||
|
||||
> Data frames are available in Grafana 7.0+, and replaced the Time series and Table structures with a more generic data structure that can support a wider range of data types. |
||||
|
||||
This document gives an overview of the data frame structure, and of how data is handled within Grafana. |
||||
|
||||
## The data frame |
||||
|
||||
A data frame is a columnar-oriented table structure, which means it stores data by column and not by row. To understand what this means, let’s look at the TypeScript definition used by Grafana: |
||||
|
||||
```ts |
||||
interface DataFrame { |
||||
name?: string; |
||||
// reference to query that create the frame |
||||
refId?: string; |
||||
|
||||
fields: []Field; |
||||
} |
||||
``` |
||||
|
||||
In essence, a data frame is a collection of _fields_, where each field corresponds to a column. Each field, in turn, consists of a collection of values, along with meta information, such as the data type of those values. |
||||
|
||||
```ts |
||||
interface Field { |
||||
name: string; |
||||
// Prometheus like Labels / Tags |
||||
labels?: Record<string, string>; |
||||
|
||||
// For example string, number, time (or more specific primitives in the backend) |
||||
type: FieldType; |
||||
// Array of values all of the same type |
||||
values: Vector<T>; |
||||
|
||||
// Optional display data for the field (e.g. unit, name over-ride, etc) |
||||
config: FieldConfig; |
||||
} |
||||
``` |
||||
|
||||
Let's look an example. The table below demonstrates a data frame with two fields, _time_ and _temperature_. |
||||
|
||||
| time | temperature | |
||||
| ------------------- | ----------- | |
||||
| 2020-01-02 03:04:00 | 45.0 | |
||||
| 2020-01-02 03:05:00 | 47.0 | |
||||
| 2020-01-02 03:06:00 | 48.0 | |
||||
|
||||
Each field has three values, and each value in a field must share the same type. In this case, all values in the time field are timestamps, and all values in the temperature field are numbers. |
||||
|
||||
One restriction on data frames is that all fields in the frame must be of the same length to be a valid data frame. |
||||
|
||||
### Field configuration |
||||
|
||||
Each field in a data frame contains optional information about the values in the field, such as units, scaling, and so on. |
||||
|
||||
By adding field configurations to a data frame, Grafana can configure visualizations automatically. For example, you could configure Grafana to automatically set the unit provided by the data source. |
||||
|
||||
## Transformations |
||||
|
||||
Along with the type information, field configs enable _data transformations_ within Grafana. |
||||
|
||||
A data transformation is any function that accepts a data frame as input, and returns another data frame as output. By using data frames in your plugin, you get a range of transformations for free. |
||||
|
||||
## Data frames as time series |
||||
|
||||
A data frame with at least one time field is considered a _time series_. |
||||
|
||||
For more information on time series, refer to our [Introduction to time series]({{< relref "../../basics/timeseries.md" >}}). |
||||
|
||||
### Wide format |
||||
|
||||
When a collection of time series shares the same _time index_—the time fields in each time series are identical—they can be stored together, in a _wide_ format. By reusing the time field, we can reduce the amount of data being sent to the browser. |
||||
|
||||
In this example, the `cpu` usage from each host share the time index, so we can store them in the same data frame. |
||||
|
||||
```text |
||||
Name: Wide |
||||
Dimensions: 3 fields by 2 rows |
||||
+---------------------+-----------------+-----------------+ |
||||
| Name: time | Name: cpu | Name: cpu | |
||||
| Labels: | Labels: host=a | Labels: host=b | |
||||
| Type: []time.Time | Type: []float64 | Type: []float64 | |
||||
+---------------------+-----------------+-----------------+ |
||||
| 2020-01-02 03:04:00 | 3 | 4 | |
||||
| 2020-01-02 03:05:00 | 6 | 7 | |
||||
+---------------------+-----------------+-----------------+ |
||||
``` |
||||
|
||||
However, if the two time series don't share the same time values, they are represented as two distinct data frames. |
||||
|
||||
```text |
||||
Name: cpu |
||||
Dimensions: 2 fields by 2 rows |
||||
+---------------------+-----------------+ |
||||
| Name: time | Name: cpu | |
||||
| Labels: | Labels: host=a | |
||||
| Type: []time.Time | Type: []float64 | |
||||
+---------------------+-----------------+ |
||||
| 2020-01-02 03:04:00 | 3 | |
||||
| 2020-01-02 03:05:00 | 6 | |
||||
+---------------------+-----------------+ |
||||
|
||||
Name: cpu |
||||
Dimensions: 2 fields by 2 rows |
||||
+---------------------+-----------------+ |
||||
| Name: time | Name: cpu | |
||||
| Labels: | Labels: host=b | |
||||
| Type: []time.Time | Type: []float64 | |
||||
+---------------------+-----------------+ |
||||
| 2020-01-02 03:04:01 | 4 | |
||||
| 2020-01-02 03:05:01 | 7 | |
||||
+---------------------+-----------------+ |
||||
``` |
||||
|
||||
The wide format can typically be used when multiple time series are collected by the same process. In this case, every measurement is made at the same interval and will therefore share the same time values. |
||||
|
||||
### Long format |
||||
|
||||
Some data sources return data in a _long_ format (also called _narrow_ format). This is common format returned by, for example, SQL databases. |
||||
|
||||
In long format, string values are represented as separate fields rather than as labels. As a result, a data form in long form may have duplicated time values. |
||||
|
||||
Grafana can detect and convert data frames in long format into wide format. |
||||
|
||||
> **Note:** Long format is currently only supported in the backend: [Grafana Issue #22219](https://github.com/grafana/grafana/issues/22219). |
||||
|
||||
For example, the following data frame in long format: |
||||
|
||||
```text |
||||
Name: Long |
||||
Dimensions: 4 fields by 4 rows |
||||
+---------------------+-----------------+-----------------+----------------+ |
||||
| Name: time | Name: aMetric | Name: bMetric | Name: host | |
||||
| Labels: | Labels: | Labels: | Labels: | |
||||
| Type: []time.Time | Type: []float64 | Type: []float64 | Type: []string | |
||||
+---------------------+-----------------+-----------------+----------------+ |
||||
| 2020-01-02 03:04:00 | 2 | 10 | foo | |
||||
| 2020-01-02 03:04:00 | 5 | 15 | bar | |
||||
| 2020-01-02 03:05:00 | 3 | 11 | foo | |
||||
| 2020-01-02 03:05:00 | 6 | 16 | bar | |
||||
+---------------------+-----------------+-----------------+----------------+ |
||||
``` |
||||
|
||||
can be converted into a data frame in wide format: |
||||
|
||||
```text |
||||
Name: Wide |
||||
Dimensions: 5 fields by 2 rows |
||||
+---------------------+------------------+------------------+------------------+------------------+ |
||||
| Name: time | Name: aMetric | Name: bMetric | Name: aMetric | Name: bMetric | |
||||
| Labels: | Labels: host=foo | Labels: host=foo | Labels: host=bar | Labels: host=bar | |
||||
| Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []float64 | |
||||
+---------------------+------------------+------------------+------------------+------------------+ |
||||
| 2020-01-02 03:04:00 | 2 | 10 | 5 | 15 | |
||||
| 2020-01-02 03:05:00 | 3 | 11 | 6 | 16 | |
||||
+---------------------+------------------+------------------+------------------+------------------+ |
||||
``` |
||||
|
||||
> **Note:** Not all panels support the wide time series data frame format. To keep full backward compatibility we have introduced a transformation that can be used to convert from the wide to the long format. Read more about how to use it here: [Prepare time series-transformation]({{< relref "../../panels/transformations/types-options.md#prepare-time-series" >}}). |
||||
|
||||
## Technical references |
||||
|
||||
This section contains links to technical reference and implementations of data frames. |
||||
|
||||
### Apache Arrow |
||||
|
||||
The data frame structure is inspired by, and uses the [Apache Arrow Project](https://arrow.apache.org/). Javascript Data frames use Arrow Tables as the underlying structure, and the backend Go code serializes its Frames in Arrow Tables for transmission. |
||||
|
||||
### Javascript |
||||
|
||||
The Javascript implementation of data frames is in the [`/src/dataframe` folder](https://github.com/grafana/grafana/tree/main/packages/grafana-data/src/dataframe) and [`/src/types/dataframe.ts`](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/dataFrame.ts) of the [`@grafana/data` package](https://github.com/grafana/grafana/tree/main/packages/grafana-data). |
||||
|
||||
### Go |
||||
|
||||
For documentation on the Go implementation of data frames, refer to the [github.com/grafana/grafana-plugin-sdk-go/data package](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/data?tab=doc). |
@ -1,63 +0,0 @@ |
||||
+++ |
||||
title = "Error handling" |
||||
+++ |
||||
|
||||
# Error handling |
||||
|
||||
This guide explains how to handle errors in plugins. |
||||
|
||||
## Provide usable defaults |
||||
|
||||
Allow the user to learn your plugin in small steps. Provide a useful default configuration so that: |
||||
|
||||
- The user can get started right away. |
||||
- You can avoid unnecessary error messages. |
||||
|
||||
For example, by selecting the first field of an expected type, the panel can display a visualization without any user configuration. If a user explicitly selects a field, then use that one. Otherwise, default to the first field of type `string`: |
||||
|
||||
```ts |
||||
const numberField = frame.fields.find((field) => |
||||
options.numberFieldName ? field.name === options.numberFieldName : field.type === FieldType.number |
||||
); |
||||
``` |
||||
|
||||
## Display error messages |
||||
|
||||
To display an error message to the user, `throw` an `Error` with the message you want to display: |
||||
|
||||
```ts |
||||
throw new Error('An error occurred'); |
||||
``` |
||||
|
||||
Grafana displays the error message in the top-left corner of the panel. |
||||
|
||||
{{< figure src="/static/img/docs/panel_error.png" class="docs-image--no-shadow" max-width="850px" >}} |
||||
|
||||
Avoid displaying overly-technical error messages to the user. If you want to let technical users report an error, consider logging it instead. |
||||
|
||||
```ts |
||||
try { |
||||
failingFunction(); |
||||
} catch (err) { |
||||
console.error(err); |
||||
throw new Error('Something went wrong'); |
||||
} |
||||
``` |
||||
|
||||
> **Note:** Grafana displays the exception message in the UI as written, so we recommend using grammatically correct sentences. For more information, refer to the [Documentation style guide](https://github.com/grafana/grafana/blob/main/contribute/style-guides/documentation-style-guide.md). |
||||
|
||||
Here are some examples of situations where you might want to display an error to the user. |
||||
|
||||
### Invalid query response |
||||
|
||||
Users have full freedom when they create data source queries for panels. If your panel plugin requires a specific format for the query response, then use the panel canvas to guide the user. |
||||
|
||||
```ts |
||||
if (!numberField) { |
||||
throw new Error('Query result is missing a number field'); |
||||
} |
||||
|
||||
if (frame.length === 0) { |
||||
throw new Error('Query returned an empty result'); |
||||
} |
||||
``` |
@ -1,127 +0,0 @@ |
||||
+++ |
||||
title = "Legacy plugins" |
||||
aliases = ["/docs/grafana/v8.3/plugins/development/", "/docs/grafana/next/plugins/datasources/", "/docs/grafana/next/plugins/apps/", "/docs/grafana/next/plugins/panels/", "/docs/grafana/next/plugins/developing/development/"] |
||||
+++ |
||||
|
||||
# Legacy plugins |
||||
|
||||
> **Note:** Since Grafana 7.0, writing plugins using Angular is no longer recommended. If you're looking to build a new plugin, refer to [Plugins]({{< relref "../_index.md" >}}). |
||||
|
||||
You can extend Grafana by writing your own plugins and then share them with other users in [our plugin repository](https://grafana.com/plugins). |
||||
|
||||
Grafana already has a strong community of contributors and plugin developers. By making it easier to develop and install plugins with resources such as this guide, we hope that the community can grow even stronger and develop new plugins that we would never think about. |
||||
|
||||
## Short version |
||||
|
||||
1. [Set up Grafana](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md) |
||||
1. Clone an example plugin into `/var/lib/grafana/plugins` or `data/plugins` (relative to grafana git repo if you're running development version from source dir) |
||||
1. Use one of our example plugins as a starting point |
||||
|
||||
Example plugins |
||||
|
||||
- ["Hello World" panel using Angular](https://github.com/grafana/simple-angular-panel) |
||||
- ["Hello World" panel using React](https://github.com/grafana/simple-react-panel) |
||||
- [Simple json data source](https://github.com/grafana/simple-json-datasource) |
||||
- [Clock panel](https://github.com/grafana/clock-panel) |
||||
- [Pie chart panel](https://github.com/grafana/piechart-panel) |
||||
|
||||
You might also be interested in the available tutorials around authoring a plugin. |
||||
|
||||
- [Grafana Tutorials](https://grafana.com/tutorials/) |
||||
|
||||
## What languages? |
||||
|
||||
Since everything turns into JavaScript, it's up to you to choose which language you want. That said, it's probably a good idea to choose es6 or TypeScript, because we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo if you choose one of those languages. |
||||
|
||||
## Buildscript |
||||
|
||||
You can use any build system that supports systemjs. All the built content should end up in a folder named `dist` and be committed to the repository. By committing the dist folder, the person who installs your plugin does not have to run any build script. All of our example plugins have a build script configured. |
||||
|
||||
## Keep your plugin up to date |
||||
|
||||
New versions of Grafana can sometimes cause plugins to break. Check out our [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/main/PLUGIN_DEV.md) doc for changes in |
||||
Grafana that can impact your plugin. |
||||
|
||||
## Metadata |
||||
|
||||
See the [coding styleguide]({{< relref "style-guide.md" >}}) for details on the metadata. |
||||
|
||||
## module.(js|ts) |
||||
|
||||
This is the entry point for every plugin. This is the place where you should export |
||||
your plugin implementation. Depending on what kind of plugin you are developing you |
||||
will be expected to export different things. You can find what's expected for [datasource]({{< relref "data-sources.md" >}}), [panels]({{< relref "panels.md" >}}) |
||||
and [apps]({{< relref "apps.md" >}}) plugins in the documentation. |
||||
|
||||
The Grafana SDK is quite small so far and can be found here: |
||||
|
||||
- [SDK file in Grafana](https://github.com/grafana/grafana/blob/main/public/app/plugins/sdk.ts) |
||||
|
||||
The SDK contains three different plugin classes: PanelCtrl, MetricsPanelCtrl and QueryCtrl. For plugins of the panel type, the module.js file should export one of these. There are some extra classes for [data sources]({{< relref "data-sources.md" >}}). |
||||
|
||||
Example: |
||||
|
||||
```javascript |
||||
import { ClockCtrl } from './clock_ctrl'; |
||||
|
||||
export { ClockCtrl as PanelCtrl }; |
||||
``` |
||||
|
||||
The module class is also where css for the dark and light themes is imported: |
||||
|
||||
```javascript |
||||
import { loadPluginCss } from 'app/plugins/sdk'; |
||||
import WorldmapCtrl from './worldmap_ctrl'; |
||||
|
||||
loadPluginCss({ |
||||
dark: 'plugins/grafana-worldmap-panel/css/worldmap.dark.css', |
||||
light: 'plugins/grafana-worldmap-panel/css/worldmap.light.css', |
||||
}); |
||||
|
||||
export { WorldmapCtrl as PanelCtrl }; |
||||
``` |
||||
|
||||
## Start developing your plugin |
||||
|
||||
There are three ways that you can start developing a Grafana plugin. |
||||
|
||||
1. Set up a Grafana development environment. [(described here)](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md) and place your plugin in the `data/plugins` folder. |
||||
1. Install Grafana and place your plugin in the plugins directory which is set in your [config file](/administration/configuration). By default this is `/var/lib/grafana/plugins` on Linux systems. |
||||
1. Place your plugin directory anywhere you like and specify it grafana.ini. |
||||
|
||||
We encourage people to set up the full Grafana environment so that you can get inspiration from the rest of the Grafana code base. |
||||
|
||||
When Grafana starts, it scans the plugin folders and mounts every folder that contains a plugin.json file unless |
||||
the folder contains a subfolder named dist. In that case, Grafana mounts the dist folder instead. |
||||
This makes it possible to have both built and src content in the same plugin Git repo. |
||||
|
||||
## Grafana Events |
||||
|
||||
There are a number of Grafana events that a plugin can hook into: |
||||
|
||||
- `init-edit-mode` can be used to add tabs when editing a panel |
||||
- `panel-teardown` can be used for clean up |
||||
- `data-received` is an event in that is triggered on data refresh and can be hooked into |
||||
- `data-snapshot-load` is an event triggered to load data when in snapshot mode. |
||||
- `data-error` is used to handle errors on dashboard refresh. |
||||
|
||||
If a panel receives data and hooks into the `data-received` event then it should handle snapshot mode too. Otherwise the panel will not work if saved as a snapshot. [Getting Plugins to work in Snapshot Mode]({{< relref "snapshot-mode.md" >}}) describes how to add support for this. |
||||
|
||||
## Examples |
||||
|
||||
We have three different examples that you can fork/download to get started developing your Grafana plugin. |
||||
|
||||
- [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small data source plugin for querying json data from backends) |
||||
- [simple-app-plugin](https://github.com/grafana/simple-app-plugin) |
||||
- [clock-panel](https://github.com/grafana/clock-panel) |
||||
- [singlestat-panel](https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/singlestat) |
||||
- [piechart-panel](https://github.com/grafana/piechart-panel) |
||||
|
||||
## Other Articles |
||||
|
||||
- [Getting Plugins to work in Snapshot Mode]({{< relref "snapshot-mode.md" >}}) |
||||
- [Plugin Defaults and Editor Mode]({{< relref "defaults-and-editor-mode.md" >}}) |
||||
- [Grafana Plugin Code Styleguide]({{< relref "style-guide.md" >}}) |
||||
- [Grafana Apps]({{< relref "apps.md" >}}) |
||||
- [Grafana Data Sources]({{< relref "data-sources.md" >}}) |
||||
- [plugin.json Schema]({{< relref "../metadata.md" >}}) |
@ -1,54 +0,0 @@ |
||||
+++ |
||||
title = "Legacy app plugins" |
||||
keywords = ["grafana", "plugins", "documentation"] |
||||
aliases = ["/docs/grafana/v8.3/plugins/developing/apps/"] |
||||
+++ |
||||
|
||||
# Legacy app plugins |
||||
|
||||
App plugins are Grafana plugins that can bundle data source and panel plugins within one package. They also enable the plugin author to create custom pages within Grafana. The custom pages enable the plugin author to include things like documentation, sign-up forms, or to control other services with HTTP requests. |
||||
|
||||
Data source and panel plugins will show up like normal plugins. The app pages will be available in the main menu. |
||||
|
||||
{{< figure class="float-right" src="/static/img/docs/v3/app-in-main-menu.png" caption="App in Main Menu" >}} |
||||
|
||||
## Enabling app plugins |
||||
|
||||
After installing an app, it has to be enabled before it shows up as a data source or panel. You can do that on the app page in the configuration tab. |
||||
|
||||
## Developing an App Plugin |
||||
|
||||
An App is a bundle of panels, dashboards and/or data source(s). There is nothing different about developing panels and data sources for an app. |
||||
|
||||
Apps have to be enabled in Grafana and should import any included dashboards when the user enables it. A ConfigCtrl class should be created and the dashboards imported in the postUpdate hook. See example below: |
||||
|
||||
```javascript |
||||
export class ConfigCtrl { |
||||
/** @ngInject */ |
||||
constructor($scope, $injector, $q) { |
||||
this.$q = $q; |
||||
this.enabled = false; |
||||
this.appEditCtrl.setPostUpdateHook(this.postUpdate.bind(this)); |
||||
} |
||||
|
||||
postUpdate() { |
||||
if (!this.appModel.enabled) { |
||||
return; |
||||
} |
||||
|
||||
// TODO, whatever you want |
||||
console.log('Post Update:', this); |
||||
} |
||||
} |
||||
ConfigCtrl.templateUrl = 'components/config/config.html'; |
||||
``` |
||||
|
||||
If possible, a link to a dashboard or custom page should be shown after enabling the app to guide the user to the appropriate place. |
||||
|
||||
{{< figure class="float-right" src="/static/img/docs/app_plugin_after_enable.png" caption="After enabling" >}} |
||||
|
||||
### Develop your own App |
||||
|
||||
> Our goal is not to have a very extensive documentation but rather have actual |
||||
> code that people can look at. An example implementation of an app can be found |
||||
> in this [example app repo](https://github.com/grafana/simple-app-plugin) |
@ -1,180 +0,0 @@ |
||||
+++ |
||||
title = "Legacy data source plugins" |
||||
keywords = ["grafana", "plugins", "documentation"] |
||||
aliases = ["/docs/grafana/v8.3/plugins/developing/datasources/"] |
||||
+++ |
||||
|
||||
# Legacy data source plugins |
||||
|
||||
Data source plugins enable people to develop plugins for any database that |
||||
communicates over HTTP. Its up to the plugin to transform the data into |
||||
time series data so that any grafana panel can then show it. |
||||
|
||||
## Data source development |
||||
|
||||
> Our goal is not to have a very extensive documentation but rather have actual |
||||
> code that people can look at. Example implementations of a data source can be |
||||
> found in these repos: |
||||
|
||||
> - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) |
||||
> - [simple-datasource](https://github.com/grafana/simple-datasource) |
||||
> - [simple-json-backend-datasource](https://github.com/grafana/simple-json-backend-datasource) |
||||
|
||||
To interact with the rest of grafana the plugins module file can export 4 different components. |
||||
|
||||
- Datasource (Required) |
||||
- QueryCtrl (Required) |
||||
- ConfigCtrl (Required) |
||||
- AnnotationsQueryCtrl |
||||
|
||||
## Plugin json |
||||
|
||||
There are two data source specific settings for the plugin.json |
||||
|
||||
```json |
||||
"metrics": true, |
||||
"annotations": false, |
||||
``` |
||||
|
||||
These settings indicate what kind of data the plugin can deliver. At least one of them has to be true. |
||||
|
||||
## Data source |
||||
|
||||
The javascript object that communicates with the database and transforms data to times series. |
||||
|
||||
The Data source should contain the following functions: |
||||
|
||||
```javascript |
||||
query(options); // used by panels to get data |
||||
testDatasource(); // used by data source configuration page to make sure the connection is working |
||||
annotationQuery(options); // used by dashboards to get annotations |
||||
metricFindQuery(options); // used by query editor to get metric suggestions. |
||||
``` |
||||
|
||||
### testDatasource |
||||
|
||||
When a user clicks on the _Save & Test_ button when adding a new data source, the details are first saved to the database and then the `testDatasource` function that is defined in your data source plugin will be called. It is recommended that this function makes a query to the data source that will also test that the authentication details are correct. This is so the data source is correctly configured when the user tries to write a query in a new dashboard. |
||||
|
||||
### Query |
||||
|
||||
Request object passed to datasource.query function: |
||||
|
||||
```json |
||||
{ |
||||
"range": { "from": "2015-12-22T03:06:13.851Z", "to": "2015-12-22T06:48:24.137Z" }, |
||||
"interval": "5s", |
||||
"targets": [ |
||||
{ "refId": "B", "target": "upper_75" }, |
||||
{ "refId": "A", "target": "upper_90" } |
||||
], |
||||
"format": "json", |
||||
"maxDataPoints": 2495 // decided by the panel |
||||
} |
||||
``` |
||||
|
||||
There are two different kinds of results for data sources: |
||||
time series and table. Time series is the most common format and is supported by all data sources and panels. Table format is only supported by the InfluxDB data source and table panel. But we might see more of this in the future. |
||||
|
||||
Time series response from datasource.query. |
||||
An array of: |
||||
|
||||
```json |
||||
[ |
||||
{ |
||||
"target": "upper_75", |
||||
"datapoints": [ |
||||
[622, 1450754160000], |
||||
[365, 1450754220000] |
||||
] |
||||
}, |
||||
{ |
||||
"target": "upper_90", |
||||
"datapoints": [ |
||||
[861, 1450754160000], |
||||
[767, 1450754220000] |
||||
] |
||||
} |
||||
] |
||||
``` |
||||
|
||||
Table response from datasource.query. |
||||
An array of: |
||||
|
||||
```json |
||||
[ |
||||
{ |
||||
"columns": [ |
||||
{ |
||||
"text": "Time", |
||||
"type": "time", |
||||
"sort": true, |
||||
"desc": true |
||||
}, |
||||
{ |
||||
"text": "mean" |
||||
}, |
||||
{ |
||||
"text": "sum" |
||||
} |
||||
], |
||||
"rows": [ |
||||
[1457425380000, null, null], |
||||
[1457425370000, 1002.76215352, 1002.76215352] |
||||
], |
||||
"type": "table" |
||||
} |
||||
] |
||||
``` |
||||
|
||||
### Annotation Query |
||||
|
||||
Request object passed to datasource.annotationQuery function: |
||||
|
||||
```json |
||||
{ |
||||
"range": { "from": "2016-03-04T04:07:55.144Z", "to": "2016-03-04T07:07:55.144Z" }, |
||||
"rangeRaw": { "from": "now-3h", "to": "now" }, |
||||
"annotation": { |
||||
"datasource": "generic datasource", |
||||
"enable": true, |
||||
"name": "annotation name" |
||||
}, |
||||
"dashboard": DashboardModel |
||||
} |
||||
``` |
||||
|
||||
Expected result from datasource.annotationQuery: |
||||
|
||||
```json |
||||
[ |
||||
{ |
||||
"annotation": { |
||||
"name": "annotation name", //should match the annotation name in grafana |
||||
"enabled": true, |
||||
"datasource": "generic datasource" |
||||
}, |
||||
"title": "Cluster outage", |
||||
"time": 1457075272576, |
||||
"text": "Joe causes brain split", |
||||
"tags": ["joe", "cluster", "failure"] |
||||
} |
||||
] |
||||
``` |
||||
|
||||
## QueryCtrl |
||||
|
||||
A JavaScript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This class has to inherit from the `app/plugins/sdk.QueryCtrl` class. |
||||
|
||||
Requires a static template or `templateUrl` variable which will be rendered as the view for this controller. |
||||
|
||||
## ConfigCtrl |
||||
|
||||
A JavaScript class that will be instantiated and treated as an Angular controller when a user tries to edit or create a new data source of this type. |
||||
|
||||
Requires a static template or `templateUrl` variable which will be rendered as the view for this controller. |
||||
|
||||
## AnnotationsQueryCtrl |
||||
|
||||
A JavaScript class that will be instantiated and treated as an Angular controller when the user chooses this type of data source in the templating menu in the dashboard. |
||||
|
||||
Requires a static template or `templateUrl` variable which will be rendered as the view for this controller. The fields that are bound to this controller are then sent to the Database objects annotationQuery function. |
@ -1,28 +0,0 @@ |
||||
+++ |
||||
title = "Legacy panel plugins" |
||||
keywords = ["grafana", "plugins", "panel", "documentation"] |
||||
aliases = ["/docs/grafana/v8.3/plugins/developing/panels/"] |
||||
+++ |
||||
|
||||
# Legacy panel plugins |
||||
|
||||
Panels are the main building blocks of dashboards. |
||||
|
||||
## Panel development |
||||
|
||||
### Scrolling |
||||
|
||||
The grafana dashboard framework controls the panel height. To enable a scrollbar within the panel the PanelCtrl needs to set the scrollable static variable: |
||||
|
||||
```javascript |
||||
export class MyPanelCtrl extends PanelCtrl { |
||||
static scrollable = true; |
||||
... |
||||
``` |
||||
|
||||
In this case, make sure the template has a single `<div>...</div>` root. The plugin loader will modify that element adding a scrollbar. |
||||
|
||||
### Examples |
||||
|
||||
- [clock-panel](https://github.com/grafana/clock-panel) |
||||
- [singlestat-panel](https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/singlestat) |
@ -1,180 +0,0 @@ |
||||
+++ |
||||
title = "Legacy review guidelines" |
||||
aliases = ["/docs/grafana/v8.3/plugins/developing/plugin-review-guidelines/"] |
||||
+++ |
||||
|
||||
# Legacy review guidelines |
||||
|
||||
The Grafana team reviews all plugins that are published on Grafana.com. There are two areas we review, the metadata for the plugin and the plugin functionality. |
||||
|
||||
## Metadata |
||||
|
||||
The plugin metadata consists of a `plugin.json` file and the README.md file. The `plugin.json` file is used by Grafana to load the plugin, and the README.md file is shown in the plugins section of Grafana and the plugins section of https://grafana.com. |
||||
|
||||
### README.md |
||||
|
||||
The README.md file is shown on the plugins page in Grafana and the plugin page on Grafana.com. There are some differences between the GitHub markdown and the markdown allowed in Grafana/Grafana.com: |
||||
|
||||
- Cannot contain inline HTML. |
||||
- Any image links should be absolute links. For example: https://raw.githubusercontent.com/grafana/azure-monitor-datasource/master/dist/img/grafana_cloud_install.png |
||||
|
||||
The README should: |
||||
|
||||
- describe the purpose of the plugin. |
||||
- contain steps on how to get started. |
||||
|
||||
### Plugin.json |
||||
|
||||
The `plugin.json` file is the same concept as the `package.json` file for an npm package. When the Grafana server starts it will scan the plugin folders (all folders in the data/plugins subfolder) and load every folder that contains a `plugin.json` file unless the folder contains a subfolder named `dist`. In that case, the Grafana server will load the `dist` folder instead. |
||||
|
||||
A minimal `plugin.json` file: |
||||
|
||||
```json |
||||
{ |
||||
"type": "panel", |
||||
"name": "Clock", |
||||
"id": "yourorg-clock-panel", |
||||
|
||||
"info": { |
||||
"description": "Clock panel for grafana", |
||||
"author": { |
||||
"name": "Author Name", |
||||
"url": "http://yourwebsite.com" |
||||
}, |
||||
"keywords": ["clock", "panel"], |
||||
"version": "1.0.0", |
||||
"updated": "2018-03-24" |
||||
}, |
||||
|
||||
"dependencies": { |
||||
"grafanaVersion": "3.x.x", |
||||
"plugins": [] |
||||
} |
||||
} |
||||
``` |
||||
|
||||
- The convention for the plugin id is **[grafana.com username/org]-[plugin name]-[datasource|app|panel]** and it has to be unique. The org **cannot** be `grafana` unless it is a plugin created by the Grafana core team. |
||||
|
||||
Examples: |
||||
|
||||
- raintank-worldping-app |
||||
- ryantxu-ajax-panel |
||||
- alexanderzobnin-zabbix-app |
||||
- hawkular-datasource |
||||
|
||||
- The `type` field should be either `datasource` `app` or `panel`. |
||||
- The `version` field should be in the form: x.x.x e.g. `1.0.0` or `0.4.1`. |
||||
|
||||
The full file format for `plugin.json` file is in [plugin.json](http://docs.grafana.org/plugins/developing/plugin.json/). |
||||
|
||||
## Plugin Language |
||||
|
||||
JavaScript, TypeScript, ES6 (or any other language) are all fine as long as the contents of the `dist` subdirectory are transpiled to JavaScript (ES5). |
||||
|
||||
## File and Directory Structure Conventions |
||||
|
||||
Here is a typical directory structure for a plugin. |
||||
|
||||
```bash |
||||
johnnyb-awesome-datasource |
||||
|-- dist |
||||
|-- src |
||||
| |-- img |
||||
| | |-- logo.svg |
||||
| |-- partials |
||||
| | |-- annotations.editor.html |
||||
| | |-- config.html |
||||
| | |-- query.editor.html |
||||
| |-- datasource.js |
||||
| |-- module.js |
||||
| |-- plugin.json |
||||
| |-- query_ctrl.js |
||||
|-- Gruntfile.js |
||||
|-- LICENSE |
||||
|-- package.json |
||||
|-- README.md |
||||
``` |
||||
|
||||
Most JavaScript projects have a build step. The generated JavaScript should be placed in the `dist` directory and the source code in the `src` directory. We recommend that the plugin.json file be placed in the src directory and then copied over to the dist directory when building. The `README.md` can be placed in the root or in the dist directory. |
||||
|
||||
Directories: |
||||
|
||||
- `src/` contains plugin source files. |
||||
- `src/partials` contains html templates. |
||||
- `src/img` contains plugin logos and other images. |
||||
- `dist/` contains built content. |
||||
|
||||
## HTML and CSS |
||||
|
||||
For the HTML on editor tabs, we recommend using the inbuilt Grafana styles rather than defining your own. This makes plugins feel like a more natural part of Grafana. If done correctly, the html will also be responsive and adapt to smaller screens. The `gf-form` css classes should be used for labels and inputs. |
||||
|
||||
Below is a minimal example of an editor row with one form group and two fields, a dropdown and a text input: |
||||
|
||||
```html |
||||
<div class="editor-row"> |
||||
<div class="section gf-form-group"> |
||||
<h5 class="section-heading">My Plugin Options</h5> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label width-10">Label1</label> |
||||
<div class="gf-form-select-wrapper max-width-10"> |
||||
<select |
||||
class="input-small gf-form-input" |
||||
ng-model="ctrl.panel.mySelectProperty" |
||||
ng-options="t for t in ['option1', 'option2', 'option3']" |
||||
ng-change="ctrl.onSelectChange()" |
||||
></select> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label width-10">Label2</label> |
||||
<input |
||||
type="text" |
||||
class="input-small gf-form-input width-10" |
||||
ng-model="ctrl.panel.myProperty" |
||||
ng-change="ctrl.onFieldChange()" |
||||
placeholder="suggestion for user" |
||||
ng-model-onblur |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
``` |
||||
|
||||
Use the `width-x` and `max-width-x` classes to control the width of your labels and input fields. Try to get labels and input fields to line up neatly by having the same width for all the labels in a group and the same width for all inputs in a group if possible. |
||||
|
||||
## Data Sources |
||||
|
||||
For more information about data sources, refer to the [basic guide for data sources](http://docs.grafana.org/plugins/developing/datasources/). |
||||
|
||||
### Configuration Page Guidelines |
||||
|
||||
- It should be as easy as possible for a user to configure a URL. If the data source is using the `datasource-http-settings` component, it should use the `suggest-url` attribute to suggest the default URL or a URL that is similar to what it should be (especially important if the URL refers to a REST endpoint that is not common knowledge for most users e.g. `https://yourserver:4000/api/custom-endpoint`). |
||||
|
||||
```html |
||||
<datasource-http-settings current="ctrl.current" suggest-url="http://localhost:8080"> </datasource-http-settings> |
||||
``` |
||||
|
||||
- The `testDatasource` function should make a query to the data source that will also test that the authentication details are correct. This is so the data source is correctly configured when the user tries to write a query in a new dashboard. |
||||
|
||||
#### Password Security |
||||
|
||||
If possible, any passwords or secrets should be saved in the `secureJsonData` blob. To encrypt sensitive data, the Grafana server's proxy feature must be used. The Grafana server has support for token authentication (OAuth) and HTTP Header authentication. If the calls have to be sent directly from the browser to a third-party API, this will not be possible and sensitive data will not be encrypted. |
||||
|
||||
Read more here about how [authentication for data sources]({{< relref "../add-authentication-for-data-source-plugins.md" >}}) works. |
||||
|
||||
If using the proxy feature, the Configuration page should use the `secureJsonData` blob like this: |
||||
|
||||
- good: `<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input>` |
||||
- bad: `<input type="password" class="gf-form-input" ng-model='ctrl.current.password' placeholder="password"></input>` |
||||
|
||||
### Query Editor |
||||
|
||||
Each query editor is unique and can have a unique style. It should be adapted to what the users of the data source are used to. |
||||
|
||||
- Should use the Grafana CSS `gf-form` classes. |
||||
- Should be neat and tidy. Labels and fields in columns should be aligned and should be the same width if possible. |
||||
- The data source should be able to handle when a user toggles a query (by clicking on the eye icon) and not execute the query. This is done by checking the `hide` property - an [example](https://github.com/grafana/grafana/blob/e75840737e81f70b6d169df21eca86a624d4bdc4/public/app/plugins/datasource/postgres/datasource.ts#L73). |
||||
- Should not execute queries if fields in the Query Editor are empty and the query will throw an exception (defensive programming). |
||||
- Should handle errors. There are two main ways to do this: |
||||
- use the notification system in Grafana to show a toaster pop-up with the error message. For an example of a pop-up with the error message, refer to [code in triggers_panel_ctrl](https://github.com/alexanderzobnin/grafana-zabbix/blob/fdbbba2fb03f5f2a4b3b0715415e09d5a4cf6cde/src/panel-triggers/triggers_panel_ctrl.js#L467-L471). |
||||
- provide an error notification in the query editor like the MySQL/Postgres data sources do. For an example of error notification in the query editor, refer to [code in query_ctrl](https://github.com/grafana/azure-monitor-datasource/blob/b184d077f082a69f962120ef0d1f8296a0d46f03/src/query_ctrl.ts#L36-L51) and in the [html](https://github.com/grafana/azure-monitor-datasource/blob/b184d077f082a69f962120ef0d1f8296a0d46f03/src/partials/query.editor.html#L190-L193). |
@ -1,75 +0,0 @@ |
||||
+++ |
||||
title = "Legacy snapshot mode" |
||||
aliases = ["/docs/grafana/v8.3/plugins/developing/snapshot-mode/"] |
||||
+++ |
||||
|
||||
# Legacy snapshot mode |
||||
|
||||
{{< figure class="float-right" src="/static/img/docs/Grafana-snapshot-example.png" caption="A dashboard using snapshot data and not live data." >}} |
||||
|
||||
Grafana has this great feature where you can [save a snapshot of your dashboard]({{< relref "../../../dashboards/json-model.md" >}}). Instead of sending a screenshot of a dashboard to someone, you can send them a working, interactive Grafana dashboard with the snapshot data embedded inside it. The snapshot can be saved on your Grafana server and is available to all your co-workers. Raintank also hosts a [snapshot server](https://snapshots.raintank.io) if you want to send the snapshot to someone who does not have access to your Grafana server. |
||||
|
||||
{{< figure class="float-right" src="/static/img/docs/animated_gifs/snapshots.gif" caption="Selecting a snapshot" >}} |
||||
|
||||
This all works because Grafana saves a snapshot of the current data in the dashboard json instead of fetching the data from a data source. However, if you are building a custom panel plugin then this will not work straight out of the box. You will need to make some small (and easy!) changes first. |
||||
|
||||
## Enabling support for loading snapshot data |
||||
|
||||
Grafana automatically saves data from data sources in the dashboard json when the snapshot is created so we do not have to write any code for that. Enabling snapshot support for reading time series data is very simple. First in the constructor, we need to add an event handler for `data-snapshot-load`. This event is triggered by Grafana when the snapshot data is loaded from the dashboard json. |
||||
|
||||
```javascript |
||||
constructor($scope, $injector, contextSrv) { |
||||
super($scope, $injector); |
||||
... |
||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); |
||||
this.events.on('data-received', this.onDataReceived.bind(this)); |
||||
this.events.on('panel-teardown', this.onPanelTeardown.bind(this)); |
||||
this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); |
||||
``` |
||||
|
||||
Then we need to create a simple event handler that just forwards the data on to our regular `data-received` handler: |
||||
|
||||
```javascript |
||||
onDataSnapshotLoad(snapshotData) { |
||||
this.onDataReceived(snapshotData); |
||||
} |
||||
``` |
||||
|
||||
This will cover most use cases for snapshot support. Sometimes you will want to save data that is not time series data from a Grafana data source and then you have to do a bit more work to get snapshot support. |
||||
|
||||
## Saving custom data for snapshots |
||||
|
||||
Data that is not time series data from a Grafana data source is not saved automatically by Grafana. Saving custom data for snapshot mode has to be done manually. |
||||
|
||||
{{< figure class="float-right" src="/static/img/docs/Grafana-save-snapshot.png" caption="Save snapshot" >}} |
||||
|
||||
Grafana gives us a chance to save data to the dashboard json when it is creating a snapshot. In the 'data-received' event handler, you can check the snapshot flag on the dashboard object. If this is true, then Grafana is creating a snapshot and you can manually save custom data to the panel json. In the example, a new field called snapshotLocationData in the panel json is initialized with a snapshot of the custom data. |
||||
|
||||
```javascript |
||||
onDataReceived(dataList) { |
||||
if (!dataList) return; |
||||
|
||||
if (this.dashboard.snapshot && this.locations) { |
||||
this.panel.snapshotLocationData = this.locations; |
||||
} |
||||
``` |
||||
|
||||
Now the location data is saved in the dashboard json but we will have to load it manually as well. |
||||
|
||||
## Loading custom data for snapshots |
||||
|
||||
The example below shows a function that loads the custom data. The data source for the custom data (an external API in this case) is not available in snapshot mode so a guard check is made to see if there is any snapshot data available first. If there is, then the snapshot data is used instead of trying to load the data from the external API. |
||||
|
||||
```javascript |
||||
loadLocationDataFromFile(reload) { |
||||
if (this.map && !reload) return; |
||||
|
||||
if (this.panel.snapshotLocationData) { |
||||
this.locations = this.panel.snapshotLocationData; |
||||
return; |
||||
} |
||||
``` |
||||
|
||||
It is really easy to forget to add this support but it enables a great feature and can be used to demo your panel. |
||||
|
||||
If there is a panel plugin that you would like to be installed on the Raintank Snapshot server then please contact us via [Slack](https://slack.grafana.com) or [GitHub](https://github.com/grafana/grafana). |
@ -1,192 +0,0 @@ |
||||
+++ |
||||
title = "Legacy code style guide" |
||||
aliases = ["/docs/grafana/v8.3/plugins/developing/code-styleguide/"] |
||||
+++ |
||||
|
||||
# Legacy code style guide |
||||
|
||||
This guide has two parts. The first part describes the metadata and the second part is a styleguide for HTML/CSS and JavaScript in Grafana plugins and applies if you are using ES6 in your plugin. If using TypeScript then the [Angular TypeScript styleguide](https://angular.io/styleguide) is recommended. |
||||
|
||||
## Metadata |
||||
|
||||
The plugin metadata consists of a plugin.json file and the README.md file. These two files are used by Grafana and Grafana.com. |
||||
|
||||
### Plugin.json (mandatory) |
||||
|
||||
The plugin.json file is the same concept as the package.json file for an npm package. When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless the folder contains a subfolder named `dist`. In that case grafana will mount the `dist` folder instead. |
||||
|
||||
The most important fields are the first three, especially the id. The convention for the plugin id is **[github username/org]-[plugin name]-[datasource|app|panel]** and it has to be unique. |
||||
|
||||
Examples: |
||||
|
||||
```bash |
||||
raintank-worldping-app |
||||
grafana-simple-json-datasource |
||||
grafana-piechart-panel |
||||
mtanda-histogram-panel |
||||
``` |
||||
|
||||
For more information about the file format for `plugin.json` file, refer to [metadata]({{< relref "../metadata.md" >}}). |
||||
|
||||
Minimal plugin.json: |
||||
|
||||
```javascript |
||||
{ |
||||
"type": "panel", |
||||
"name": "Clock", |
||||
"id": "yourorg-clock-panel", |
||||
|
||||
"info": { |
||||
"description": "Clock panel for grafana", |
||||
"author": { |
||||
"name": "Grafana Labs", |
||||
"url": "https://grafana.com" |
||||
}, |
||||
"keywords": ["clock", "panel"], |
||||
"version": "1.0.0", |
||||
"updated": "2015-03-24" |
||||
}, |
||||
|
||||
"dependencies": { |
||||
"grafanaVersion": "3.x.x", |
||||
"plugins": [ ] |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### README.md |
||||
|
||||
The README.md file is rendered both in the grafana.com plugins page, and within the Grafana application. The only difference from how GitHub renders markdown is that html is not allowed. |
||||
|
||||
## File and Directory Structure Conventions |
||||
|
||||
Here is a typical directory structure for a plugin. |
||||
|
||||
```bash |
||||
johnnyb-awesome-datasource |
||||
|-- dist |
||||
|-- spec |
||||
| |-- datasource_spec.js |
||||
| |-- query_ctrl_spec.js |
||||
| |-- test-main.js |
||||
|-- src |
||||
| |-- img |
||||
| | |-- logo.svg |
||||
| |-- partials |
||||
| | |-- annotations.editor.html |
||||
| | |-- config.html |
||||
| | |-- query.editor.html |
||||
| |-- datasource.js |
||||
| |-- module.js |
||||
| |-- plugin.json |
||||
| |-- query_ctrl.js |
||||
|-- Gruntfile.js |
||||
|-- LICENSE |
||||
|-- package.json |
||||
|-- README.md |
||||
``` |
||||
|
||||
Most JavaScript projects have a build step and most Grafana plugins are built using Babel and ES6. The generated JavaScript should be placed in the `dist` directory and the source code in the `src` directory. We recommend that the plugin.json file be placed in the src directory and then copied over to the dist directory when building. The `README.md` can be placed in the root or in the dist directory. |
||||
|
||||
Directories: |
||||
|
||||
- `src/` contains plugin source files. |
||||
- `src/partials` contains html templates. |
||||
- `src/img` contains plugin logos and other images. |
||||
- `spec/` contains tests (optional). |
||||
- `dist/` contains built content. |
||||
|
||||
## HTML and CSS |
||||
|
||||
For the HTML on editor tabs, we recommend using the inbuilt Grafana styles rather than defining your own. This makes plugins feel like a more natural part of Grafana. If done correctly, the html will also be responsive and adapt to smaller screens. The `gf-form` css classes should be used for labels and inputs. |
||||
|
||||
Below is a minimal example of an editor row with one form group and two fields, a dropdown and a text input: |
||||
|
||||
```html |
||||
<div class="editor-row"> |
||||
<div class="section gf-form-group"> |
||||
<h5 class="section-heading">My Plugin Options</h5> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label width-10">Label1</label> |
||||
<div class="gf-form-select-wrapper max-width-10"> |
||||
<select |
||||
class="input-small gf-form-input" |
||||
ng-model="ctrl.panel.mySelectProperty" |
||||
ng-options="t for t in ['option1', 'option2', 'option3']" |
||||
ng-change="ctrl.onSelectChange()" |
||||
></select> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label width-10">Label2</label> |
||||
<input |
||||
type="text" |
||||
class="input-small gf-form-input width-10" |
||||
ng-model="ctrl.panel.myProperty" |
||||
ng-change="ctrl.onFieldChange()" |
||||
placeholder="suggestion for user" |
||||
ng-model-onblur |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
``` |
||||
|
||||
Use the `width-x` and `max-width-x` classes to control the width of your labels and input fields. Try to get labels and input fields to line up neatly by having the same width for all the labels in a group and the same width for all inputs in a group if possible. |
||||
|
||||
## Build Scripts |
||||
|
||||
Our recommendation is to use whatever you usually use - Grunt, Gulp or npm scripts. Most plugins seems to use Grunt so that is probably the easiest to get started with if you do not have a preferred build system. The only requirement is that it supports systemjs which is required by Grafana to load plugins. |
||||
|
||||
## Linting |
||||
|
||||
We recommend that you use a linter for your JavaScript. For ES6, the standard linter is [eslint](http://eslint.org/). Rules for linting are described in an .eslintrc that is placed in the root directory. For an example of linting rules in a plugin, refer to [.eslintrc](https://github.com/grafana/worldmap-panel/blob/master/.eslintrc). |
||||
|
||||
### ES6 features |
||||
|
||||
1. Use `const` if a variable is not going to be reassigned. |
||||
1. Prefer to use `let` instead `var` ([Exploring ES6](http://exploringjs.com/es6/ch_core-features.html#_from-var-to-letconst)) |
||||
1. Use arrow functions, which don’t shadow `this` ([Exploring ES6](http://exploringjs.com/es6/ch_core-features.html#_from-function-expressions-to-arrow-functions)): |
||||
|
||||
```js |
||||
testDatasource() { |
||||
return this.getServerStatus() |
||||
.then(status => { |
||||
return this.doSomething(status); |
||||
}) |
||||
} |
||||
``` |
||||
|
||||
better than |
||||
|
||||
```js |
||||
testDatasource() { |
||||
var self = this; |
||||
return this.getServerStatus() |
||||
.then(function(status) { |
||||
return self.doSomething(status); |
||||
}) |
||||
} |
||||
``` |
||||
|
||||
1. Use native _Promise_ object: |
||||
|
||||
```js |
||||
metricFindQuery(query) { |
||||
if (!query) { |
||||
return Promise.resolve([]); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
better than |
||||
|
||||
```js |
||||
metricFindQuery(query) { |
||||
if (!query) { |
||||
return this.$q.when([]); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
1. If using Lodash, then be consistent and prefer that to the native ES6 array functions. |
@ -1,253 +0,0 @@ |
||||
+++ |
||||
# ------------------------------------------------------------------------- |
||||
# Do not edit this file. It is automatically generated by json-schema-docs. |
||||
# ------------------------------------------------------------------------- |
||||
title = "plugin.json" |
||||
keywords = ["grafana", "plugins", "documentation"] |
||||
aliases = ["/docs/grafana/v8.3/plugins/developing/plugin.json/"] |
||||
+++ |
||||
|
||||
# plugin.json |
||||
|
||||
The plugin.json file is required for all plugins. When Grafana starts, it scans the plugin folders and mounts every folder that contains a plugin.json file unless the folder contains a subfolder named dist. In that case, Grafana mounts the dist folder instead. |
||||
|
||||
## Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------------------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
||||
| `dependencies` | [object](#dependencies) | **Yes** | Dependencies needed by the plugin. | |
||||
| `id` | string | **Yes** | Unique name of the plugin. If the plugin is published on grafana.com, then the plugin id has to follow the naming conventions. | |
||||
| `info` | [object](#info) | **Yes** | Metadata for the plugin. Some fields are used on the plugins page in Grafana and others on grafana.com if the plugin is published. | |
||||
| `name` | string | **Yes** | Human-readable name of the plugin that is shown to the user in the UI. | |
||||
| `type` | string | **Yes** | Plugin type. Possible values are: `app`, `datasource`, `panel`. | |
||||
| `$schema` | string | No | Schema definition for the plugin.json file. | |
||||
| `alerting` | boolean | No | For data source plugins, if the plugin supports alerting. | |
||||
| `annotations` | boolean | No | For data source plugins, if the plugin supports annotation queries. | |
||||
| `autoEnabled` | boolean | No | Set to true for app plugins that should be enabled by default in all orgs | |
||||
| `backend` | boolean | No | If the plugin has a backend component. | |
||||
| `category` | string | No | Plugin category used on the Add data source page. Possible values are: `tsdb`, `logging`, `cloud`, `tracing`, `sql`, `enterprise`, `other`. | |
||||
| `enterpriseFeatures` | [object](#enterprisefeatures) | No | Grafana Enerprise specific features. | |
||||
| `executable` | string | No | The first part of the file name of the backend component executable. There can be multiple executables built for different operating system and architecture. Grafana will check for executables named `<executable>_<$GOOS>_<lower case $GOARCH><.exe for Windows>`, e.g. `plugin_linux_amd64`. Combination of $GOOS and $GOARCH can be found here: https://golang.org/doc/install/source#environment. | |
||||
| `hiddenQueries` | boolean | No | For data source plugins, include hidden queries in the data request. | |
||||
| `includes` | [object](#includes)[] | No | Resources to include in plugin. | |
||||
| `logs` | boolean | No | For data source plugins, if the plugin supports logs. | |
||||
| `metrics` | boolean | No | For data source plugins, if the plugin supports metric queries. Used in Explore. | |
||||
| `preload` | boolean | No | Initialize plugin on startup. By default, the plugin initializes on first use. | |
||||
| `queryOptions` | [object](#queryoptions) | No | For data source plugins. There is a query options section in the plugin's query editor and these options can be turned on if needed. | |
||||
| `routes` | [object](#routes)[] | No | For data source plugins. Proxy routes used for plugin authentication and adding headers to HTTP requests made by the plugin. For more information, refer to [Authentication for data source plugins](https://grafana.com/docs/grafana/v8.3/developers/plugins/authentication/). | |
||||
| `skipDataQuery` | boolean | No | For panel plugins. Hides the query editor. | |
||||
| `state` | string | No | Marks a plugin as a pre-release. Possible values are: `alpha`, `beta`. | |
||||
| `streaming` | boolean | No | For data source plugins, if the plugin supports streaming. | |
||||
| `tables` | boolean | No | This is an undocumented feature. | |
||||
| `tracing` | boolean | No | For data source plugins, if the plugin supports tracing. | |
||||
|
||||
## dependencies |
||||
|
||||
Dependencies needed by the plugin. |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| ------------------- | -------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- | |
||||
| `grafanaDependency` | string | **Yes** | Required Grafana version for this plugin. Validated using https://github.com/npm/node-semver. | |
||||
| `grafanaVersion` | string | No | (Deprecated) Required Grafana version for this plugin, e.g. `6.x.x 7.x.x` to denote plugin requires Grafana v6.x.x or v7.x.x. | |
||||
| `plugins` | [object](#plugins)[] | No | An array of required plugins on which this plugin depends. | |
||||
|
||||
### plugins |
||||
|
||||
Plugin dependency. Used to display information about plugin dependencies in the Grafana UI. |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| --------- | ------ | -------- | -------------------------------------------------- | |
||||
| `id` | string | **Yes** | | |
||||
| `name` | string | **Yes** | | |
||||
| `type` | string | **Yes** | Possible values are: `app`, `datasource`, `panel`. | |
||||
| `version` | string | **Yes** | | |
||||
|
||||
## enterpriseFeatures |
||||
|
||||
Grafana Enerprise specific features. |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| ------------------------- | ------- | -------- | ------------------------------------------------------------------- | |
||||
| `healthDiagnosticsErrors` | boolean | No | Enable/Disable health diagnostics errors. Requires Grafana >=7.5.5. | |
||||
|
||||
## includes |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| ------------ | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
||||
| `addToNav` | boolean | No | Add the include to the side menu. | |
||||
| `component` | string | No | (Legacy) The Angular component to use for a page. | |
||||
| `defaultNav` | boolean | No | Page or dashboard when user clicks the icon in the side menu. | |
||||
| `icon` | string | No | Icon to use in the side menu. For information on available icon, refer to [Icons Overview](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview). | |
||||
| `name` | string | No | | |
||||
| `path` | string | No | Used for app plugins. | |
||||
| `role` | string | No | Possible values are: `Admin`, `Editor`, `Viewer`. | |
||||
| `type` | string | No | Possible values are: `dashboard`, `page`, `panel`, `datasource`. | |
||||
| `uid` | string | No | Unique identifier of the included resource | |
||||
|
||||
## info |
||||
|
||||
Metadata for the plugin. Some fields are used on the plugins page in Grafana and others on grafana.com if the plugin is published. |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| ------------- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- | |
||||
| `keywords` | string[] | **Yes** | Array of plugin keywords. Used for search on grafana.com. | |
||||
| `logos` | [object](#logos) | **Yes** | SVG images that are used as plugin icons. | |
||||
| `updated` | string | **Yes** | Date when this plugin was built. | |
||||
| `version` | string | **Yes** | Project version of this commit, e.g. `6.7.x`. | |
||||
| `author` | [object](#author) | No | Information about the plugin author. | |
||||
| `build` | [object](#build) | No | Build information | |
||||
| `description` | string | No | Description of plugin. Used on the plugins page in Grafana and for search on grafana.com. | |
||||
| `links` | [object](#links)[] | No | An array of link objects to be displayed on this plugin's project page in the form `{name: 'foo', url: 'http://example.com'}` | |
||||
| `screenshots` | [object](#screenshots)[] | No | An array of screenshot objects in the form `{name: 'bar', path: 'img/screenshot.png'}` | |
||||
|
||||
### author |
||||
|
||||
Information about the plugin author. |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ------ | -------- | ------------------------- | |
||||
| `email` | string | No | Author's name. | |
||||
| `name` | string | No | Author's name. | |
||||
| `url` | string | No | Link to author's website. | |
||||
|
||||
### build |
||||
|
||||
Build information |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ------ | -------- | ---------------------------------------------------- | |
||||
| `branch` | string | No | Git branch the plugin was built from. | |
||||
| `hash` | string | No | Git hash of the commit the plugin was built from | |
||||
| `number` | number | No | | |
||||
| `pr` | number | No | GitHub pull request the plugin was built from | |
||||
| `repo` | string | No | | |
||||
| `time` | number | No | Time when the plugin was built, as a Unix timestamp. | |
||||
|
||||
### links |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ------ | -------- | ----------- | |
||||
| `name` | string | No | | |
||||
| `url` | string | No | | |
||||
|
||||
### logos |
||||
|
||||
SVG images that are used as plugin icons. |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------- | |
||||
| `large` | string | **Yes** | Link to the "large" version of the plugin logo, which must be an SVG image. "Large" and "small" logos can be the same image. | |
||||
| `small` | string | **Yes** | Link to the "small" version of the plugin logo, which must be an SVG image. "Large" and "small" logos can be the same image. | |
||||
|
||||
### screenshots |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ------ | -------- | ----------- | |
||||
| `name` | string | No | | |
||||
| `path` | string | No | | |
||||
|
||||
## queryOptions |
||||
|
||||
For data source plugins. There is a query options section in the plugin's query editor and these options can be turned on if needed. |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| --------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | |
||||
| `cacheTimeout` | boolean | No | For data source plugins. If the `cache timeout` option should be shown in the query options section in the query editor. | |
||||
| `maxDataPoints` | boolean | No | For data source plugins. If the `max data points` option should be shown in the query options section in the query editor. | |
||||
| `minInterval` | boolean | No | For data source plugins. If the `min interval` option should be shown in the query options section in the query editor. | |
||||
|
||||
## routes |
||||
|
||||
For data source plugins. Proxy routes used for plugin authentication and adding headers to HTTP requests made by the plugin. For more information, refer to [Authentication for data source plugins](https://grafana.com/docs/grafana/v8.3/developers/plugins/authentication/). |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------------- | ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- | |
||||
| `body` | [object](#body) | No | For data source plugins. Route headers set the body content and length to the proxied request. | |
||||
| `headers` | array | No | For data source plugins. Route headers adds HTTP headers to the proxied request. | |
||||
| `jwtTokenAuth` | [object](#jwttokenauth) | No | For data source plugins. Token authentication section used with an JWT OAuth API. | |
||||
| `method` | string | No | For data source plugins. Route method matches the HTTP verb like GET or POST. Multiple methods can be provided as a comma-separated list. | |
||||
| `path` | string | No | For data source plugins. The route path that is replaced by the route URL field when proxying the call. | |
||||
| `reqRole` | string | No | | |
||||
| `reqSignedIn` | boolean | No | | |
||||
| `tokenAuth` | [object](#tokenauth) | No | For data source plugins. Token authentication section used with an OAuth API. | |
||||
| `url` | string | No | For data source plugins. Route URL is where the request is proxied to. | |
||||
|
||||
### body |
||||
|
||||
For data source plugins. Route headers set the body content and length to the proxied request. |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ---- | -------- | ----------- | |
||||
|
||||
### jwtTokenAuth |
||||
|
||||
For data source plugins. Token authentication section used with an JWT OAuth API. |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ----------------- | -------- | --------------------------------------------------------------------- | |
||||
| `params` | [object](#params) | No | Parameters for the JWT token authentication request. | |
||||
| `scopes` | string[] | No | The list of scopes that your application should be granted access to. | |
||||
| `url` | string | No | URL to fetch the JWT token. | |
||||
|
||||
#### params |
||||
|
||||
Parameters for the JWT token authentication request. |
||||
|
||||
##### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------------- | ------ | -------- | ----------- | |
||||
| `client_email` | string | No | | |
||||
| `private_key` | string | No | | |
||||
| `token_uri` | string | No | | |
||||
|
||||
### tokenAuth |
||||
|
||||
For data source plugins. Token authentication section used with an OAuth API. |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ----------------- | -------- | --------------------------------------------------------------------- | |
||||
| `params` | [object](#params) | No | Parameters for the token authentication request. | |
||||
| `scopes` | string[] | No | The list of scopes that your application should be granted access to. | |
||||
| `url` | string | No | URL to fetch the authentication token. | |
||||
|
||||
#### params |
||||
|
||||
Parameters for the token authentication request. |
||||
|
||||
##### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| --------------- | ------ | -------- | ----------------------------------------------------------------------------------------- | |
||||
| `client_id` | string | No | OAuth client ID | |
||||
| `client_secret` | string | No | OAuth client secret. Usually populated by decrypting the secret from the SecureJson blob. | |
||||
| `grant_type` | string | No | OAuth grant type | |
||||
| `resource` | string | No | OAuth resource | |
@ -1,10 +0,0 @@ |
||||
+++ |
||||
# ------------------------------------------------------------------------- |
||||
# Do not edit this file. It is automatically generated by json-schema-docs. |
||||
# ------------------------------------------------------------------------- |
||||
title = "plugin.json" |
||||
keywords = ["grafana", "plugins", "documentation"] |
||||
aliases = ["/docs/grafana/v8.3/plugins/developing/plugin.json/"] |
||||
+++ |
||||
|
||||
{{ .Markdown 1 }} |
@ -1,508 +0,0 @@ |
||||
+++ |
||||
title = "Plugin migration guide" |
||||
+++ |
||||
|
||||
# Plugin migration guide |
||||
|
||||
## Introduction |
||||
|
||||
This guide helps you identify the steps you need to take based on the Grafana version your plugin supports and explains how to migrate the plugin to the 8.2.x or a later version. |
||||
|
||||
> **Note:** If you've successfully migrated your plugin using this guide, then share your experiences with us! If you find missing information, then we encourage you to [submit an issue on GitHub](https://github.com/grafana/grafana/issues/new?title=Docs%20feedback:%20/developers/plugins/migration-guide.md) so that we can improve this guide! |
||||
|
||||
## Table of contents |
||||
|
||||
- [From version 7.x.x to 8.0.0](#from-version-7xx-to-800) |
||||
- [Backend plugin v1 support has been dropped](#backend-plugin-v1-support-has-been-dropped) |
||||
- [1. Add dependency on grafana-plugin-sdk-go](#1-add-dependency-on-grafana-plugin-sdk-go) |
||||
- [2. Update the way you bootstrap your plugin](#2-update-the-way-you-bootstrap-your-plugin) |
||||
- [3. Update the plugin package](#3-update-the-plugin-package) |
||||
- [Sign and load backend plugins](#sign-and-load-backend-plugins) |
||||
- [Update react-hook-form from v6 to v7](#update-react-hook-form-from-v6-to-v7) |
||||
- [Update the plugin.json](#update-the-pluginjson) |
||||
- [Update imports to match emotion 11](#update-imports-to-match-emotion-11) |
||||
- [8.0 Deprecations](#80-deprecations) |
||||
- [Grafana theme v1](#grafana-theme-v1) |
||||
- [From version 6.2.x to 7.4.0](#from-version-62x-to-740) |
||||
- [Legend components](#legend-components) |
||||
- [From version 6.x.x to 7.0.0](#from-version-6xx-to-700) |
||||
- [What's new in Grafana 7.0?](#whats-new-in-grafana-70) |
||||
- [Migrate a plugin from Angular to React](#migrate-a-plugin-from-angular-to-react) |
||||
- [Migrate a panel plugin](#migrate-a-panel-plugin) |
||||
- [Migrate a data source plugin](#migrate-a-data-source-plugin) |
||||
- [Migrate to data frames](#migrate-to-data-frames) |
||||
- [Troubleshoot plugin migration](#troubleshoot-plugin-migration) |
||||
|
||||
## From version 7.x.x to 8.x.x |
||||
|
||||
This section explains how to migrate Grafana v7.x.x plugins to the updated plugin system available in Grafana v8.x.x. Depending on your plugin, you need to perform one or more of the following steps. We have documented the breaking changes in Grafana v8.x.x and the steps you need to take to upgrade your plugin. |
||||
|
||||
### Backend plugin v1 support has been dropped |
||||
|
||||
Use the new [plugin sdk](https://github.com/grafana/grafana-plugin-sdk-go) to run your backend plugin running in Grafana 8. |
||||
|
||||
#### 1. Add dependency on grafana-plugin-sdk-go |
||||
|
||||
Add a dependency on the `https://github.com/grafana/grafana-plugin-sdk-go`. We recommend using [go modules](https://go.dev/blog/using-go-modules) to manage your dependencies. |
||||
|
||||
#### 2. Update the way you bootstrap your plugin |
||||
|
||||
Update your `main` package to bootstrap via the new plugin sdk. |
||||
|
||||
```go |
||||
// before |
||||
package main |
||||
|
||||
import ( |
||||
"github.com/grafana/grafana_plugin_model/go/datasource" |
||||
hclog "github.com/hashicorp/go-hclog" |
||||
plugin "github.com/hashicorp/go-plugin" |
||||
|
||||
"github.com/myorgid/datasource/pkg/plugin" |
||||
) |
||||
|
||||
func main() { |
||||
pluginLogger.Debug("Running GRPC server") |
||||
|
||||
ds, err := NewSampleDatasource(pluginLogger); |
||||
if err != nil { |
||||
pluginLogger.Error("Unable to create plugin"); |
||||
} |
||||
|
||||
plugin.Serve(&plugin.ServeConfig{ |
||||
HandshakeConfig: plugin.HandshakeConfig{ |
||||
ProtocolVersion: 1, |
||||
MagicCookieKey: "grafana_plugin_type", |
||||
MagicCookieValue: "datasource", |
||||
}, |
||||
Plugins: map[string]plugin.Plugin{ |
||||
"myorgid-datasource": &datasource.DatasourcePluginImpl{Plugin: ds}, |
||||
}, |
||||
GRPCServer: plugin.DefaultGRPCServer, |
||||
}) |
||||
} |
||||
|
||||
// after |
||||
package main |
||||
|
||||
import ( |
||||
"os" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log" |
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource" |
||||
|
||||
"github.com/myorgid/datasource/pkg/plugin" |
||||
) |
||||
|
||||
func main() { |
||||
log.DefaultLogger.Debug("Running GRPC server") |
||||
|
||||
if err := datasource.Manage("myorgid-datasource", NewSampleDatasource, datasource.ManageOpts{}); err != nil { |
||||
log.DefaultLogger.Error(err.Error()) |
||||
os.Exit(1) |
||||
} |
||||
} |
||||
``` |
||||
|
||||
#### 3. Update the plugin package |
||||
|
||||
Update your `plugin` package to use the new plugin sdk. |
||||
|
||||
```go |
||||
// before |
||||
package plugin |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana_plugin_model/go/datasource" |
||||
"github.com/hashicorp/go-hclog" |
||||
) |
||||
|
||||
func NewSampleDatasource(pluginLogger hclog.Logger) (*SampleDatasource, error) { |
||||
return &SampleDatasource{ |
||||
logger: pluginLogger, |
||||
}, nil |
||||
} |
||||
|
||||
type SampleDatasource struct{ |
||||
logger hclog.Logger |
||||
} |
||||
|
||||
func (d *SampleDatasource) Query(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) { |
||||
d.logger.Info("QueryData called", "request", req) |
||||
// logic for querying your datasource. |
||||
} |
||||
|
||||
// after |
||||
package plugin |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" |
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
) |
||||
|
||||
func NewSampleDatasource(_ backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { |
||||
return &SampleDatasource{}, nil |
||||
} |
||||
|
||||
type SampleDatasource struct{} |
||||
|
||||
|
||||
func (d *SampleDatasource) Dispose() { |
||||
// Clean up datasource instance resources. |
||||
} |
||||
|
||||
func (d *SampleDatasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
log.DefaultLogger.Info("QueryData called", "request", req) |
||||
// logic for querying your datasource. |
||||
} |
||||
|
||||
func (d *SampleDatasource) CheckHealth(_ context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { |
||||
log.DefaultLogger.Info("CheckHealth called", "request", req) |
||||
// The main use case for these health checks is the test button on the |
||||
// datasource configuration page which allows users to verify that |
||||
// a datasource is working as expected. |
||||
} |
||||
``` |
||||
|
||||
### Sign and load backend plugins |
||||
|
||||
We strongly recommend that you not allow unsigned plugins in your Grafana installation. By allowing unsigned plugins, we cannot guarantee the authenticity of the plugin, which could compromise the security of your Grafana installation. |
||||
|
||||
To sign your plugin, see [Sign a plugin](https://grafana.com/docs/grafana/v8.3/developers/plugins/sign-a-plugin/#sign-a-plugin). |
||||
|
||||
You can still run and develop an unsigned plugin by running your Grafana instance in [development mode](https://grafana.com/docs/grafana/v8.3/administration/configuration/#app_mode). Alternatively, you can use the [allow_loading_unsigned_plugins configuration setting.]({{< relref "../../administration/#allow_loading_unsigned_plugins" >}}) |
||||
|
||||
### Update react-hook-form from v6 to v7 |
||||
|
||||
We have upgraded react-hook-form from version 6 to version 7. To make your forms compatible with version 7, refer to the [react-hook-form-migration-guide](https://react-hook-form.com/migrate-v6-to-v7/). |
||||
|
||||
### Update the plugin.json |
||||
|
||||
The property that defines which Grafana version your plugin supports has been renamed and now it is a range instead of a specific version. |
||||
|
||||
```json |
||||
// before |
||||
{ |
||||
"dependencies": { |
||||
"grafanaVersion": "7.5.x", |
||||
"plugins": [] |
||||
} |
||||
} |
||||
|
||||
// after |
||||
{ |
||||
"dependencies": { |
||||
"grafanaDependency": ">=8.0.0", |
||||
"plugins": [] |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Update imports to match emotion 11 |
||||
|
||||
Grafana uses Emotion library to manage frontend styling. We have updated the Emotion package and this can affect your frontend plugin if you have custom styling. You only need to update the import statements to get it working in Grafana 8. |
||||
|
||||
```ts |
||||
// before |
||||
import { cx, css } from 'emotion'; |
||||
|
||||
// after |
||||
import { cx, css } from '@emotion/css'; |
||||
``` |
||||
|
||||
### 8.0 deprecations |
||||
|
||||
#### Grafana theme v1 |
||||
|
||||
In Grafana 8 we have introduced a new improved version of our theming system. The previous version of the theming system is still available but is deprecated and will be removed in the next major version of Grafana. |
||||
|
||||
You can find more detailed information on how to apply the v2 theme [here](https://github.com/grafana/grafana/blob/main/contribute/style-guides/themes.md#theming-grafana). |
||||
|
||||
**How to style a functional component** |
||||
|
||||
The `useStyles` hook is the preferred way to access the theme when styling. It provides basic memoization and access to the theme object. |
||||
|
||||
```ts |
||||
// before |
||||
import React, { ReactElement } from 'react'; |
||||
import css from 'emotion'; |
||||
import { GrafanaTheme } from '@grafana/data'; |
||||
import { useStyles } from '@grafana/ui'; |
||||
|
||||
function Component(): ReactElement | null { |
||||
const styles = useStyles(getStyles); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({ |
||||
myStyle: css` |
||||
background: ${theme.colors.bodyBg}; |
||||
display: flex; |
||||
`, |
||||
}); |
||||
|
||||
// after |
||||
import React, { ReactElement } from 'react'; |
||||
import { css } from '@emotion/css'; |
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { useStyles2 } from '@grafana/ui'; |
||||
|
||||
function Component(): ReactElement | null { |
||||
const theme = useStyles2(getStyles); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
myStyle: css` |
||||
background: ${theme.colors.background.canvas}; |
||||
display: flex; |
||||
`, |
||||
}); |
||||
``` |
||||
|
||||
**How to use the theme in a functional component** |
||||
|
||||
```ts |
||||
// before |
||||
import React, { ReactElement } from 'react'; |
||||
import { useTheme } from '@grafana/ui'; |
||||
|
||||
function Component(): ReactElement | null { |
||||
const theme = useTheme(); |
||||
} |
||||
|
||||
// after |
||||
import React, { ReactElement } from 'react'; |
||||
import { useTheme2 } from '@grafana/ui'; |
||||
|
||||
function Component(): ReactElement | null { |
||||
const theme = useTheme2(); |
||||
// Your component has access to the theme variables now |
||||
} |
||||
``` |
||||
|
||||
**How to use the theme in a class component** |
||||
|
||||
```ts |
||||
// before |
||||
import React from 'react'; |
||||
import { Themeable, withTheme } from '@grafana/ui'; |
||||
|
||||
type Props = {} & Themeable; |
||||
|
||||
class Component extends React.Component<Props> { |
||||
render() { |
||||
const { theme } = this.props; |
||||
// Your component has access to the theme variables now |
||||
} |
||||
} |
||||
|
||||
export default withTheme(Component); |
||||
|
||||
// after |
||||
import React from 'react'; |
||||
import { Themeable2, withTheme2 } from '@grafana/ui'; |
||||
|
||||
type Props = {} & Themeable2; |
||||
|
||||
class Component extends React.Component<Props> { |
||||
render() { |
||||
const { theme } = this.props; |
||||
// Your component has access to the theme variables now |
||||
} |
||||
} |
||||
|
||||
export default withTheme2(Component); |
||||
``` |
||||
|
||||
**Gradually migrating components** |
||||
|
||||
If you need to use both the v1 and v2 themes due to using migrated and non-migrated components in the same context, use the `v1` property on the `v2` theme as described in the following example. |
||||
|
||||
```ts |
||||
function Component(): ReactElement | null { |
||||
const theme = useTheme2(); |
||||
return ( |
||||
<NonMigrated theme={theme.v1}> |
||||
<Migrated theme={theme] /> |
||||
</NonMigrate> |
||||
); |
||||
}; |
||||
``` |
||||
|
||||
## From version 6.2.x to 7.4.0 |
||||
|
||||
### Legend components |
||||
|
||||
The Legend components have been refactored and introduced the following changes within the `@grafana/ui` package. |
||||
|
||||
```ts |
||||
// before |
||||
import { LegendItem, LegendOptions, GraphLegend } from '@grafana/ui'; |
||||
|
||||
// after |
||||
import { VizLegendItem, VizLegendOptions, VizLegend } from '@grafana/ui'; |
||||
``` |
||||
|
||||
- `LegendPlacement` has been updated from `'under' | 'right' | 'over'` to `'bottom' | 'right'` so you can not place the legend above the visualization anymore. |
||||
- The `isVisible` in the `LegendItem` has been renamed to `disabled` in `VizLegendItem`. |
||||
|
||||
## From version 6.5.x to 7.3.0 |
||||
|
||||
### getColorForTheme changes |
||||
|
||||
The `getColorForTheme` function arguments have changed from `(color: ColorDefinition, theme?: GrafanaThemeType)` to `(color: string, theme: GrafanaTheme)`. |
||||
|
||||
```ts |
||||
// before |
||||
const color: ColorDefinition = { |
||||
hue: 'green'; |
||||
name: 'dark-green'; |
||||
variants: { |
||||
light: '#19730E' |
||||
dark: '#37872D' |
||||
}; |
||||
} |
||||
const themeType: GrafanaThemeType = 'dark'; |
||||
const themeColor = getColorForTheme(color, themeType); |
||||
|
||||
// after |
||||
const color = 'green'; |
||||
const theme: GrafanaTheme = useTheme(); |
||||
const themeColor = getColorForTheme(color, theme); |
||||
|
||||
``` |
||||
|
||||
## From version 6.x.x to 7.x.x |
||||
|
||||
### What's new in Grafana 7.0? |
||||
|
||||
Grafana 7.0 introduced a whole new plugin platform based on React. The new platform supersedes the previous Angular-based plugin platform. |
||||
|
||||
Plugins built using Angular still work for the foreseeable future, but we encourage new plugin authors to develop with the new platform. |
||||
|
||||
#### New data format |
||||
|
||||
Along with the move to React, the new plugin platform introduced a new internal data format called [data frames]({{< relref "data-frames.md" >}}). |
||||
|
||||
Previously, data source plugins could send data either as time series or tables. With data frames, data sources can send any data in a table-like structure. This gives you more flexibility to visualize your data in Grafana. |
||||
|
||||
#### Improved TypeScript support |
||||
|
||||
While the previous Angular-based plugin SDK did support TypeScript, for the React platform, we’ve greatly improved the support. All our APIs are now TypeScript, which might require existing code to update to the new stricter type definitions. Grafana 7.0 also introduced several new APIs for plugin developers that take advantage of many of the new features in Grafana 7.0. |
||||
|
||||
#### Grafana Toolkit |
||||
|
||||
With Grafana 7.0, we released a new tool for making it easier to develop plugins. Before, you’d use Gulp, Grunt, or similar tools to generate the minified assets. Grafana Toolkit takes care of building and testing your plugin without complicated configuration files. |
||||
|
||||
For more information, refer to [@grafana/toolkit](https://www.npmjs.com/package/@grafana/toolkit). |
||||
|
||||
#### Field options |
||||
|
||||
Grafana 7.0 introduced the concept of _field options_, a new way of configuring your data before it gets visualized. Since this was not available in previous versions, any plugin that enables field-based configuration will not work in previous versions of Grafana. |
||||
|
||||
For plugins prior to Grafana 7.0, all options are considered _Display options_. The tab for field configuration isn't available. |
||||
|
||||
#### Backend plugins |
||||
|
||||
While backend plugins were available as an experimental feature in previous versions of Grafana, the support has been greatly improved for Grafana 7. Backend plugins for Grafana 7.0 are backwards-compatible and will continue to work. However, the old backend plugin system has been deprecated, and we recommend that you use the new SDK for backend plugins. |
||||
|
||||
Since Grafana 7.0 introduced [signing of backend plugins]({{< relref "../../plugins/plugin-signatures.md" >}}), community plugins won’t load by default if they’re unsigned. |
||||
|
||||
To learn more, refer to [Backend plugins]({{< relref "backend" >}}). |
||||
|
||||
### Migrate a plugin from Angular to React |
||||
|
||||
If you’re looking to migrate a plugin to the new plugin platform, then we recommend that you release it under a new major version. Consider keeping a release branch for the previous version to be able to roll out patch releases for versions prior to Grafana 7. |
||||
|
||||
While there's no 1-to-1 migration path from an Angular plugin to the new React platform, from early adopters, we’ve learned that one of the easiest ways to migrate is to: |
||||
|
||||
1. Create a new branch called `migrate-to-react`. |
||||
1. Start from scratch with one of the templates provided by Grafana Toolkit. |
||||
1. Move the existing code into the new plugin incrementally, one component at a time. |
||||
|
||||
#### Migrate a panel plugin |
||||
|
||||
Prior to Grafana 7.0, you would export a MetricsPanelCtrl from module.ts. |
||||
|
||||
**src/module.ts** |
||||
|
||||
```ts |
||||
import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk'; |
||||
|
||||
class MyPanelCtrl extends MetricsPanelCtrl { |
||||
// ... |
||||
} |
||||
|
||||
export { MyPanelCtrl as PanelCtrl }; |
||||
``` |
||||
|
||||
Starting with 7.0, plugins now export a PanelPlugin from module.ts where MyPanel is a React component containing the props from PanelProps. |
||||
|
||||
**src/module.ts** |
||||
|
||||
```ts |
||||
import { PanelPlugin } from '@grafana/data'; |
||||
|
||||
export const plugin = new PanelPlugin<MyOptions>(MyPanel); |
||||
``` |
||||
|
||||
**src/MyPanel.tsx** |
||||
|
||||
```ts |
||||
import { PanelProps } from '@grafana/data'; |
||||
|
||||
interface Props extends PanelProps<SimpleOptions> {} |
||||
|
||||
export const MyPanel: React.FC<Props> = ({ options, data, width, height }) => { |
||||
// ... |
||||
}; |
||||
``` |
||||
|
||||
#### Migrate a data source plugin |
||||
|
||||
While all plugins are different, we'd like to share a migration process that has worked for some of our users. |
||||
|
||||
1. Define your configuration model and `ConfigEditor`. For many plugins, the configuration editor is the simplest component so it's a good candidate to start with. |
||||
1. Implement the `testDatasource()` method on the class that extends `DataSourceApi` using the settings in your configuration model to make sure you can successfully configure and access the external API. |
||||
1. Implement the `query()` method. At this point, you can hard-code your query, because we haven’t yet implemented the query editor. The `query()` method supports both the new data frame response and the old TimeSeries response, so don’t worry about converting to the new format just yet. |
||||
1. Implement the `QueryEditor`. How much work this requires depends on how complex your query model is. |
||||
|
||||
By now, you should be able to release your new version. |
||||
|
||||
To fully migrate to the new plugin platform, convert the time series response to a data frame response. |
||||
|
||||
#### Migrate to data frames |
||||
|
||||
Before 7.0, data source and panel plugins exchanged data using either time series or tables. Starting with 7.0, plugins use the new data frame format to pass data from data sources to panels. |
||||
|
||||
Grafana 7.0 is backward compatible with the old data format used in previous versions. Panels and data sources using the old format will still work with plugins using the new data frame format. |
||||
|
||||
The `DataQueryResponse` returned by the `query` method can be either a [LegacyResponseData](https://grafana.com/docs/grafana/v8.3/packages_api/data/legacyresponsedata/) or a [DataFrame](https://grafana.com/docs/grafana/v8.3/packages_api/data/dataframe/). |
||||
|
||||
The [toDataFrame()](https://grafana.com/docs/grafana/v8.3/packages_api/data/todataframe/) function converts a legacy response, such as `TimeSeries` or `Table`, to a `DataFrame`. Use it to gradually move your code to the new format. |
||||
|
||||
```ts |
||||
import { toDataFrame } from '@grafana/data'; |
||||
``` |
||||
|
||||
```ts |
||||
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> { |
||||
return { |
||||
data: options.targets.map(query => { |
||||
const timeSeries: TimeSeries = await doLegacyRequest(query); |
||||
return toDataFrame(timeSeries); |
||||
} |
||||
}; |
||||
} |
||||
``` |
||||
|
||||
For more information, refer to [Data frames]({{< relref "data-frames.md">}}). |
||||
|
||||
### Troubleshoot plugin migration |
||||
|
||||
As of Grafana 7.0, backend plugins can now be cryptographically signed to verify their origin. By default, Grafana ignores unsigned plugins. For more information, refer to [Allow unsigned plugins]({{< relref "../../plugins/plugin-signatures.md#allow-unsigned-plugins" >}}). |
@ -1,47 +0,0 @@ |
||||
+++ |
||||
title = "Package a plugin" |
||||
type = "docs" |
||||
aliases = ["/docs/grafana/v8.3/developers/plugins/share-a-plugin/"] |
||||
+++ |
||||
|
||||
# Package a plugin |
||||
|
||||
You've just built your first plugin, and now you want to share it with the world. In this guide, you'll learn how to package and share your plugin with others. |
||||
|
||||
For Grafana to be able to load a plugin, it first needs to be built. When you build a plugin from source, a `dist` directory is created that contains the production build, or _plugin assets_, for your plugin. |
||||
|
||||
When the Grafana server starts, it recursively looks in the plugin directory for any directory that contains a `plugin.json` file and tries to load the plugin assets in the same directory. |
||||
|
||||
There are three steps needed to package a plugin: |
||||
|
||||
- Building the plugin |
||||
- Signing the plugin |
||||
- Archiving the plugin |
||||
|
||||
1. Build the plugin |
||||
|
||||
``` |
||||
yarn install --pure-lockfile |
||||
yarn build |
||||
``` |
||||
|
||||
1. (Optional) If your data source plugin has a backend plugin, build it as well. |
||||
|
||||
``` |
||||
mage |
||||
``` |
||||
|
||||
1. [Sign the plugin]({{< relref "sign-a-plugin.md" >}}). |
||||
|
||||
1. Create a ZIP archive of the `dist` directory. |
||||
|
||||
``` |
||||
mv dist/ myorg-simple-panel |
||||
zip myorg-simple-panel-1.0.0.zip myorg-simple-panel -r |
||||
``` |
||||
|
||||
## Publish your plugin on Grafana.com |
||||
|
||||
The best way to share your plugin with the world is to publish it on [Grafana Plugins](https://grafana.com/plugins). By having your plugin published on Grafana.com, more users will be able to discover your plugin. |
||||
|
||||
To publish a plugin to [Grafana Plugins](https://grafana.com/grafana/plugins), create a pull request to the [Grafana Plugin Repository](https://github.com/grafana/grafana-plugin-repository). Please note that both the source code and the packaged plugin archive need to be publicly available. |
@ -1,128 +0,0 @@ |
||||
+++ |
||||
title = "Sign a plugin" |
||||
+++ |
||||
|
||||
# Sign a plugin |
||||
|
||||
Signing a plugin allows Grafana to verify the authenticity of the plugin with [signature verification]({{< relref "../../plugins/plugin-signatures.md" >}}). This gives users a way to make sure plugins haven't been tampered with. All Grafana Labs-authored backend plugins, including Enterprise plugins, are signed. |
||||
|
||||
> **Important:** Future versions of Grafana will require all plugins to be signed. |
||||
|
||||
Before you can sign your plugin, you need to decide whether you want to sign it as a _public_ or a _private_ plugin. |
||||
|
||||
If you want to make your plugin publicly available outside of your organization, you need to sign your plugin under a _community_ or _commercial_ [signature level](#plugin-signature-levels). Public plugins are available from [grafana.com/plugins](https://grafana.com/plugins) and can be installed by anyone. |
||||
|
||||
For more information on how to install public plugin, refer to [Install Grafana plugins]({{< relref "../../plugins/installation.md" >}}). |
||||
|
||||
If you intend to only use the plugin within your organization, you can to sign it under a _private_ [signature level](#plugin-signature-levels). |
||||
|
||||
## Generate an API key |
||||
|
||||
To verify ownership of your plugin, you need to generate an API key that you'll use every time you need to sign a new version of your plugin. |
||||
|
||||
1. [Create a Grafana Cloud account](https://grafana.com/signup). |
||||
|
||||
1. Make sure that the first part of the plugin ID matches the slug of your Grafana Cloud account. |
||||
|
||||
You can find the plugin ID in the `plugin.json` file inside your plugin directory. For example, if your account slug is `acmecorp`, you need to prefix the plugin ID with `acmecorp-`. |
||||
|
||||
1. [Create a Grafana Cloud API key](https://grafana.com/docs/grafana-cloud/reference/create-api-key/) with the **PluginPublisher** role. |
||||
|
||||
## Sign a public plugin |
||||
|
||||
Public plugins need to be reviewed by the Grafana team before you can sign them. |
||||
|
||||
1. Submit your plugin for review by creating a pull request in the [grafana-plugin-repository](https://github.com/grafana/grafana-plugin-repository). |
||||
1. When your plugin is approved, you're granted a plugin signature level. **Without a plugin signature level, you won't be able to sign your plugin**. |
||||
1. In your plugin directory, sign the plugin with the API key you just created. Grafana Toolkit creates a [MANIFEST.txt](#plugin-manifest) file in the `dist` directory of your plugin. |
||||
|
||||
```bash |
||||
export GRAFANA_API_KEY=<YOUR_API_KEY> |
||||
npx @grafana/toolkit plugin:sign |
||||
``` |
||||
|
||||
> **Note:** If running NPM 7+ the `npx` commands mentioned in this article may hang. The workaround is to use `npx --legacy-peer-deps <command to run>`. |
||||
|
||||
## Sign a private plugin |
||||
|
||||
1. In your plugin directory, sign the plugin with the API key you just created. Grafana Toolkit creates a [MANIFEST.txt](#plugin-manifest) file in the `dist` directory of your plugin. |
||||
|
||||
The `rootUrls` flag accepts a comma-separated list of URLs to the Grafana instances where you intend to install the plugin. |
||||
|
||||
```bash |
||||
export GRAFANA_API_KEY=<YOUR_API_KEY> |
||||
npx @grafana/toolkit plugin:sign --rootUrls https://example.com/grafana |
||||
``` |
||||
|
||||
## Plugin signature levels |
||||
|
||||
To sign a plugin, you need to decide the _signature level_ you want to sign it under. The signature level of your plugin determines how you can distribute it. |
||||
|
||||
You can sign your plugin under three different _signature levels_. |
||||
|
||||
| **Plugin Level** | **Paid Subscription Required?** | **Description** | |
||||
| ---------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
||||
| Private | No;<br>Free of charge | <p>You can create and sign a Private Plugin for any technology at no charge.</p><p>Private Plugins are for use on your own Grafana. They may not be distributed to the Grafana community, and are not published in the Grafana catalog.</p> | |
||||
| Community | No;<br>Free of charge | <p>You can create, sign and distribute plugins at no charge, provided that all dependent technologies are open source and not for profit.</p><p>Community Plugins are published in the official Grafana catalog, and are available to the Grafana community.</p> | |
||||
| Commercial | Yes;<br>Commercial Plugin Subscription required | <p>You can create, sign and distribute plugins with dependent technologies that are closed source or commercially backed, by entering into a Commercial Plugin Subscription with Grafana Labs.</p><p>Commercial Plugins are published on the official Grafana catalog, and are available to the Grafana community.</p> | |
||||
|
||||
For instructions on how to sign a plugin under the Community and Commercial signature level, refer to [Sign a public plugin](#sign-a-public-plugin). |
||||
|
||||
For instructions on how to sign a plugin under the Private signature level, refer to [Sign a private plugin](#sign-a-private-plugin). |
||||
|
||||
## Plugin manifest |
||||
|
||||
For Grafana to verify the digital signature of a plugin, the plugin must include a signed manifest file, _MANIFEST.txt_. The signed manifest file contains two sections: |
||||
|
||||
- **Signed message -** The signed message contains plugin metadata and plugin files with their respective checksums (SHA256). |
||||
- **Digital signature -** The digital signature is created by encrypting the signed message using a private key. Grafana has a public key built-in that can be used to verify that the digital signature have been encrypted using expected private key. |
||||
|
||||
**Example manifest file:** |
||||
|
||||
```txt |
||||
-----BEGIN PGP SIGNED MESSAGE----- |
||||
Hash: SHA512 |
||||
|
||||
{ |
||||
"manifestVersion": "2.0.0", |
||||
"signatureType": "community", |
||||
"signedByOrg": "myorgid", |
||||
"signedByOrgName": "My Org", |
||||
"plugin": "myorgid-simple-panel", |
||||
"version": "1.0.0", |
||||
"time": 1602753404133, |
||||
"keyId": "7e4d0c6a708866e7", |
||||
"files": { |
||||
"LICENSE": "12ab7a0961275f5ce7a428e662279cf49bab887d12b2ff7bfde738346178c28c", |
||||
"module.js.LICENSE.txt": "0d8f66cd4afb566cb5b7e1540c68f43b939d3eba12ace290f18abc4f4cb53ed0", |
||||
"module.js.map": "8a4ede5b5847dec1c6c30008d07bef8a049408d2b1e862841e30357f82e0fa19", |
||||
"plugin.json": "13be5f2fd55bee787c5413b5ba6a1fae2dfe8d2df6c867dadc4657b98f821f90", |
||||
"README.md": "2d90145b28f22348d4f50a81695e888c68ebd4f8baec731fdf2d79c8b187a27f", |
||||
"module.js": "b4b6945bbf3332b08e5e1cb214a5b85c82557b292577eb58c8eb1703bc8e4577" |
||||
} |
||||
} |
||||
-----BEGIN PGP SIGNATURE----- |
||||
Version: OpenPGP.js v4.10.1 |
||||
Comment: https://openpgpjs.org |
||||
|
||||
wqEEARMKAAYFAl+IE3wACgkQfk0ManCIZudpdwIHTCqjVzfm7DechTa7BTbd |
||||
+dNIQtwh8Tv2Q9HksgN6c6M9nbQTP0xNHwxSxHOI8EL3euz/OagzWoiIWulG |
||||
7AQo7FYCCQGucaLPPK3tsWaeFqVKy+JtQhrJJui23DAZLSYQYZlKQ+nFqc9x |
||||
T6scfmuhWC/TOcm83EVoCzIV3R5dOTKHqkjIUg== |
||||
=GdNq |
||||
-----END PGP SIGNATURE----- |
||||
``` |
||||
|
||||
## Troubleshooting issues while signing your plugin |
||||
|
||||
### Why am I getting a "Modified signature" in Grafana? |
||||
|
||||
Due to an issue when signing the plugin on Windows, grafana-toolkit generates an invalid MANIFEST.txt. You can fix this by replacing all double backslashes, `\\`, with a forward slash, `/` in the MANIFEST.txt file. You need to do this every time you sign your plugin. |
||||
|
||||
### Error signing manifest: Field is required: rootUrls |
||||
|
||||
If you're trying to sign a **public** plugin, this means that your plugin doesn't have a plugin signature level assigned to it yet. A Grafana team member will assign a signature level to your plugin once it has been reviewed and approved. For more information, refer to [Sign a public plugin](#sign-a-public-plugin). |
||||
|
||||
If you're trying to sign a **private** plugin, this means that you need to add a `rootUrls` flag to the `plugin:sign` command. The `rootUrls` must match the [root_url]({{< relref "../../administration/configuration.md#root_url" >}}) configuration. For more information, refer to [Sign a private plugin](#sign-a-private-plugin). |
||||
|
||||
If you still get this error, make sure that the API key was generated by a Grafana Cloud account that matches the first part of the plugin ID. |
@ -1,121 +0,0 @@ |
||||
+++ |
||||
title = "Working with data frames" |
||||
+++ |
||||
|
||||
# Working with data frames |
||||
|
||||
The data frame is a columnar data structure which allows efficient querying of large amounts of data. Since data frames are a central concept when developing plugins for Grafana, in this guide we'll look at some ways you can use them. |
||||
|
||||
The DataFrame interface contains a `name` and an array of `fields` where each field contains the name, type, and the values for the field. |
||||
|
||||
> **Note:** If you're looking to migrate an existing plugin to use the data frame format, refer to [Migrate to data frames]({{< relref "migration-guide.md#migrate-to-data-frames" >}}). |
||||
|
||||
## Create a data frame |
||||
|
||||
If you build a data source plugin, then you'll most likely want to convert a response from an external API to a data frame. Let's look at how to create a data frame. |
||||
|
||||
Let's start with creating a simple data frame that represents a time series. The easiest way to create a data frame is to use the toDataFrame function. |
||||
|
||||
```ts |
||||
// Need to be of the same length. |
||||
const timeValues = [1599471973065, 1599471975729]; |
||||
const numberValues = [12.3, 28.6]; |
||||
|
||||
// Create data frame from values. |
||||
const frame = toDataFrame({ |
||||
name: 'http_requests_total', |
||||
fields: [ |
||||
{ name: 'Time', type: FieldType.time, values: timeValues }, |
||||
{ name: 'Value', type: FieldType.number, values: numberValues }, |
||||
], |
||||
}); |
||||
``` |
||||
|
||||
> **Note:** Data frames representing time series contain at least a `time` field, and a `number` field. By convention, built-in plugins use `Time` and `Value` as field names for data frames containing time series data. |
||||
|
||||
As you can see from the example, creating data frames like this requires that your data is already stored as columnar data. If you already have the records in the form of an array of objects, then you can pass it to `toDataFrame` which tries to guess the schema based on the types and names of the objects in the array. If you're creating complex data frames this way, then be sure to verify that you get the schema you expect. |
||||
|
||||
```ts |
||||
const series = [ |
||||
{ Time: 1599471973065, Value: 12.3 }, |
||||
{ Time: 1599471975729, Value: 28.6 }, |
||||
]; |
||||
|
||||
const frame = toDataFrame(series); |
||||
frame.name = 'http_requests_total'; |
||||
``` |
||||
|
||||
## Read values from a data frame |
||||
|
||||
When you're building a panel plugin, the data frames returned by the data source are available from the `data` prop in your panel component. |
||||
|
||||
```ts |
||||
const SimplePanel: React.FC<Props> = ({ data }) => { |
||||
const frame = data.series[0]; |
||||
|
||||
// ... |
||||
}; |
||||
``` |
||||
|
||||
Before you start reading the data, think about what data you expect. For example, to visualize a time series we'd need at least one time field, and one number field. |
||||
|
||||
```ts |
||||
const timeField = frame.fields.find((field) => field.type === FieldType.time); |
||||
const valueField = frame.fields.find((field) => field.type === FieldType.number); |
||||
``` |
||||
|
||||
Other types of visualizations might need multiple dimensions. For example, a bubble chart that uses three numeric fields: the X-axis, Y-axis, and one for the radius of each bubble. In this case, instead of hard coding the field names, we recommend that you let the user choose the field to use for each dimension. |
||||
|
||||
```ts |
||||
const x = frame.fields.find((field) => field.name === xField); |
||||
const y = frame.fields.find((field) => field.name === yField); |
||||
const size = frame.fields.find((field) => field.name === sizeField); |
||||
|
||||
for (let i = 0; i < frame.length; i++) { |
||||
const row = [x?.values.get(i), y?.values.get(i), size?.values.get(i)]; |
||||
|
||||
// ... |
||||
} |
||||
``` |
||||
|
||||
Alternatively, you can use the DataFrameView, which gives you an array of objects that contain a property for each field in the frame. |
||||
|
||||
```ts |
||||
const view = new DataFrameView(frame); |
||||
|
||||
view.forEach((row) => { |
||||
console.log(row[options.xField], row[options.yField], row[options.sizeField]); |
||||
}); |
||||
``` |
||||
|
||||
## Display values from a data frame |
||||
|
||||
Field options let the user control how Grafana displays the data in a data frame. |
||||
|
||||
To apply the field options to a value, use the `display` method on the corresponding field. The result contains information such as the color and suffix to use when display the value. |
||||
|
||||
```ts |
||||
const valueField = frame.fields.find((field) => field.type === FieldType.number); |
||||
|
||||
return ( |
||||
<div> |
||||
{valueField |
||||
? valueField.values.toArray().map((value) => { |
||||
const displayValue = valueField.display!(value); |
||||
return ( |
||||
<p style={{ color: displayValue.color }}> |
||||
{displayValue.text} {displayValue.suffix ? displayValue.suffix : ''} |
||||
</p> |
||||
); |
||||
}) |
||||
: null} |
||||
</div> |
||||
); |
||||
``` |
||||
|
||||
To apply field options to the name of a field, use getFieldDisplayName. |
||||
|
||||
```ts |
||||
const valueField = frame.fields.find((field) => field.type === FieldType.number); |
||||
const valueFieldName = getFieldDisplayName(valueField, frame); |
||||
``` |
Loading…
Reference in new issue